This post was last updated 08/12/2016.
One of the nicest changes in Angular 2 is the new template syntax. The declarative nature of the brackets around a property and parentheses around events initially worried many people but it has really grown on me.
For example:
<myElement (click)=“doSomething($event)”></myElement>
Right away, I know clearly that we’re binding to the (click) event in the DOM, and firing doSomething, and passing in the $event object. Pretty straightforward in this example but we can see where it really saves us when we have something like this:
<myElement (data)=”doSomething($event)”></myElement>
Now the problem here is that (data) is ambiguous. If it was just a regular attribute without the brackets, I would have to dive into the component itself to see whether or not this was an output or input property, or maybe some native attribute I didn't know about. However, with the (event) syntax it becomes much more clear and readable. That being said, I'd recommend naming your outputs something a bit more informative, but that's not the point of this post.
Now, in this case (data) HAS to be a custom event created inside of the myElement component because by default Angular2 provides you with all the regular DOM events, as well as some hammer.js events on each element without needing to define it in every individual component.
Does this mean we as developers have to define these events inside each individual component? What if we want an application-wide event? What if there is a js library that decorates DOM elements with special events and we want to access them?
Unfortunately, as you probably guessed, we can’t just include that library in our project and suddenly have the ability to access these events with our (event) syntax.
There are a few ways around this. For example, you could create a base component that emitted any wanted events and extend it:
@Component({})
class Dog {
@Output() pet = new EventEmitter();
@HostListener('click', ['$event'])
bark(e) {
this.pet.emit('woof');
}
}
@Component({
selector: 'poodle',
template: `<h2>I'm a poodle!</h2>`
})
class Poodle extends Dog {}
//In a pretend component's template
<poodle (pet)="listenToPet($event)"></poodle> //console log, spoiler: it says 'woof'
Here we have Dog as a class and we can see that we are listening to the click event (using @HostListener) on the component, and when it fires, emitting 'woof' up through the pet event. This method works! However, maybe this isn't the right solution for you. Is there another way?
There is! Let's hijack Angular2's internal event management system! It sounds a lot more exciting than it actually is. Here’s how it’s done - and as a note, this blog post was heavily inspired by the amazing work done over at Ben Nadel’s Blog: http://www.bennadel.com/blog/3025-creating-custom-dom-and-host-event-bindings-in-angular-2-beta-6.htm
Let’s take a look at a make-believe bootstrap of an Angular2 application:
Import App from ‘app’;
Import CustomEvent from ‘custom-event’;
import {EVENT_MANAGER_PLUGINS} from ‘@angular/platform-browser/src/dom/events/event_manager’;
bootstrap(App, [
EVENT_MANAGER_PLUGINS, {
useClass: ResizeEventPlugin
}
]);
First - we’re providing bootstrap with an EVENT_MANAGER_PLUGIN class and, additionally, replacing it with a custom class of our own named ResizeEventPlugin.
Now let’s look at the inside of this ResizeEventPlugin class
export default class ResizeEventPlugin {
private static EVENT_NAME: string = ‘elementResize’;
//Element Resize Detector is a library I use to detect if an element changes in size
private static _erd = require('element-resize-detector')({
strategy: 'scroll'
});
static addElementResizeDetector = (element, resizeCallback) => () => {
ResizeEventPlugin._erd.listenTo(element, resizeCallback);
};
static removeElementResizeDetector = (element) => ()=> {
ResizeEventPlugin._erd.uninstall(element);
};
addEventListener(element, eventName, handler){
let zone = this.manager.getZone();
zone.runOutsideAngular(ResizeEventPlugin.addElementResizeDetector(element, handler));
return ResizeEventPlugin.removeElementResizeDetector(element);
}
supports(eventName) {
return eventName === ResizeEventPlugin.EVENT_NAME;
}
}
That's it! You’re more or less done once you’ve written this out. But what’s happening here? Let’s look at the supports method first. This method is essentially a filter - it runs through all instances of event markup in the template. For example, if it sees a (keyup) event in a template, it'll grab that instance and fire off supports with 'keyup' as the first param. If you return true, then the addEventListener method fires for that instance. So you can be additionally clever here and check for all sorts of events that already exist and have additional functionality. Maybe you want to log everything you click on? Just hijack the click event in supports and handle it in the addEventListener. In our case, we’re checking to see if the event name passed in matches the name of this output we’re creating.
Let's move on to the addEventListener, as this expects a few useful parameters to be passed in. First, the DOM reference of the element in question. Second, the name of the event that triggered this listener. Finally, the function that is being fired with this event. For example:
<div (elementResize)=”checkIfTooBig($event)”> </div>
In this case, checkIfTooBig is what’s being passed in as the third parameter in addEventListener.
Next, inside the listener we see that we’re doing some stuff with zone - something that is available on the ‘manager’ property. That property wasn’t explicitly defined by me but instead provided to the EVENT_MANAGER_PLUGIN class (which we are overwriting), so it might be confusing seeing it here. Needless to say, it’s available. So in this case I am creating a new zone to run this non-angular code in, and this allows Angular to properly handle this event within it's own internal change detection.
Finally, the return statement is very important as it handles what to do when that element is destroyed. In my case, I want to remove my third party library's listener which is important in preventing memory leaks.
And there you have it! There is some variation as to how you can handle this, and Ben Nadel shows you something similar in ES5 - but I hope this example gets you off on the right foot!