Search code examples
angularangular-changedetectionangular-lifecycle-hooks

Listen to className changes on an element replaced by ng-content


I need to listen to class name changes on a element being replaced by the <ng-content>. I have tried many ways and the only way I found for this is by using setInterval which I think is not a good practice. Suppose I will inject an input element inside the <app-child> component

@Component({
  selector: 'app-parent',
  template: `
    <app-child>
     <input type="text">
    </app-child>
  `
})
export class ParentComponent implements OnInit {
  ngOnInit() { }
}

And that I want to do something inside child.component.ts whenever the class attribute of the input change:

@Component({
  selector: 'app-child',
  template: `<ng-content select="input"></ng-content>`
})
export class ChildComponent implements OnInit {
  @ContentChild(HTMLInputElement) input: any;

  ngOnInit() {
    setInterval(() => {
       const { className } = this.input.nativeElement;
       console.log(className);
    }, 500);
  }
}

This approach manages to detect the class change but the problem with setInterval is that that callback will be running on background every 500 milliseconds, is there another way to detect the change?

Note: I've already tried the hook ngAfterContentChecked which is ran automatically after any change detection but inside I don't have access to the latest change on this.input.nativeElement.className as if this function was executed before the value was changed.


Solution

  • You can use the MutationObserver Api

    Something like this:

      ngAfterContentInit(): void {
        this.changes = new MutationObserver((mutations: MutationRecord[]) => {
          mutations.forEach((mutation: MutationRecord) => {
            // this is called twice because the old class is removed and the new added
            console.log(
              `${mutation.attributeName} changed to ${this.input.nativeElement.classList}`
            );
          });
        });
    
        this.changes.observe(this.input.nativeElement, {
          attributeFilter: ['class'],
        });
      }
    

    Here is a stackblitz with it running https://stackblitz.com/edit/angular-ivy-tz5q88?file=src%2Fapp%2Fchild.component.ts