Note: There is an open issue related to this but it's been open for nearly 5 years.
The underlying problem is that the renderer doesn't apply the _ngcontent-app-c123
attribute to the dynamic component. Therefore you need ::ng-deep
to avoid generating css that targets (and requires) that attribute selector.
Solution 1: Put a wrapper around it.
This is the obvious solution, but you end up with an extra wrapper.
<div id="usersettings">
<ng-template [cdkPortalOutlet]="userSettingsPortal"></ng-template>
</div>
You can then style this div as you'd expect with #usersettings
.
Solution 2: Custom directive to apply _ngcontent
attribute.
If you know the portal is only going to be displayed in one place (not moved around) then the following will work. Please note this answer is my own from the aforementioned issue.
I find using ComponentPortal
the easiest and best way to generate dynamic components, and you can then attach them easily to an ng-template
element. If you're not already using it I recommend it for simplicity.
Creating a ComponentPortal is quite easy:
ngAfterViewInit() {
this.userSettingsPortal = new ComponentPortal(UserSettingsComponent);
}
Then you render it like this:
<ng-template [cdkPortalOutlet]="userSettingsPortal"></ng-template>
You can inject dependencies by the mechanism described here.
(Note for > Angular 10 you shouldn't use the deprecated answer with WeakMap).
Important Design / Injector Tree considerations
You may just be creating one dynamic component, or you may be creating a whole tree. In either case you really need to pass the injector of your host component into the ComponentPortal
constructor to get the 'normal' behavior you'd expect in a component injector tree.
Oddly the example shown above (from CDK docs) doesn't do this. I think the reason is one of the primary uses for portals is to take a component defined one place and put it on your page wherever you want. So in that case a parent injector makes less sense.
If you're generating a component dynamically and placing it in the same component you really should be using the following constructor:
const componentPortal = new ComponentPortal(component, null, parentInjector);
However if you're creating a tree of dynamic components this becomes a logistical pain! You have to clutter up your host components with all this parentInjector code.
My solution to ViewEncapsulation.Emulated issue
My application is a graphical UI to design a page from components like grids, tables, images, video etc.
The model is defined as a tree of 'rendered nodes' something like the following.
As you can see I have a ComponentPortal
in each node:
export type RenderedPage =
{
children: (RenderedPageNode | undefined)[];
}
// this corresponds to a node in the tree
export type RenderedPageNode =
{
portal: ComponentPortal;
children: RenderedPageNode[] | undefined;
}
BTW. This model is displayed by a component that iterates through children
and recursively calls itself to stamp out the tree. Basically it's an *ngFor
loop of ng-template [cdkPortalOutlet]="node.portal"
.
I started by (naively) creating all the ComponentPortal
for the tree eagerly. The problem with this way is that the correct component instance injector isn't available at the time I create the tree. When you create a ComponentPortal
your component is not actually instantiated. This means that component injected services - notably Renderer2
aren't the one you actually would want. In fact when I tried @SkipSelf() private renderer2: Renderer2
it would jump all the way to the outermost dynamic component.
So I realized I'd need to avoid creating the component portal until the actual host component was being 'run':
This is what the original attempt looked like (with eagerly created portalInstances):
<ng-template [cdkPortalOutlet]="pagenode.portalInstance"></ng-template>
Then I realized I could just make my own portal directive to do exactly what I wanted and more!
<ng-template [dynamicComponentOutlet]="pagenode"></ng-template>
Note how I pass in the node and not the portal instance.
So what this directive will do is:
- Take a prerendered
pagenode
representing the dynamic component's definition only (and its children)
- The first time it tries to attach the portal it'll actaually create the
ComponentPortal
instance with the correct parent Injector
- Because the injector context of the
dynamicComponentOutlet
outlet is the host component it can also generate and apply the _ngcontent-app-c338
attribute (which is the whole issue this issue is about!).
Here's my solution:
- First I needed to create a
LazyComponentOutlet
which contains a placeholder for the ComponentPortal
and also any data needed to create it. I've just called this params
because it will be up to you. I'm also not including the ComponentPortalParams
definition for the same reason. At minimum it would need to include the component type.
// this corresponds to a node in the tree
export type RenderedPageNode =
{
// lazily instantiated portal
lazyPortal: LazyComponentPortal;
children: RenderedPageNode[] | undefined;
}
export type LazyComponentPortal =
{
// the actual ComponentPortal which initially is undefined until the directive initializes it
componentPortal: ComponentPortal<any> | undefined;
// whatever we need to create a component
params: ComponentPortalParams // this is application specific to whatever you need
}
Then the DynamicComponentPortalHost
attribute (rename this however you please):
Note this is inspired by the way they do portal inheritance in portal-directives.ts
@Directive({
selector: '[dynamicComponentOutlet]',
exportAs: 'rrDynamicComponentHost',
inputs: ['dynamicComponentOutlet: rrDynamicComponentHost'],
providers: [{
provide: CdkPortalOutlet,
useExisting: DynamicComponentPortalHostDirective
}]
})
export class DynamicComponentPortalHostDirective extends CdkPortalOutlet {
constructor(
// parameters required by CdkPortalOutlet constructor (passed via super)
_componentFactoryResolver: ComponentFactoryResolver,
_viewContainerRef: ViewContainerRef,
@Inject(DOCUMENT) _document: any,
// renderer inherited from host component (where the ng-template is defined)
private renderer2: Renderer2,
// injector (from parent) to use as a parent injector for our ComponentPortal
private injector: Injector,
// my own service to create a ComponentPortal
// it's up to you how you create a ComponentPortal inside this
private componentPortalFactory: ComponentPortalFactoryService)
{
super(_componentFactoryResolver, _viewContainerRef, _document);
// need to subscribe immediately because ngOnInit is too late
// when the component is attached we can immediately grab its element
this._subscription.add(this.attached.subscribe((component: ComponentRef<any> | null) => {
if (component)
{
// use parent renderer to determine the correct content attribute for us
// to do this we just render a fake element and 'borrow' it's first (and only) attribute
// _ngcontent-app-c338
const contentAttr = this.renderer2.createElement('div').attributes[0].name;
renderer2.setAttribute(component.location.nativeElement, contentAttr, '');
}
}));
}
_subscription = new Subscription()
ngOnDestroy()
{
this._subscription.unsubscribe();
}
@Input('dynamicComponentOutlet')
set dynamicComponentOutlet(pageNode: RenderedPageNode)
{
// if we haven't yet instantiated a ComponentPortal instance create one
if (!pageNode.lazyPortal.componentPortal)
{
// create component portal
// how you do this is up to you, just be sure to use the constructor that includes injector
const componentPortal = this.componentPortalFactory.createComponentPortal(pageNode.lazyPortal.params, this.injector);
// we now have an actual instance of ComponentPortal, so save a reference
value.portal.componentPortal = componentPortal;
}
// set the ComponentPortal on the actual 'inherited cdkPortal'
this.portal = value.portal.componentPortal!;
}
}
Finally this method works just as well for a single item and not a tree. Or you could extract just the part that renders the ngContent
attribute if you can't shoehorn this into an existing project.
And that's it! Of course if they fix (or change encapsulation) this in future you've only got to update this in one place.