question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

ViewEncapsulation.Emulated is not working for dynamically created components

See original GitHub issue

I’m submitting a … (check one with “x”)

[x] bug report => search github for a similar issue or PR before submitting
[ ] feature request
[ ] support request => Please do not submit support request here, instead see https://github.com/angular/angular/blob/master/CONTRIBUTING.md#question

Current behavior

Angular 2 doesn’t set _ngcontent-* attribute on the components created using ViewContainerRef.createComponent() when ViewEncapsulation.Emulated is used.

Expected behavior

Angular 2 sets _ngcontent-* attribute on the components created using ViewContainerRef.createComponent() when ViewEncapsulation.Emulated is used.

Minimal reproduction of the problem with instructions

~See reproduction example: http://plnkr.co/edit/0HF3A2muRlAVOEonJvs7~ Updated reproduction for Angular 6: https://stackblitz.com/edit/angular-uzjtlp

If you change view encapsulation to native (in browser supporting it) my-dynamic-comp will become red (which is expected behaviour). But with emulated view encapsulation it stays green. It’s because _ngcontent-* attribute is not set on the dynamically created component’s host element.

What is the motivation / use case for changing the behavior?

Emulated behaviour works different from native, so I assume it is a bug.

Please tell us about your environment:

  • Angular version: 2.0.2
  • Browser: all
  • Language: TypeScript (but it may be relevant for others as well)

Issue Analytics

  • State:open
  • Created 7 years ago
  • Reactions:16
  • Comments:9 (5 by maintainers)

github_iconTop GitHub Comments

1reaction
devoto13commented, Jun 27, 2018

@mlc-mlapis I have added the StackBlitz URL to the original issue using Angular 6. It does not work exactly the same as before.

@Ettapp This is not enough. The element with _nghost-c4 also should have _ngcontent-* from the parent component, which it doesn’t.

PS You can use * selector instead of exact component name if you want styles to apply to any element. E.g. :host .container ::ng-deep > * {}, it will apply styles to any dynamic component located inside element with container class.

0reactions
simeylacommented, Jul 1, 2021

Custom directive + ComponentPortal solution:

I find using ComponentPortal the best way to generate components, and you 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:

  1. 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.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Dynamic components with ViewEncapsulation.Emulated + ...
I started by (naively) creating all the ComponentPortal for the tree eagerly. The problem with this way is that the correct component instance ......
Read more >
ViewEncapsulation.Emulated is not working for dynamically created ...
It's because _ngcontent-* attribute is not set on the dynamically created component's host element. What is the motivation / use case for changing...
Read more >
View encapsulation - Angular
ViewEncapsulation.Emulated, Angular modifies the component's CSS selectors so that they are only applied to the component's view and do not affect other ...
Read more >
Understanding Angular's ViewEncapsulation
When creating a component, Angular can render this component in a ... can work together with the Shadow DOM and also how it...
Read more >
Shadow DOM | None - Angular 6 | 7 - CodeWithSrini - YouTube
Understanding the concepts of View Encapsulation in Angular 6|7.The concepts covers three types of encapsulation such as Emulated, ...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found