43

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

Kim Kern
  • 54,283
  • 17
  • 197
  • 195
JoG
  • 962
  • 2
  • 11
  • 17

8 Answers8

44

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 }
  ]
})
Wildhammer
  • 2,017
  • 1
  • 27
  • 33
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.

billyjov
  • 2,778
  • 19
  • 35
Awadhoot
  • 737
  • 6
  • 19
  • 2
    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 – Joe Feb 12 '18 at 19:18
  • Probably inside 'ComponentToPort', you will need to import CONTAINER_DATA from the location you created it. That might resolve the issue. – Awadhoot Feb 14 '18 at 07:54
  • 13
    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 `@Input`s? – wosevision Aug 16 '18 at 13:48
  • 4
    what about @output? – Sunil Garg Apr 25 '19 at 09:27
  • @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. – UrbKr Apr 26 '19 at 20:29
  • You need to declare the injected componentData in the constructor of the ComponentToPort `constructor(@Inject(CONTAINER_DATA) public componentData: any ) {}` – SeppeDev May 15 '19 at 16:22
  • What about `ngContent`? If my `ComponentToPort` has a `` how do I pass data to it? – muuvmuuv Apr 15 '20 at 12:10
  • @muuvmuuv maybe you'd have to pass another portal in instead of ng-content? – Simon_Weaver Jul 06 '20 at 03:55
  • 4
    PortalInjector is deprecated now. It seems that Injector.create is recommended instead. – Hisham Oct 07 '20 at 18:45
  • And we can use Material elements in the `ComponentToPort` ? Say a `` ? – Stephane Apr 02 '22 at 08:37
36

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();
Karlos1337
  • 529
  • 5
  • 8
  • 1
    a much easier solution than the other answers, at least for me ! – Bob Aug 02 '19 at 14:14
  • indeed so much easier than creating a custom injector ! – Christophe Le Besnerais Jan 06 '20 at 12:28
  • 1
    best answer, the injector way makes you write components in a non "standard" way and has limitations – Mauro Insacco Mar 08 '20 at 15:10
  • 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 :-) – Simon_Weaver Jul 06 '20 at 03:25
  • 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). – Andrei Cojea May 10 '21 at 16:08
  • This is the best solution so far, but be aware that `ngOnChanges` does not get called – David Sep 24 '21 at 08:08
  • Addition to my comment above: here is the github issue for adding an API to set inputs https://github.com/angular/angular/issues/22567 – David Sep 24 '21 at 10:26
  • 1
    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. – WardenUnleashed Apr 07 '22 at 00:52
19

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;
    }
Robouste
  • 3,020
  • 4
  • 33
  • 55
Rusty Rob
  • 16,489
  • 8
  • 100
  • 116
  • there's also an @output() that emits the ref, see: https://github.com/angular/components/blob/master/src/cdk/portal/portal-directives.ts – Rusty Rob Feb 28 '20 at 03:34
  • 1
    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. – Gaurav Panwar May 28 '20 at 10:15
  • 1
    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. – Coderer Apr 18 '22 at 13:27
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);
  }
}
Sunil Garg
  • 14,608
  • 25
  • 132
  • 189
domen
  • 325
  • 2
  • 5
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;
}
Gaurav Panwar
  • 974
  • 10
  • 11
  • 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). – Simon_Weaver Jul 06 '20 at 03:58
  • 1
    If anyone is trying to put something outside of the Angular context then also check out Angular Elements (https://angular.io/guide/elements). – Simon_Weaver Jul 06 '20 at 03:59
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

Dimanoid
  • 6,999
  • 4
  • 40
  • 55
2

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.

J.Root
  • 141
  • 1
  • 1
  • 7