Angular CDK: How to set Inputs in a ComponentPortal

Viewed 29617

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

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 }
  ]
})

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();

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

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

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

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

I now, 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.

Related