In the previous two posts, we did a deep-dive into how we could listen to DOM events in an Angular app through the following two methods:

  1. Event Binding: One-way data binding, in which information is sent from a component’s template to the component’s class
  2. @HostListener: Angular decorator that handles events on the host element of a component or directive

As we covered these methods, we learned how much they simplify the process of listening to DOM events while also providing many other nifty little features. However, a common aspect of these two methods is that they are both completely tied to their template or host element, which creates certain limitations. In this post, we will go over what those limitations are and how we can use Angular’s Renderer2.listen method to resolve them as well as many other cases where you can put it into use.

What is Renderer2 in Angular?

As Angular is a multi-platform framework, it offers secure and easy-to-use tools that manage and abstract away differences across browsers and platforms. One of those tools is Renderer2, which is an Angular’s built-in service that provides APIs for interacting with and manipulating DOM elements.

As different environments such as web browsers, web-workers, and server-side rendering have different implementations of DOM, these Renderer2 APIs are built to manage those differences and yield consistent results. Note that Renderer2 not only facilitates manipulating HTML elements, but also the types of XML, SVG, etc. To learn more about the intricacies of Renderer2, I recommend taking a closer look at its source code.

Angular Renderer2.listen vs Element.addEventListener

Let’s recall how we add and remove event listeners using native DOM APIs. If we want to listen to a mousemove event on some native DOM element and invoke its callback function, onMouseMove, we would call the .addEventListener in the following way:

nativeElement.addEventListener("mousemove", onMouseMove);

To remove this event listener, we need to call .removeEventListener on the same button element with the exact same matching parameters:

nativeElement.removeEventListener("mousemove", onMouseMove);

But it’s very easy to blunder when you try to match event listeners for removal. Properly removing an event listener that is added manually is crucial as it prevents memory leaks as well as performance drag on your application.

With Renderer2.listen, it becomes much easier to clean up event listeners. Take a look at its API pattern:

Renderer2.listen(target: any, eventName: string, callback: (event: any) =>  boolean | void ): () => void;

You can see that Renderer2.listen method itself returns another method with the type of () => void. That returned method is used for removing the event listener. This is much simpler than trying to match event listeners for removal. Just save the returned method’s reference to a variable or property, and call that whenever you need to remove the listener. Let’s see how this would play out in a simple example:

Live example: https://stackblitz.com/edit/mousemove-document

In the example above, I am trying to listen to the mousemove event on document. So I’ve imported Renderer2 into my component first and then added the mousemove event listener todocument through Renderer2.listen in theOnInit cycle.

Note that when I add the event listener, I’m saving the returned method to the unlistener property which will be called later to remove the event listener in the OnDestroy cycle, which gets called when the component gets destroyed.

Another important thing to note here is that I’ve passed document not as an object, but as a string. If we pass document as an object, we will see the following error when we try to run our app in a server-side environment:

ERROR ReferenceError: document is not defined

This is because there are no global objects such as window or document in environments other than web browsers. We could resolve this issue by checking what environment this code is running in. But with Renderer2.listen, we can simply pass either string "document" or "window" and let Angular do the checking for us and adapt to the environment that it’s running in.

Angular Renderer2.listen vs Event Binding

Let’s see an example where you want to create Drag and Drop functionality using generic mousedown, mousemoveand mouseup events. This example below accomplishes just that through template event binding:

The main idea here is that dragging should start with a mousedown event on the draggable element. Only after that first mousedown should we start responding to the mousemove event(s) on the document, as the element could be dragged anywhere on the document. To make sure mousedown is registered first, we check if the draggableEl property is defined in the mousemove event’s handler. On themouseup event, we reset the whole process by simply assigning null to the draggableEl property.

As you can see in the demo above, the code looks extremely simple… but there are some serious issues that we need to fix. For one, we’ve added three active event listeners on the draggable element that will always be listening until the target element gets removed from the DOM. To make matters worse, we are listening to mousemove and mouseup events globally because they are defined on document. So we are continuously listening and trying to respond to all mousemove and mouseup events on the entire app — and it doesn’t end there! Every time these events fire, they also trigger Angular’s change detection throughout all components and directives, which could cause significant performance deterioration.

