Hacking Angular2: Binding Multiple DOM Events
Learning how to bind multiple events to one function inside Angular2 Templates
‘Hacking Angular2' is a series of posts explaining how to extend Angular2 core classes to implement features reported on Github.
Github Issue we will ‘hack':
“I was curious if there was a better way to bind on multiple events at once…”
Wouldn’t it be nice to bind multiple events to the same expression in our Angular2 Templates? It would probably look like this if we did so:
<div (click,submit,mouseenter)=’foo($event)’></div>
EventManager
In my last post, I described how to create custom events by extending EventManagerPlugin. The EventManagerPlugin is an extendable class which allows you to specify behavior for event strings.
In fact, it is extended in multiple places in the Angular2 source to implement the normal event bindings you are already using.
EventManagerPlugins (AKA the EVENT_MANAGER_PLUGINS provider/OpaqueToken) lives as a private member of EventManager.
How it works
When you add an event to an angular template, while the DOM tree is being parsed, the DomRenderer’s instance calls listen().
As you can see, listen() this then calls the EventManager’s instance function addEventListener(). EventManager then searches through its list of registered EventManagerPlugins calling their supports() function and passing the eventName.
This function returns either true or false telling the EventManager whether or not this plugin can/is supposed to handle this custom event. When a valid plugin is found, the manager calls that plugin’s version of addEventListener() performing the custom action.
Step 1: Creating a new plugin
Now that we have an idea how this all works, lets implement a new plugin.
Like I mentioned above, we want to have multiple events on our Angular2 templates tie to the same expression like this:
<div (click,submit,mouseenter)=’foo($event)’></div>
If thats the case the rules for our supports() function should be pretty clear. We need a string which has (1 or more) commas in it, that separate event names. We should also handle when people put something stupid in like “(,click)”. So I wrote my supports() function as follows:
This will allows EventManager to delegate event strings like “click,submit,mousedown” to this plugin.
Step 2: implementing the eventListeners
Now that we have implemented supports(), EventManager will now call plugin.addEventListener() so the plugin can define its custom behavior.
Our custom behavior is simple: “add event listeners for all the events in the eventArray that we parsed”.
Fail Number 1
My first attempt as you can see has me using DOM.onAndCancel(element, singleEventName, outsideHandler); This is the equivalent of conveniently performing the native addEventListener() and removeEventListener().
This however came with some issues. For example when I used “(clickOutside,click,submit)” which includes a custom event (created from another plugin), it did not fire because it was executed in the current plugin’s context.
Success!!!
Wait, but we do have the EventManager instance available, instead of attaching the events in this plugin, why don’t we pass EventManager the events, and let it do its job and figure out which plugins should use them.
So I refactored the code to look like this:
Step 3: add it to EVENT_MANAGER_PLUGINS
This worked like a charm!!!!! All that was left to do was to implement the plugin and bootstrap it.
I’ve published an npm module for this plugin that you can add to your Angular2 app with ease! Feel free to check it out or refer to its github page.
You can also find a working example of this code in my Angular2-Webpack-Lite repository under the experiment branch and a plunkr implementing the plugin.
Why is this a ‘hack’?
If implementing this feature was as easy as seen here, you’d think it would have already been done.
There are a few things to keep into consideration for compatibility:
- The `(eventName)` syntax we use on the DOM is purely sugar. This plugin works with the sugar, however the cannonical ‘on-event’ name also binds to Angular2 functions. Therefore when trying to use the following will not work (and turns out only the first event will trigger):
<input type='text' on-blur,on-click=”logEvents($event)” />