Angular CDK: How to set Inputs in a ComponentPortal

25,421

Solution 1

You can create a custom injector and inject it to the component portal you create.

createInjector(dataToPass): PortalInjector {
    const injectorTokens = new WeakMap();
    injectorTokens.set(CONTAINER_DATA, dataToPass);
    return new PortalInjector(this._injector, injectorTokens);
}

CONTAINER_DATA is a custom injector (InjectorToken) created by -

export const CONTAINER_DATA = new InjectionToken<{}>('CONTAINER_DATA');

To consume created injector, use -

let containerPortal = new ComponentPortal(ComponentToPort, null, this.createInjector({
          data1,
          data2
        }));

overlay.attach(containerPortal);

overlay is an instance of OverlayRef (Which is Portal Outlet)

Inside ComponentToPort, you will need to inject the created injector -

@Inject(CONTAINER_DATA) public componentData: any

More on this here.

Solution 2

If you are using Angular 10+ and following Awadhoot's answer, PortalInjector is now deprecated so instead of:

new PortalInjector(this.injector, new WeakMap([[SOME_TOKEN, data]]))

You now have:

Injector.create({
  parent: this.injector,
  providers: [
    { provide: SOME_TOKEN, useValue: data }
  ]
})

Solution 3

Can set component inputs (or bind to outputs as an observable) in this way:

portal = new ComponentPortal(MyComponent);
this.portalHost = new DomPortalHost(
      this.elementRef.nativeElement,
      this.componentFactoryResolver,
      this.appRef,
      this.injector
    );

const componentRef = this.portalHost.attach(this.portal);
componentRef.instance.myInput = data;
componentRef.instance.myOutput.subscribe(...);
componentRef.changeDetectorRef.detectChanges();

Solution 4

this seems a bit more simple, using the cdkPortalOutlet and the (attached) emitter

import {Component, ComponentRef, AfterViewInit, TemplateRef, ViewChild, ViewContainerRef, Input, OnInit} from '@angular/core';
import {ComponentPortal, CdkPortalOutletAttachedRef, Portal, TemplatePortal, CdkPortalOutlet} from '@angular/cdk/portal';

/**
 * @title Portal overview
 */
@Component({
  selector: 'cdk-portal-overview-example',
  template: '<ng-template [cdkPortalOutlet]="componentPortal" (attached)=foo($event)></ng-template>',
  styleUrls: ['cdk-portal-overview-example.css'],
})
export class CdkPortalOverviewExample implements OnInit {
  componentPortal: ComponentPortal<ComponentPortalExample>;

  constructor(private _viewContainerRef: ViewContainerRef) {}

  ngOnInit() {
    this.componentPortal = new ComponentPortal(ComponentPortalExample);
  }

  foo(ref: CdkPortalOutletAttachedRef) {
    ref = ref as ComponentRef<ComponentPortalExample>;
    ref.instance.message = 'zap';
  }
}

@Component({
  selector: 'component-portal-example',
  template: 'Hello, this is a component portal {{message}}'
})
export class ComponentPortalExample {
  @Input() message: string;
}

Solution 5

You can inject data to ComponentPortal with specific injector passed on 3rd param of ComponentPortal

fix syntax issue:

Can't resolve all parameters for Component: ([object Object], [object Object], ?

This is the code

export const PORTAL_DATA = new InjectionToken<{}>('PortalData');

class ContainerComponent {
  constructor(private injector: Injector, private overlay: Overlay) {}

  attachPortal() {
    const componentPortal = new ComponentPortal(
      ComponentToPort,
      null,
      this.createInjector({id: 'first-data'})
    );
    this.overlay.create().attach(componentPortal);
  }

  private createInjector(data): PortalInjector {

    const injectorTokens = new WeakMap<any, any>([
      [PORTAL_DATA, data],
    ]);

    return new PortalInjector(this.injector, injectorTokens);
  }
}

class ComponentToPort {
  constructor(@Inject(PORTAL_DATA) public data ) {
    console.log(data);
  }
}
Share:
25,421
JoG
Author by

JoG

Updated on July 09, 2022