The first step to solve the above-mentioned problems is to add the mousemove and the mouseup event listeners inside themousedown event listener’s handler. However, we cannot accomplish that with event bindings as they are inlined to their template elements permanently. In the case of using @HostListener, we would still face the same problem.

Hence, we need to use Renderer2.listen which allows us to dynamically add and remove listeners when only certain conditions are met. In our case, we need to add the mousemove and the mouseup event listeners to document only when the mousedown event is registered on the draggable element. In the example below, I’ve demonstrated how to do that using Renderer2.listen:

The code is a bit more involved compared to what we saw with the event binding example. Here, the important thing to pay attention to is how we are adding and removing event listeners. Now we have only one active listener on the draggable element instead of three as the event listeners on mousemove and mouseup are nested inside the mousedown event listener’s handler. And you can also see that the event listeners on mousemove and mouseup are removed when the mouseup event fires.

But we have to address another issue I’ve briefly mentioned earlier: every time one of these mousedown, mousemove, and mouseup events fire, Angular change detection would also run. Remember that Angular change detection runs through all components and directives in the following three cases:

  • When event listeners respond to their target DOM events
  • When setTimeout() or setInterval() executes
  • When ajax requests respond

This is especially concerning as we are dealing with an event such as mousemove that fires on each pixel movement you make with your mouse. So Angular change detection might be unnecessarily triggered thousands of times. Fortunately, Angular lets us run our code outside of its change detection zone.

What we need to do is to make use of NgZone, which is a service that lets you run your code inside or outside of the Angular zone. To run our code outside of the Angular zone, we need to use the following API pattern:

NgZone.runOutsideAngular<T>(fn: (...args: any[]) => T): T

We also need to be careful while using the API above because any data binding changes made outside of the Angular zone will not take effect unless you are directly manipulating the DOM. In the case of our Drag and Drop example, we could add mousemove event listener outside of the Angular zone:

Live example: https://stackblitz.com/edit/basic-dnd-with-renderer2-listen-ngzone

As you can see, I’ve placed themousemove event listener entirely inside this.ngZone.runOutsideAngular‘s callback method to prevent change detection from running. But for the mousedown event, change detection would still run when the event fires as this.ngZone.runOutsideAngular is used inside its handler. That’s an important difference to keep in mind. This is the reason why we cannot suppress change detection if we add event listeners through Event Binding or @HostListener methods. Hence, if you need to listen to events that fire frequently many times such mousemove, touchmove, or scroll, consider using Renderer2.listen with this.ngZone.runOutsideAngular.

Bonus tip:

Another cool feature Renderer2.listen offers is that you can listen to DOM events and also listen to Angular Pseudo Events. That means you could do this:

Renderer2.listen(nativeElement, "keyup.enter", event => { ... })Renderer2.listen(nativeElement, "keydown.esc", event => { ... })Renderer2.listen(nativeElement, "keyup.arrowup", event => { ... })Renderer2.listen(nativeElement, "keyup.arrowdown", event => { ... })

If you want to learn more about Angular Pseudo Events, please refer to this article.

Let’s recap — Key Takeaways:

  • Renderer2 is Angular’s built-in service that provides APIs for interacting with and manipulating DOM elements. Manipulating DOM elements through Renderer2 yields consistent results across different web browsers as well as environments.
  • With Renderer2.listen, it becomes much easier to clean up event listeners. You no longer need to match event listeners for removal.
  • Unlike Event Binding or @HostListener methods, with Renderer2.listen, you can manage complex event listeners by adding and removing them on specific conditions.
  • You can leverage Renderer2.listen together with NgZone.runOutsideAngular to suppress Angular change detection.

Stay tuned for the next blog post of this series, which will be on RxJS and how we could use it to listen to DOM events in Angular app.

--

--