The Portal module in Angular Material (CDK) is a very powerful way to render dynamic content into a 'PortalOutlet'.
A Portal<T>
can be a TemplateRef where T
is the type of the context data.
<ng-template cdkPortal let-data>
<p>Portal contents {{ data | json }}</p>
</ng-template>
You get a reference to it with @ViewChild
@ViewChild(CdkPortal) myPortalRef: CdkPortal;
You can display a portal in a cdkPortalOutlet
Content gets inserted here:
<ng-template [cdkPortalOutlet]="myPortalRef"></ng-template>
This will insert the portal contents into the outlet.
But there are two big missing features with the provided directives.
1) no way to set the actual context object on the outlet using the directive
2) no way to set the context object on the portal itself
You can manually create an portal and provide a context, or attach a Portal to a PortalOutlet programatically with a context but I want to do it more declaratively.
The similar (and more commonly used) [NgTemplateOutlet]
does provide a property for [ngTemplateOutletContext]
.
What's the best way to set context on a CdkPortal? Should I extend the directives myself, or try to pass through my data with a service? I thought the whole point of this module was to make things super easy.
Angular source
See also: https://github.com/angular/material2/issues/6310
I have hit the same questioning, and ended up thinking we are actually taking this from the wrong angle!
Injecting a context only makes sense if we have a template to project. In that case, the variables are added to the template environment and they can be bound to component properties in the template code:
<ng-template #user_details let-user>
<qtl-manage-user-panel [user]="user"></qtl-manage-user-panel>
</ng-template>
But without a ng-template
(i.e. the Portal
case), that would feel weird. The target component would have out-of-the-box context, without a let
directive. And what variables should it be bound to?
So, getting back to our case of projecting an actual component into the Portal: how to pass context? The best answer is Injection I think.
The context will be passed through a custom Injector to the component's constructor when it's built. All it takes is creating custom injection tokens:
export const MY_TOKEN = new InjectionToken<TTT>('xxx');
Then, when creating the ComponentPortal
, we give a custom injector:
const injector =
Injector.create({
providers: [
{
provide: MY_TOKEN,
useValue: /* Here, the context variable */,
},
]});
const cPortal = new ComponentPortal(ComponentClass, null, injector);
That way, the context variable will be injected in the ComponentClass
constructor, and can be accessed through:
constructor(
@Inject(MY_TOKEN) public context: CCC,
) { ... }
And now context
can be used in the component class or template.
A little late to the party, but I hope it can help people ending up on this question in the future.
I think you can assign the context directly to the portal but I haven't tested this. It has to be assigned before the outlet uses the portal and that might be tricky.
@ViewChild(CdkPortal) myPortalRef: CdkPortal;
public ngAfterViewInit(): void {
this.myPortalRef.context = {...}; // your context data
}
I think the issue here is that the context is attached to the portal instead of being independant. So you can't use the same portal to render in two different outlets with two different contexts.
Also, have you tried using cdkPortalOutlet
with the micro-syntax parser in Angular to see if it works?
<ng-template *cdkPortalOutlet="myPortalRef; context: contextExp"></ng-template>
From what I can tell the ng-template
methods for the portals isn't used in the library. Everything with portals is done as services, and the template variant appears to have been an add on feature for the community but it wasn't fully vetted.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With