Applying host styles to child router outlets

114 Views Asked by At

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.

3

There are 3 best solutions below

12
Naren Murali On BEST ANSWER

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 definetly 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 accomodate 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 its 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

1
Eliseo On

If you can change the background-color of router-outlet name="slotA" and router-outlet name="slotB" you can use the next-sibling combinator (the +) in global style.css like:

router-outlet[name="slotA"] + *{
  background-color:#88f;
}
router-outlet[name="slotB"] + *{
  background-color:#f88;
}
0
jgosar On

I'm posting a summary of what I have learned and about the solutions here for posterity. Thanks to @naren-murali for helping solve the issue.

Here is a minimum example that replicates the problem:

https://stackblitz.com/edit/stackblitz-starters-rhnecp

The proper fix was to add selectors to both components:

https://stackblitz.com/edit/stackblitz-starters-gs1pji

I reported this issue to the Angular team as well:

https://github.com/angular/angular/issues/54304

They pointed out that there was a warning in the console related to this:

NG0912: Component ID generation collision detected.
Components 'RouterOutlet1Component' and 'RouterOutlet2Component'
with selector 'ng-component' generated the same component ID.
To fix this, you can change the selector of one of those components
or add an extra host attribute to force a different ID.
Find more at https://angular.io/errors/NG0912

So the solution was in the warning message all along :)