Reapplying an Angular Directive on DOM Changes

Jared Youtsey
ngconf
3 min readApr 8, 2022

--

We needed to project HTML content loaded from the database into a component. Each time the content changes we need to add some behavior to that external content, i.e. add a class to every anchor tag. We want this behavior to be added by an Angular Directive. But a Directive will not be re-executed unless the element it is attached to is recreated or triggered in some way. We don’t want the parent component to know anything about the behavior of the Directive (that’s why you use a directive in the first place!). So, we want everything needed to make the Directive work baked directly into the Directive.

So, let’s start with the basics of the Directive, and then we’ll extend it to watch the DOM.

AfterViewInit ensures the element has content already projected through property binding in the Component template. We then find all of the anchor tags and loop through them using the Renderer2 to add our cool class to each one.

Now we just need to add the directive to the component’s HTML:

<div
[myStyleAnchors]
[innerHtml]="{{ someSafeHtml }}">
</div>

Voila!

But, what if someSafeHtml changes dynamically at runtime? The new HTML will get rendered, but the behavior of the Directive will not be reapplied.

Now, you could create an @Input() myStyleAnchors: string; on your Directive and bind the someSafeHtml into that. You could then use ngOnChanges to decide when to re-process the anchors. The only downside to this is that the HTML could be quite large. In our case, it can be a tremendous amount of content.

There is another way of monitoring for DOM changes, the MutationObserver. It has three methods, two of which we will cover. Note that this is not an RxJS Observable. This is native!

First, we create a new observer in our Directive that re-processes the anchors anytime the observer detects changes in the DOM:

private observer = new MutationObserver(() => this.styleAnchors());

The observe method is called with the nativeElement to be watched. Additional flags describe the specific DOM content to observe.

private registerListenerForDomChanges() {
const attributes = false;
const childList = true;
const subtree = true;
this.observer.observe(this.el.nativeElement, {
attributes,
childList,
subtree
});
}

We want to clean up when this gets destroyed, so let’s add this to ngOnDestroy:

ngOnDestroy() {
this.observer.disconnect();
}

Now, let’s hook it up!

ngAfterViewInit(){
this.styleAnchors();
this.registerListenerForDomChanges();
}

Now, putting classes on the anchor tags could cause us to infinitely loop if we passed different flags to the observe method, so let’s protect against that:

private processing = false;private styleAnchors() {
if (!this.processing) {
this.processing = true;
const anchors = this.el.nativeElement.querySelectorAll('a');
anchors.forEach(a => this.renderer.addClass(a, 'my-cool-class'));
this.processing = false;
}
}

Now, when the child DOM of this element changes contents the Directive will re-process the anchor tags!

In the Gist below I included a bonus on how to mock out everything you need for unit testing this kind of Directive as well!

Happy coding!

--

--

Jared Youtsey
ngconf
Editor for

Passionate about JavaScript and Angular. Pure front-end development for the past eight years or so…