Search code examples
angularangular-renderer2

Why doesn't a component style apply to a dynamically inserted element?


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 screenshot showing "Pre-existing .target element" with a red background and "Dynamic .target element" with no background


Solution

  • 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:

    1. Use 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);
        }
      }
    }
    

    A screenshot showing "Pre-existing .target element" and "Dynamic .target element" both with red backgrounds

    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);
      }
    }
    
    1. Change the way Angular scopes the component's styles by setting 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.