Directives and AfterViewInit

Posted on May 20, 2019

Written by Preston Lamb

angular

I recently needed to create an attribute directive for my Angular application. I wanted to be able to style results for a typeahead result list. The point was to highlight the segments of the result that matched the search term. If the search term was ‘test’, for example, a result could look like this:

This is a test

I had all the utility functions written to match the text, add in the strong tags where needed, and I had unit tests written as well. But when I went to actually implement the directive, no text for the results was showing up; I just got an empty string. I wasn’t sure what was going on, but after a little work I was able to figure it out. I’ll lay out what I went through and hopefully it can help someone else out.

Angular directives come in three flavors: attribute directives, structural directives, and components (which are directives with templates). Attribute directives change the appearance of an HTML element, another directive, or a component. Structural directives change the DOM layout by adding and removing DOM elements. Finally, components have all the functionality of attribute directives, and potentially structural directives, but also contain a template.

Because I had written unit tests for the directive and the utility functions, I knew the directive was working. I was able to test that the matching term was wrapped in strong tags after going through the utility function and that it was output in the debugElement of the test with those strong tags. So why wasn’t the value showing up?

As I was trying multiple implementations of my directive, I noticed that any time a variable was interpolated, the result’s text was an empty string; but when it was used on an element that only had HTML it worked. Here are examples of when it did and didn’t work:

<!-- Didn't work -->
<p typeaheadResult>{{ myVariable }}</p>
<!-- Did work -->
<p typeaheadResult>My Result</p>

Obviously it had to do with the interpolation. To test out my theory, I set a timeout in the directive to wait for 3 seconds before running the matching function. When I did that, the attribute began to work. So, I figured out that the attribute code ran before the interpolation was done. So the last step was to make the attribute code only run when the element had text inside it and to fun the function when the interpolated text was ready.

The attribute worked by using the ngOnChanges lifecycle method. Here’s what the function looked like before fixing my interpolation error:

ngOnChanges(changes: SimpleChanges) {
	if (changes.matchTerm && this.highlightMatches) {
		this.markStringMatches(this._element);
	}
}

When the matchTerm changed, the markStringMatches function was called. But because interpolation wasn’t ready, it was called with an empty string in the element. So the interpolation was replaced with an empty string. So I changed that function to the following:

ngOnChanges(changes: SimpleChanges) {
	if (changes.matchTerm && this.highlightMatches && this._element.nativeElement.textContent) {
		this.markStringMatches(this._element);
	}
}

It was a minor change, but I made sure to wait to run the markStringMatches function until the element had text in it. Also, in the case that the interpolation took a long time (for one reason or another) I added another lifecycle method: AfterViewInit. The content of that function is the same as that of the ngOnChanges function. That way, once the interpolation has occurred (and the view is ready), the markStringMatches function is run and the directive worked.

Conclusion

Sometimes some of our Angular code needs the view to be ready to work properly, but will run before the view is ready. By using the provided lifecycle hooks, we can make our Angular code wait until the view is ready before running and thus we will get the desired result.

Hopefully this has helped you and provided you with an answer to your question as well!

Click here to subscribe to the newsletter and be the notified when a new blog post is available!