Four ways of listening to DOM events in Angular (Part 2: @HostListener)

Shijir Tsogoo
Clarity Design System
6 min readNov 27, 2018

In the previous article of this series, we talked about what Angular Event Binding is and how it can be used in a variety of different cases. We learned that the primary purpose of Angular Event Binding is to listen to events inside a component’s template. So now how about listening to events on the host element that wraps the component’s template? How can we do that properly in Angular apps? That’s the very question we will answer in this article.

Before diving directly into listening to DOM events on a host element, I think we need to touch on what a host element is. The concept of host element applies not only to components but also attribute directives.

In a component, the host element is the outer most shell element that contains the template, and its tag name is your chosen selector string in the component’s configuration.

Let’s take a look at a simple example here:

In the example above, I’ve created a component called BaconIpsum which provides a bacon ipsum dummy text through its template. Angular renders this using the selector string bacon-ipsum whose content is the template. So here, the <bacon-ipsum> element is the component’s host element.

As for an attribute directive, you place it on an existing element in your application and that very element becomes the host element of the directive.

Here, in the example below, I created a bare-bone directive with a selector string onlyMyBacon:

Any element that I place this onlyMyBacon directive on will be the directive’s host element. If you go back to the first example, you would find that I have placed the directive on the<mark>element inside the component’s template.

As Angular developers, we will certainly encounter many instances in which we need to listen to events on the host element whether it’s of a component or attribute directive. That’s where @HostListener comes in handy.

Let’s see an example of using @HostListener to listen to a DOM event in a directive. I will use and expand my previous directive example with @HostListener:

Now any element with the onlyMyBacon directive on will display an alert “Don’t touch my bacon!” when you hover over with your mouse cursor.

As described before, this directive will turn any element that it is placed on into its host element and it set its mouseenter event listener on the host element through @HostListener . As you can see in the code, you would declare the @HostListener decorator method with the DOM event passed in as the argument, followed by its event handler method.

Check out the aforementioned code snippets in the live demo:

But why do we need @HostListener here?

You might ask at this point, why is it necessary to use @HostListener? Why do we need to bother to learn another higher-level API in Angular just to listen to DOM events? Why can’t we just stick to our traditional way of listening to events with our trusty Element.addEventListener method? Well, you can do that and avoid using @HostListener. But to achieve the same result, our example would turn into the following:

Now, in the example above, we are dealing with a lot more things compared to our first example. We had to first inject the directive’s element reference to the constructor, access its native DOM element, and add our event listeners to the host element. And if you remember, in my previous post on Angular Event Binding, I’ve stated that it’s never a good idea to access DOM elements and their methods or properties directly, let alone manipulating them as it creates problems with Angular’s complex change detection system as well as web workers. And touching the DOM elements directly is one of the most common mistakes Angular Developers make.

On top of that, you can see that we had to implement Angular’s lifecycle hook methods to manually add the listener when the directive is initialized and remove it when it gets destroyed. When you manually add a listener to a DOM element, always make sure to remove the listener later when you no longer need it. Otherwise, it could potentially lead to memory leak problems.

So by using @HostListener, you can let Angular do the proper removals and clean up for you, and your code will also remain clean, concise, and declarative.

Passing and accessing arguments in the event handler method

With @HostListener, you can access the event payload object through$event keyword:

@HostListener('mouseenter', ['$event'])
onMouseEnter(event: any) {
// Logs the id of the element
// where the event is originally invoked.
console.log(event.target.id);
}

All arguments you would like to access in the event handler method should be included in that array first. Note that, in the code snippet above, it’s still passing an object despite being wrapped inside quotes. But If you would like to pass an actual string directly as an argument, that should be inside nested quotes as shown below:

@HostListener('mouseenter', ['"hello!"', '$event']) onMouseEnter(greeting: string, event: any) {  // Logs the string argument, "hello!"
console.log(greeting);
// Logs the id of the element
// where the event is originally invoked.
console.log(event.target.id);
}

You can also pass a specific property from an object. For instance, in the snippet below, it’s directly passing the target element’s id that the event is originated from by specifying the nested properties from the $event object:

@HostListener('mouseenter', ['$event.target.id']) 
onMouseEnter(id: string) {
// Logs the id of the element
// where the event is originally invoked.
console.log(id);
}

Listening to events originating inside and outside the directive

One of the most important concepts in handling events is event delegation or event bubbling. If you are not familiar with these terms or concepts, I highly recommend reading How JavaScript Event Delegation Works by David Walsh. I have also gone over how these concepts apply to Angular event binding in the previous post. As with @HostListener, you can also catch and respond to events that originated in any nested or child element as they bubble up to the directive. In the example below, I placed the clickCatcher directive on the very parent element, ul . However, you can see that it registers click events from not only its own host element but also from its children items:

Also, unless the propagations are stopped on the way up, any DOM event eventually bubbles up to the global root elements such as document or window. So you can use @HostListener to set your listeners on the global element to catch any event that is fired on any node element in your application:

@HostListener('window:click', ['$event.target'])
onClick(targetElement: string) {
console.log(`You clicked on`, targetElement);
}

However, it’s only recommended to set event listeners on window or document if you are sure that the directive will be used only once or very few times across the application. Having many directives that listen to the same event on the global elements could eventually hurt the performance of your application.

Lastly, just like in Angular event binding, you can also use Angular Pseudo-Events with @HostListener. I have written a blog post on Angular Pseudo-Events and touched on how to use them with @HostListener so check out that blog post too. Stay tuned for the next blog posts on event listeners as I explore the other methods of listening to DOM events in Angular. Okay, now done with @HostListener!

Let’s recap — Key Takeaways:

  • @HostListener is Angular’s decorator method that’s used for listening to DOM events on the host element of both component and attribute directives.
  • @HostListener sets the listeners once the directive is initialized and removes them automatically once the directive gets destroyed.
  • @HostListener catches and listens to events from children or nested elements when they bubble up to the host element. Moreover, you can listen to events on global elements, but you should do so sparingly for performance reasons.

--

--