Comments

  • JoG
    JoG almost 2 years

    I would like to use the new Portal from material CDK to inject dynamic content in multiple part of a form.

    I have a complex form structure and the goal is to have a form that specify multiple place where sub components could (or not) inject templates.

    Maybe the CDK Portal is not the best solution for this?

    I tried something but I am sure it is not the way of doing: https://stackblitz.com/edit/angular-yuz1kg

    I tried also with new ComponentPortal(MyPortalComponent) but how can we set Inputs on it ? Usually is something like componentRef.component.instance.myInput

  • Joe
    Joe over 6 years
    When I try to do this I get the following: Can't resolve all parameters for ComponentToPort: ([object Object], [object Object], ?). Where the ? is the CONTAINER_DATA
  • Awadhoot
    Awadhoot over 6 years
    Probably inside 'ComponentToPort', you will need to import CONTAINER_DATA from the location you created it. That might resolve the issue.
  • wosevision
    wosevision almost 6 years
    Is this actually the only way to accomplish this? The problem with using injected tokens is you aren't afforded any kind of change detection – no ngOnChanges, no async | pipe, nada. Short of passing in an Observable as the token, it seems you're left with a purely static value. Is this really the case, there's there's no way to leverage @Inputs?
  • Sunil Garg
    Sunil Garg about 5 years
    you need to explain the code as well why this will work
  • Sunil Garg
    Sunil Garg about 5 years
    what about @output?
  • UrbKr
    UrbKr about 5 years
    @wosevision Perhaps by using a template portal instead of a component portal. You can bind inputs and outputs in a template and then just use the overlay library to position it.
  • SeppeDev
    SeppeDev about 5 years
    You need to declare the injected componentData in the constructor of the ComponentToPort constructor(@Inject(CONTAINER_DATA) public componentData: any ) {}
  • Bob
    Bob almost 5 years
    a much easier solution than the other answers, at least for me !
  • Christophe Le Besnerais
    Christophe Le Besnerais over 4 years
    indeed so much easier than creating a custom injector !
  • Rusty Rob
    Rusty Rob over 4 years
    there's also an @output() that emits the ref, see: github.com/angular/components/blob/master/src/cdk/portal/…
  • Mauro Insacco
    Mauro Insacco over 4 years
    best answer, the injector way makes you write components in a non "standard" way and has limitations
  • muuvmuuv
    muuvmuuv about 4 years
    What about ngContent? If my ComponentToPort has a <ng-content> how do I pass data to it?
  • Gaurav Panwar
    Gaurav Panwar about 4 years
    This is really a very good soltution for the given problem. I have tried all which mentioned above but this one is the smallest and found best solution to pass data into component portals.
  • aruno
    aruno almost 4 years
    So is this sort of bypassing the proper @Input() mechanism, and/or does it matter since you explicitly call detectChanges - or is that the whole point :-)
  • aruno
    aruno almost 4 years
    @muuvmuuv maybe you'd have to pass another portal in instead of ng-content?
  • aruno
    aruno almost 4 years
    Note: The description for DomPortalOutlet is "A PortalOutlet for attaching portals to an arbitrary DOM element outside of the Angular application context." - So it's really intended for putting some Angular component on a completely random element on your page. If you're using Angular alone you probably want ng-template approach (as you showed above).
  • aruno
    aruno almost 4 years
    If anyone is trying to put something outside of the Angular context then also check out Angular Elements (angular.io/guide/elements).
  • Hisham
    Hisham over 3 years
    PortalInjector is deprecated now. It seems that Injector.create is recommended instead.
  • Andrei Cojea
    Andrei Cojea about 3 years
    This has a huge advantage besides simplicity, it allows you to make the component agnostic about the overlay. A potential downside is that it might execute ngOnInit before the inputs are set (didn't check this yet).
  • David
    David almost 3 years
    This is the best solution so far, but be aware that ngOnChanges does not get called
  • David
    David almost 3 years
    Addition to my comment above: here is the github issue for adding an API to set inputs github.com/angular/angular/issues/22567
  • Stephane
    Stephane about 2 years
    And we can use Material elements in the ComponentToPort ? Say a <mat-icon> ?
  • Rodrigo Assis Neves
    Rodrigo Assis Neves about 2 years
    Thanks, exactly what I was looking for with the deprecation of PortalInjector
  • WardenUnleashed
    WardenUnleashed about 2 years
    This won't work if the portal host is abstracted away from where you construct the portal, for example if you are passing them to a service or the like.
  • Coderer
    Coderer about 2 years
    This should be the accepted answer. The OP wanted to get a ref to the dynamically created component instance and call methods on it. Using the attached event is the right way to do that.