I am programmatically appending a new element to an existing element in my component, but the styles associated with the .target
class defined in the component's styles
do not appear to apply to the new element. Why are the styles not applied?
import { AfterViewInit, Component, ElementRef, inject, viewChild } from '@angular/core';
@Component({
selector: 'app-foo',
standalone: true,
template: `
<div #container>
<div class="target">Pre-existing .target element</div>
</div>
`,
styles: `
.target {
background-color: rgba(255, 0, 0, 0.5);
}
`,
})
export class FooComponent implements AfterViewInit {
private readonly container = viewChild.required<ElementRef<HTMLDivElement>>('container');
ngAfterViewInit() {
if (this.container().nativeElement) {
const target = document.createElement('div');
target.innerText = 'Dynamic .target element';
target.classList.add('target');
this.container().nativeElement.appendChild(target);
}
}
}
A component's styles
are scoped to the component by default. Angular achieves this by adding an attribute to elements in the component's scope and by changing the selectors in the styles
to require this attribute.
You can see this in the result of rendering your component (where _ngcontent-ng-c2626609219
is the attribute that's been inserted):
<div _ngcontent-ng-c2626609219="">
<div _ngcontent-ng-c2626609219="" class="target">Pre-existing .target element</div>
<div class="target">Dynamic .target element</div>
</div>
This means that your .target
selector was converted to something like .target[_ngcontent-ng-c2626609219]
which is why your new element does not have the expected styling - the selector doesn't match the new element because it does not have this attribute.
There are two ways to fix this behavior:
Renderer2.createElement()
instead of document.createElement()
.import { AfterViewInit, Component, ElementRef, inject, Renderer2, viewChild } from '@angular/core';
@Component({
selector: 'app-foo',
standalone: true,
template: `
<div #container>
<div class="target">Pre-existing .target element</div>
</div>
`,
styles: `
.target {
background-color: rgba(255, 0, 0, 0.5);
}
`,
})
export class FooComponent implements AfterViewInit {
private readonly renderer2 = inject(Renderer2);
private readonly container = viewChild.required<ElementRef<HTMLDivElement>>('container');
ngAfterViewInit() {
if (this.container().nativeElement) {
const target = this.renderer2.createElement('div');
target.innerText = 'Dynamic .target element';
target.classList.add('target');
this.container().nativeElement.appendChild(target);
}
}
}
The dynamic element now also has the attribute and so now also matches the modified selector.
<div _ngcontent-ng-c2626609219="">
<div _ngcontent-ng-c2626609219="" class="target">Pre-existing .target element</div>
<div _ngcontent-ng-c2626609219="" class="target">Dynamic .target element</div>
</div>
You can use the other methods on Renderer2
as well, but they're not necessary in this case:
ngAfterViewInit() {
if (this.container().nativeElement) {
const target = this.renderer2.createElement('div');
const text = this.renderer2.createText('Dynamic .target element');
this.renderer2.appendChild(target, text);
this.renderer2.addClass(target, 'target');
this.renderer2.appendChild(this.container().nativeElement, target);
}
}
encapsulation
to ViewEncapsulation.None
:import { Component, ViewEncapsulation } from '@angular/core';
@Component({
selector: 'app-foo',
standalone: true,
template: `
<div #container>
<div class="target">Pre-existing .target element</div>
</div>
`,
styles: `
.target {
background-color: rgba(255, 0, 0, 0.5);
}
`,
encapsulation: ViewEncapsulation.None,
})
export class FooComponent {
// ...
}
The result of rendering your component now lacks the custom attribute and the .target
selector has not been changed to use the attribute:
<div>
<div class="target">Pre-existing .target element</div>
<div class="target">Dynamic .target element</div>
</div>
You have to be careful with this approach because the .target
style may now match other elements you're not intending for it to match.