Angular CDK: How to set Inputs in a ComponentPortal
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
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);
}
}
JoG
Updated on July 09, 2022Comments
-
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 likecomponentRef.component.instance.myInput
-
Joe over 6 yearsWhen 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 over 6 yearsProbably inside 'ComponentToPort', you will need to import CONTAINER_DATA from the location you created it. That might resolve the issue.
-
wosevision almost 6 yearsIs 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
, noasync | 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? -
Sunil Garg about 5 yearsyou need to explain the code as well why this will work
-
Sunil Garg about 5 yearswhat about @output?
-
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 about 5 yearsYou need to declare the injected componentData in the constructor of the ComponentToPort
constructor(@Inject(CONTAINER_DATA) public componentData: any ) {}
-
Bob almost 5 yearsa much easier solution than the other answers, at least for me !
-
Christophe Le Besnerais over 4 yearsindeed so much easier than creating a custom injector !
-
Rusty Rob over 4 yearsthere's also an @output() that emits the ref, see: github.com/angular/components/blob/master/src/cdk/portal/…
-
Mauro Insacco over 4 yearsbest answer, the injector way makes you write components in a non "standard" way and has limitations
-
muuvmuuv about 4 yearsWhat about
ngContent
? If myComponentToPort
has a<ng-content>
how do I pass data to it? -
Gaurav Panwar about 4 yearsThis 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 almost 4 yearsSo is this sort of bypassing the proper
@Input()
mechanism, and/or does it matter since you explicitly calldetectChanges
- or is that the whole point :-) -
aruno almost 4 years@muuvmuuv maybe you'd have to pass another portal in instead of ng-content?
-
aruno almost 4 yearsNote: 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 wantng-template
approach (as you showed above). -
aruno almost 4 yearsIf anyone is trying to put something outside of the Angular context then also check out Angular Elements (angular.io/guide/elements).
-
Hisham over 3 yearsPortalInjector is deprecated now. It seems that Injector.create is recommended instead.
-
Andrei Cojea about 3 yearsThis 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 almost 3 yearsThis is the best solution so far, but be aware that
ngOnChanges
does not get called -
David almost 3 yearsAddition to my comment above: here is the github issue for adding an API to set inputs github.com/angular/angular/issues/22567
-
Stephane about 2 yearsAnd we can use Material elements in the
ComponentToPort
? Say a<mat-icon>
? -
Rodrigo Assis Neves about 2 yearsThanks, exactly what I was looking for with the deprecation of PortalInjector
-
WardenUnleashed about 2 yearsThis 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 about 2 yearsThis 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.