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