47

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

8 Answers 8

48

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 }
  ]
})
Sign up to request clarification or add additional context in comments.

1 Comment

Thanks, exactly what I was looking for with the deprecation of PortalInjector
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.

10 Comments

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
Probably inside 'ComponentToPort', you will need to import CONTAINER_DATA from the location you created it. That might resolve the issue.
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?
what about @output?
PortalInjector is deprecated now. It seems that Injector.create is recommended instead.
|
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();

8 Comments

a much easier solution than the other answers, at least for me !
indeed so much easier than creating a custom injector !
best answer, the injector way makes you write components in a non "standard" way and has limitations
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 :-)
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.
|
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;
    }

4 Comments

there's also an @output() that emits the ref, see: github.com/angular/components/blob/master/src/cdk/portal/…
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.
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.
I tried many many things, only this worked thanks!
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);
  }
}

1 Comment

you need to explain the code as well why this will work
7

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.

Comments

6

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;
}

2 Comments

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).
If anyone is trying to put something outside of the Angular context then also check out Angular Elements (angular.io/guide/elements).
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

2 Comments

this doesn't trigger ngOnChanges unfortunately
You can do it manually cmpRef.changeDetectorRef.detectChanges();

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.