Global Events and Event Delegation in Angular 2
In Angular 2, we are used to listening for events on the Component's DOM elements, however how do we listen to events outside of this range? Two ways to do this:
- Creating a Host Listener
- Using the Component's host property
The Host Listener Way
Angular 2 provides a HostListener Decorator in order to match an event triggered on a target with a function declared on the next line.
Just like this:
import { HostListener, Component } from "@angular/core";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"]
})
export class AppComponent {
@HostListener("document:click", ["$event"])
onDocumentClicked(ev) {
console.log("clicked", ev);
}
}
The syntax is as follow: 'target:event', [args].
When clicking on the document, we will be able to use the event triggered, you can also pass other arguments like $event.target for example.
However the target must be global, if you prefer, you can replace document by window.
The Host Way
Angular 2 Components provide a host property in order to match a function with an event triggered on a target.
import { HostListener, Component } from "@angular/core";
@Component({
selector: "app-root",
templateUrl: "./app.component.html",
styleUrls: ["./app.component.css"],
host: {
"(document:click)": "onDocumentClicked($event)"
}
})
export class AppComponent {
onDocumentClicked(ev) {
console.log("clicked", ev);
}
}
The syntax is as follow: ('target:event)': 'function(args)'.
Same mechanism as the HostListener, only global and configuration friendly.
Event Delegation
Now that we have more experience under our belt, we can tackle one very important JavaScript concept: Event Delegation.
I discovered this concept thanks to the Great David Walsh, head there for a quick 5-minutes read.
So how do we do this in Angular 2?
Let's start by using a similar template:
<ul #ulEl id="parent-list">
<li id="post-1">Item 1</li>
<li id="post-2">Item 2</li>
<li id="post-3">Item 3</li>
<li id="post-4">Item 4</li>
<li id="post-5">Item 5</li>
<li id="post-6">Item 6</li>
</ul>
Notice the #ulEl?
It's a reference that we will use in order to attach the listener in our Component here:
import {ElementRef, Renderer, ViewChild, Component} from '@angular/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent {
constructor (private renderer: Renderer){ }
@ViewChild('ulEl') ulEl: ElementRef;
ngOnInit() {
this.tmpListener = this.renderer.listen(this.ulEl.nativeElement, 'click', this.logElement);
}
// Shorthand to get event.target
logElement({target}) {
if(target && target.nodeName == "LI") {
console.log('Target id: ', target.id);
// Add Business Logic here
}
}
ngOnDestroy() {
this.tmpListener();
}
}
We get our ul Element and stock it as an ElementRef.
When we init our Component, we use the Renderer (for cross-platform compatibility) in order to attach an EventListener.
If the <ul> or a <li> tag is clicked, we trigger the logElement function.
This function will acquire the target from the event, if this target is a <li> tag we log the id property.
Conclusion
As usual many ways to do the work.
I prefer to go with the host property of a Component, this solution clearly lists every listener - target - function matching.
Event Delegation is an interesting trick that can help you for specific cases, it's being discussed a lot in the community, some times as a savior (see the comments here) and on other cases just as a little plus (there).