Angular CDK: How to set Inputs in a ComponentPortal
Asked Answered
H

8

45

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

Heterosporous answered 24/11, 2017 at 9:12 Comment(0)
E
39

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.

Ethic answered 3/1, 2018 at 9:34 Comment(10)
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_DATACapsulize
Probably inside 'ComponentToPort', you will need to import CONTAINER_DATA from the location you created it. That might resolve the issue.Ethic
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?Dowel
what about @output?Kerge
@Dowel 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.Vivacious
You need to declare the injected componentData in the constructor of the ComponentToPort constructor(@Inject(CONTAINER_DATA) public componentData: any ) {}Adara
What about ngContent? If my ComponentToPort has a <ng-content> how do I pass data to it?Galyak
@Galyak maybe you'd have to pass another portal in instead of ng-content?Afresh
PortalInjector is deprecated now. It seems that Injector.create is recommended instead.Cordite
And we can use Material elements in the ComponentToPort ? Say a <mat-icon> ?Masticatory
D
46

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 }
  ]
})
Doradorado answered 14/10, 2020 at 15:7 Comment(1)
Thanks, exactly what I was looking for with the deprecation of PortalInjectorPickled
E
39

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.

Ethic answered 3/1, 2018 at 9:34 Comment(10)
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_DATACapsulize
Probably inside 'ComponentToPort', you will need to import CONTAINER_DATA from the location you created it. That might resolve the issue.Ethic
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?Dowel
what about @output?Kerge
@Dowel 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.Vivacious
You need to declare the injected componentData in the constructor of the ComponentToPort constructor(@Inject(CONTAINER_DATA) public componentData: any ) {}Adara
What about ngContent? If my ComponentToPort has a <ng-content> how do I pass data to it?Galyak
@Galyak maybe you'd have to pass another portal in instead of ng-content?Afresh
PortalInjector is deprecated now. It seems that Injector.create is recommended instead.Cordite
And we can use Material elements in the ComponentToPort ? Say a <mat-icon> ?Masticatory
P
38

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();
Panjandrum answered 13/12, 2018 at 11:3 Comment(8)
a much easier solution than the other answers, at least for me !Dada
indeed so much easier than creating a custom injector !Halloween
best answer, the injector way makes you write components in a non "standard" way and has limitationsAspersion
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 :-)Afresh
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).Subclavian
This is the best solution so far, but be aware that ngOnChanges does not get calledAvuncular
Addition to my comment above: here is the github issue for adding an API to set inputs github.com/angular/angular/issues/22567Avuncular
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.Isomorph
R
21

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;
    }
Rameriz answered 26/2, 2020 at 1:29 Comment(4)
there's also an @output() that emits the ref, see: github.com/angular/components/blob/master/src/cdk/portal/…Rameriz
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.Phenobarbital
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.Henebry
I tried many many things, only this worked thanks!Aniconic
S
10

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);
  }
}
Sleave answered 13/7, 2018 at 11:11 Comment(1)
you need to explain the code as well why this will workKerge
P
5

After version angular 9 'DomPortalHost' has been deprecated and this has been changed to 'DomPortalOutlet'. so now it will like:

this.portalHost = new DomPortalOutlet(
   this.elementRef.nativeElement,
   this.componentFactoryResolver,
   this.appRef,
  this.injector
);

const componentRef = this.portalHost.attachComponentPortal(this.portal); componentRef.instance.myInput = data;

Apart from this I felt the best solution for this is just bind the (attached) event and set inputs there:

<ng-template [cdkPortalOutlet]="yourPortal" (attached)="setInputs($event)"> </ng-template>

and in ts set your inputs:

setInputs(portalOutletRef: CdkPortalOutletAttachedRef) {
    portalOutletRef = portalOutletRef as ComponentRef<myComponent>;
    portalOutletRef.instance.inputPropertyName = data;
}
Phenobarbital answered 28/5, 2020 at 10:12 Comment(2)
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).Afresh
If anyone is trying to put something outside of the Angular context then also check out Angular Elements (angular.io/guide/elements).Afresh
L
5

I know, the question is 4 years old, but maybe helpful for someone: In current version of CDK the ComponentPortal has a new function named "setInput":

setInputs(portalOutletRef: CdkPortalOutletAttachedRef) {
portalOutletRef = portalOutletRef as ComponentRef<BaseAuditView>;

portalOutletRef.setInput('prop1', this.prop1);
portalOutletRef.setInput('prop2', this.prop2);

}

if you using this function, angular`s change detections works very well!

(method) ComponentRef.setInput(name: string, value: unknown): void Updates a specified input name to a new value. Using this method will properly mark for check component using the OnPush change detection strategy. It will also assure that the OnChanges lifecycle hook runs when a dynamically created component is change-detected.

@param name — The name of an input.

@param value — The new value of an input.

Lafond answered 16/8, 2022 at 10:33 Comment(0)
J
2

No idea from what version of Angular this is working (at least from 13.3.9). But there is a much simpler way now because overlayRef.attach(portal) is now returning ComponentRef. So

    const overlayRef = this._overlay.create();
    const portal = new ComponentPortal(MyComponent);
    const cmpRef = overlayRef.attach(portal);
    cmpRef.instance.myInput = 42;

will work now

Julijulia answered 27/7, 2022 at 12:4 Comment(2)
this doesn't trigger ngOnChanges unfortunatelyDignity
You can do it manually cmpRef.changeDetectorRef.detectChanges();Julijulia

© 2022 - 2024 — McMap. All rights reserved.