Listening to DOM Changes Using MutationObserver in Angular

Published on September 8, 20173 min read

In this article, we’re going to learn how to build an Angular Directive that listens to DOM changes using MutationObserver Web API.

To explain the concept – we’ll build a simple playground with a list of Todo. We’ll implement buttons for adding a new Todo and removing an existing one.

The following image demonstrates our final result:

DOM Changes will never run away!

What is a MutationObserver?

MutationObserver is a Web API that detects a node modifications due to DOM manipulations.
Unlike the deprecated MutationEvent, the MutationObserver listens for changes efficiently. Besides, setTimeout hacks aren’t needed anymore!

To better understand MutationObserver, let’s take a look at an example:

const node = document.querySelector('.some-element');

const observer = new MutationObserver(mutations => {
  mutations.forEach(mutation => console.log(mutation));
});

observer.observe(node, {
  attributes: true,
  childList: true,
  characterData: true
});

This example detects a node through querySelector and creates a MutationObserver instance. The observer takes a function which is applied for each change (for the sake of clarity we’ll log the changes into the console). When we want to bind an observer with a specific node, we shall use the observe method. Nonetheless, an options parameter is passed and indicates which types of DOM changes we want to listen for. In our example, we’re notified of every DOM change.

After registering an observer – we’ve to take care of the subscription. A method that cancels a subscription is called disconnect:

observer.disconnect();

You can check its browser compatibility through Can I Use.

Now, we’re ready to get into our Angular business.

Preparing a Playground

Let’s start with building a simple Angular application, which will consist a Todo list.

First of all, we’ll create a Service that simulates an asynchronous action which takes 1000ms and produces a Todo:

@Injectable()
export class AppService {
  fetchData(): Rx.Observable<string> {
    return Rx.Observable
      .of('Todo')
      .delay(1000);
  }
}

Moreover, we need to create a Component that consists the Todo list, and methods for manipulating the list. Therefore, we’ll attach to our Component couple of methods – addTodo and removeTodo:

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html'
})
export class AppComponent {
  public todos: string[];

  constructor(private appService: AppService) {
    this.todos = ['Todo', 'Todo', 'Todo'];
  }

  addTodo(): void {
    this.appService
      .fetchData()
      .subscribe((todo: string) => this.todos.push(todo));
  }

  removeTodo(index: number): void {
    this.todos.splice(index, 1);
  }
}

As we know, components require an appropriate template. Let’s do it:

<ul>
  <li *ngFor="let todo of todos; let i = index">
    {{ todo }} <button (click)="removeTodo(i)">Remove</button>
  </li>
</ul>
<button (click)="addTodo()">Add Todo</button>

So far we haven’t noticed anything unusual – pretty straightforward.

Time has come for us to build a DOM changes listener.

Building a DOM Changes Listener

What we’re going to do now is to make the Directive:

@Directive({
  selector: '[domChange]'
})
export class DomChangeDirective {
  private changes: MutationObserver;

  @Output()
  public domChange = new EventEmitter();

  constructor(private elementRef: ElementRef) {
    const element = this.elementRef.nativeElement;

    this.changes = new MutationObserver((mutations: MutationRecord[]) => {
          mutations.forEach((mutation: MutationRecord) => this.domChange.emit(mutation));
        }
    );

    this.changes.observe(element, {
      attributes: true,
      childList: true,
      characterData: true
    });
  }
}

We define two fields: a MutationObserver field for tracking the DOM changes and an EventEmitter field for raising a custom events. If you’re not familiar with EventEmitter – you can read about it inside the template syntax docs.

As it might be seen, we use ElementRef to access the node element directly. Every DOM change in our node emits a custom event – which passes the change itself as an argument.

Our next step would be to unsubscribe the changes observer when the Directive is destroyed:

ngOnDestroy(): void {
  this.changes.disconnect();
}

Obviously, we have to assign the OnDestroy interface with the Directive:

export class DomChangeDirective implements OnDestroy {

We’re done with the Directive.

The final step would be to use it in order to track the changes of the list element:

<ul (domChange)="onDomChange($event)"></ul>

Note: onDomChange is a method which is invoked for each DOM change.

Conclusion

In this article we discovered the MutationObserver Web API. Furthermore, we did a DOM changes simulation and built a Directive that listens for them.

Here’s attached the final application:

You’re invited to explore it through your browser’s console.

Follow Me

Join My Newsletter

Get updates and insights directly to your inbox.

Site Navigation


© 2024, Nitay Neeman. All rights reserved.

Licensed under CC BY 4.0. Sharing and adapting this work is permitted only with proper attribution.