Search code examples
angularrouter-outletangular2viewencapsulation

Applying host styles to child router outlets


I am working on an app which has a main view, and inside it renders several named child router outlets. The code is as follows:

app.component.html:

<router-outlet />

app.routes.ts:

export const routes: Routes = [
  { path: '', component: SlotsView, children: [
      {
        path: '',
        outlet: 'slotA',
        component: RouterOutlet1,
      },{
        path: '',
        outlet: 'slotB',
        component: RouterOutlet2,
      }
    ]
  },
];

slots.view.html:

This is a view with some child routes
<br>
<router-outlet name="slotA"></router-outlet>
<br>
<router-outlet name="slotB"></router-outlet>

RouterOutlet1:

  • html:
I am Router outlet 1
  • less:
:host{
  background-color: #f88;
}
  • ts:
@Component({
  templateUrl: './router-outlet-1.router-outlet.html',
  styleUrls: ['./router-outlet-1.router-outlet.less'],
  encapsulation: ViewEncapsulation.Emulated,
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true
})
// eslint-disable-next-line  @angular-eslint/component-class-suffix
export class RouterOutlet1{
}

RouterOutlet2:

  • html:
I am Router outlet 2
  • less:
:host{
  background-color: #88f;
}
  • ts:
@Component({
  templateUrl: './router-outlet-2.router-outlet.html',
  styleUrls: ['./router-outlet-2.router-outlet.less'],
  encapsulation: ViewEncapsulation.Emulated,
  changeDetection: ChangeDetectionStrategy.OnPush,
  standalone: true
})
// eslint-disable-next-line  @angular-eslint/component-class-suffix
export class RouterOutlet1{
}

Note that the difference between both router outlets is that router outlet 1 should have a reddish color (#f88), and router outlet 2 should have a bluish color (#88f).

This is what actually happens:

enter image description here

In DevTools I see that both host elements have the same attribute (_nghost-ng-c4243086843) enter image description here

And that's why the same style is applied to both of them.

If I change the encapsulation on both router outlets to ShadowDom, the result is rendered correctly:

enter image description here

The question:

Is this a bug in Angular which should be reported to the developers, or is there a good reason for Router outlet 1's styles to be applied to Router outlet 2?

I already did a bit of googling and haven't found such a bug report.

I tried changing the encapsulation mode.


Solution

  • UPDATE

    After checking the replicated Stackblitz from the user, the below change fixes the issue.

    • Since the same ng-component is present for both router-outlets we are getting this issue, This definitely looks like a bug in Angular, because each element should have their own encapsulation if they are from different outlets.

    • To solve this issue, we need to use the host styling directly on the element so that even if the attributes match, the direct application of the styles ensures the colors are applied to the correct elements

    Snippet:

    ...
    host: {
      ['style.backgroundColor']: '#f88',
    },
    ...
    

    outlet 1

    import {
      ChangeDetectionStrategy,
      Component,
      OnInit,
      ViewEncapsulation,
    } from '@angular/core';
    
    @Component({
      templateUrl: './router-outlet1.component.html',
      styleUrls: ['./router-outlet1.component.less'],
      encapsulation: ViewEncapsulation.Emulated,
      changeDetection: ChangeDetectionStrategy.OnPush,
      host: {
        ['style.backgroundColor']: '#f88',
      },
      standalone: true,
    })
    export class RouterOutlet1Component implements OnInit {
      constructor() {}
    
      ngOnInit() {}
    }
    

    outlet 2

    import {
      ChangeDetectionStrategy,
      Component,
      OnInit,
      ViewEncapsulation,
    } from '@angular/core';
    
    @Component({
      templateUrl: './router-outlet2.component.html',
      styleUrls: ['./router-outlet2.component.less'],
      encapsulation: ViewEncapsulation.Emulated,
      changeDetection: ChangeDetectionStrategy.OnPush,
      host: {
        ['style.backgroundColor']: '#88f',
      },
      standalone: true,
    })
    export class RouterOutlet2Component implements OnInit {
      constructor() {}
    
      ngOnInit() {}
    }
    

    Stackblitz Demo


    Note: This output is coming from ng-component which is not even a proper component, you can just redirect the url to accommodate both the named router outlets and the components will get rendered instead of ng-component.


    It looks like a bug but I don't think it's replicable in the latest version of Angular (17).

    Here is a Stackblitz that shows the issue not happening:

    Stackblitz Demo

    enter image description here


    To solve this issue, you can add a host class on the component decorator, this will ensure the class gets applied on the correct host.

    outlet 2

    import {
      ChangeDetectionStrategy,
      Component,
      OnInit,
      ViewEncapsulation,
    } from '@angular/core';
    
    @Component({
      selector: 'app-router-outlet2',
      encapsulation: ViewEncapsulation.Emulated,
      changeDetection: ChangeDetectionStrategy.OnPush,
      template: `
        I am Router outlet 2
      `,
      styles: `
        :host.outlet2 {
          background-color: #88f;
        }
      `,
      host: {
        '[class.outlet2]': 'true',
      },
      standalone: true,
    })
    export class RouterOutlet2Component implements OnInit {
      constructor() {}
    
      ngOnInit() {}
    }
    

    outlet 1

    import {
      ChangeDetectionStrategy,
      Component,
      OnInit,
      ViewEncapsulation,
    } from '@angular/core';
    
    @Component({
      selector: 'app-router-outlet1',
      template: `
        I am Router outlet 1
      `,
      encapsulation: ViewEncapsulation.Emulated,
      changeDetection: ChangeDetectionStrategy.OnPush,
      styles: `
        :host.outlet1 {
          background-color: #f88;
        }
      `,
      host: {
        '[class.outlet1]': 'true',
      },
      standalone: true,
    })
    export class RouterOutlet1Component implements OnInit {
      constructor() {}
    
      ngOnInit() {}
    }
    

    Stackblitz Demo