Tuesday, July 13th, 2010
An alternative way to addEventListener
<blockquote>I can't believe none of us knew DOM2
This is how a tweet from @SubtleGradient, re-tweeted by @jdalton, has been able to steal my rest tonight ... and this post is the consequence ...
What's new in a nutshell
There is a W3C Recommendation about addEventListener behavior, which clearly specify the second argument as an EventListener.
The new part is that no library I know has ever used a proper EventListener interface, preferring the classic attached callback instead.
-
-
-
// this is how it is
-
document.addEventListener(
-
"click",
-
function (evt) { /* stuff */ },
-
false
-
);
-
-
// this is how it could be as well
-
var listener = {
-
handleEvent: function (evt) {
-
-
this === listener; // true
-
-
// and evt === classic event object
-
-
}
-
};
-
-
document.addEventListener("click", listener, false);
Benefits
The most common case that may disappear is well explained in this MDC addEventListener page.
Rather than bind inline or add anonymous functions to make our object call context preserved, we can simply add an handleEvent method to whatever object and pass it as EventListener.
Moreover, being close to full ES5 support and "use strict" directive where arguments.callee disappears, it may be more than handy to be able to perform such operation:
-
-
document.addEventListener("click", {
-
handleEvent: function (evt) {
-
// 1 shot callback event example
-
switch (evt.target.nodeType) {
-
case 1:
-
case 9:
-
evt.target.removeEventListener(
-
evt.type,
-
this, // here we are!
-
false
-
);
-
break;
-
}
-
}
-
}, false);
-
An opened door for custom listeners
As I have recently posted, custom listeners implementation can be truly handy when we are dealing with events driven applications, but as soon as I have read the tweet, I had to rewrite a fresh new way to create a listener. Please note that following code is assuming that the browser supports both DOM Level 2 and Array extras, which is true for all modern browsers, mobile oriented included.
-
-
function createEventListener() {
-
-
/*! Andrea Giammarchi for Ajaxian - Mit Style */
-
-
// a function declaration reused internally
-
function notifyEvent(callback, i, stack) {
-
// use DOM Level 0 events strategy
-
// to stop the loop if necessary
-
// checking if the result is exactly false
-
if (callback.call(
-
// the curent object as context
-
this,
-
// the classic event as first argument
-
event,
-
// the called callback (life easier)
-
callback,
-
// again the current context
-
// if the callback has been bound
-
this
-
) === false) {
-
// if false, reassign the current stack ...
-
eventListener["@"+event.type] = stack.slice();
-
// ... and break the current forEach loop
-
// (or, for the record, whatever Array.extras)
-
stack.length = 0;
-
}
-
}
-
-
var
-
// local scoped object, reachable internally
-
// usable as mixin so instances won't be polluted
-
// with all possible event types
-
// the type is prefixed in any case
-
// so that name clashes should be
-
// really rare however we use the object
-
eventListener = {
-
-
// we attach to a proper stack
-
addEvent: function (type, callback) {
-
var
-
// try to retrieve the stack ...
-
stack = (
-
// if already there ...
-
eventListener["@" + type] ||
-
// otherwise we create it once
-
(eventListener["@" + type] = [])
-
),
-
// as addEventListener, don't attach
-
// the same event twice
-
i = stack.indexOf(callback)
-
;
-
// so if it was not there ...
-
if (-1 === i) {
-
// FIFO order via stack
-
stack.push(callback);
-
}
-
},
-
-
// called via addEventListener
-
// the "this" reference will be
-
// the eventListener object,
-
// or the current instance
-
// if used as "class" mixin
-
// or via Object.create / clone / merge
-
handleEvent: function (e) {
-
// retrieve the stack
-
var stack = eventListener["@" + e.type];
-
// and if present ...
-
if (stack) {
-
// set temporarily the local event var
-
event = e;
-
// notify all registered callbacks
-
// using current this reference
-
// as forEach context
-
stack.forEach(notifyEvent, this);
-
// let the GC handle the memory later
-
event = null;
-
}
-
},
-
-
// how we remove the event, if any ...
-
removeEvent: function (type, callback) {
-
var
-
// try to retrieve the stack ...
-
stack = eventListener["@" + type],
-
// find the index ...
-
i
-
;
-
// if the stack is present
-
if (stack && ~(
-
i = stack.indexOf(callback)
-
)) {
-
// remove it
-
stack.splice(i, 1);
-
}
-
}
-
},
-
-
// I could have called this variable tmp
-
// but it's actually the current event
-
// once assigned ... so ...
-
event
-
;
-
-
// ready to go!
-
return eventListener;
-
-
}
Here a usage example:
-
-
var lst = createEventListener();
-
-
/** mixin example (add a slash before this line to test)
-
function MyEventListener() {}
-
MyEventListener.prototype.addEvent = lst.addEvent;
-
MyEventListener.prototype.handleEvent = lst.handleEvent;
-
MyEventListener.prototype.removeEvent = lst.removeEvent;
-
lst = new MyEventListener;
-
// */
-
-
document.addEventListener("click", lst, false);
-
-
lst.addEvent("click", function click(e, callback, object){
-
-
alert([
-
callback === click, // true
-
this === lst, // true
-
this === object, // true
-
e.type === "click" // true
-
]);
-
-
// test that furthermore this
-
// callback won't be fired again
-
this.removeEvent("click", callback);
-
-
// add delayed a callback
-
// without any valid reason :-)
-
setTimeout(function (self) {
-
// test addEvent again
-
self.addEvent("click", function () {
-
alert(2);
-
});
-
}, 0, this);
-
-
// block the current notification
-
return false;
-
});
-
-
// the event fired only the second click
-
lst.addEvent("click", function () {
-
alert(1);
-
});
-
-
/** de-comment the mixin example to test
-
// that no @click is attached ;-)
-
for (var key in lst) {
-
alert(key);
-
}
-
// */
-
Advantages
- both evt.stopPropagation() and evt.preventDefault() are not able to break the current notification of all attached listeners, if added to the same node, and while the FIFO order gives to the node "owner" or creator the ability to pollute the event object with some flag such evt.pleaseDontDoAnyOtherActionHere = true, not every library, script, or framework, may respect or understand this flag. With custom events we can adopt better strategies to actually avoid any other operation if this is what we meant, because we arrived before over the node and we may like to be that privileged
- being custom, we can also decide which argument should be passed for each callback, simplifying most common problems we may have when dealing with listeners
- we can better decouple DOM and listeners, being able to remove whatever amount of callbacks simply calling once node.removeEventListener(evt.type, this, false); inside any kind of notification
- being based on standard and modern browsers, we can use native power, in this case provided by forEach and indexOf operations, so that performances will be best possible
- thanks to automatic context injection, we can still reuse callbacks for different listeners, through bind, or simply considering the current context once called (or in this case the third argument by reference, if the context is different)
Last but not least, if we would like to fire an event we can bypass DOM initialization using handleEvent directly, e.g.
-
-
lst.handleEvent({
-
target: document.querySelector("#myid"),
-
type: "click",
-
-
// custom properties
-
pageX: 0,
-
pageY: 0,
-
-
// stubbed methods
-
preventDefault: function () {},
-
stopPropagation: function () {}
-
});
-
Compatibility ?
Apparently both W3C behavior and provided examples are compatible with every modern browser with DOM Level 2 support, and I believe this is great.
The only one behind here is IE9 pre 3, but again @jdalton has acted at speed light, thanks!
Related Content:











I use handleEvent for touch events on the iPhone. You have one object that you can assign to 3 events (start, move and end) and then you can sort out which event was triggered and perform the actions. You can also add add and remove events with ‘this’ which is very handy.
If you only want a reference to the event listener (in order to remove the event listener after the first use), you can simply name the function:
someElement.addEventListener(“click”, function listener(evt) {
someElement.removeEventListener(evt.type, listener, false);
}, false);
No arguments.callee needed.
actually true, I have done the same for the function click indeed in last example, but there is no “only want to” from my side, I have listed different points.
Maybe this was not that relevant at all except that via object you will remove all added callbacks rather than a single function per time. Thanks for the point
Probably most usefull is removing:
instead of something like this:
function go_once(x, type, dg, data) {
var d = {context:data, f:dg};
var f = function(e) { d.e=e; d.f(type, d); }
d.self = f;
d.remove = function() { x.removeEventListener(type, f, false);
x.addEventListener(type, f, false);
}
go_once(element, “click”, function(type, d) {
d.remove();
… use d.e …
}, “additional data”);
I have read DOM3 events spec few days ago, and also looked at Event Interface, but it was obsucre. I had not enaugh knowledge of JS object model to use it.
But reading your post it loks that it is sufficent for me to use:
x.addEventListener(“click”, {
handleEvent: function(e) {
x.removeEventListener(“click”, this, false);
use e and “additional data”
}, false);
Thanks, for this. Still, my mind cannot connect that arrays/dictionaries/function IS an object. And a fact that this, referes to this object, is quite shocking for me :) Will know now this!
it’s handy to bring a reference for many different reasons, e.g.
var whateverObject = {
handleEvent: function (e) {
if (this.data) {
// do some stuff
}
}
};
magicElement.addEventListener(“click”, whateverObject, false);
// further, asynchronously …
whateverObject.data = JSON.parse(text);
In the meanwhile we can click the element 1 hundred times but if no data there, nothing will happens.
This means no scope lookup and a reference rather than a callback, as I have said, many opened doors for custom listeners :-)
What does David Mark have to say about this!
@Jesse
“What does David Mark have to say about this!”
I think the handleEvent method trick is more efficient than wrapping listener functions to set – this – but wonder how well it is supported. Apparently MS hadn’t heard of it, but that’s not surprising.
Interesting anyway. First I’ve heard of it as well.
PS. I rarely visit this site, so questions for me are best asked by email. ;)
iirc, this does not work in Safari 2.
Dean, I have said modern browsers :D
Some old browsers or JS frameworks can’t take EventListeners for event binding, so maybe this could help :
function eventListenerToCallback( el ){
if(el.handleEvent)
{
var f = (function(){
return arguments.callee.eventListener.handleEvent.apply(
arguments.callee.eventListener, arguments
);
});
f.eventListener = el;
return f;
}
return el;
}
// and then
jQuery(‘#myButton’).click(eventListenerToCallback({
name:”Doh”, handleEvent : function(e){ alert(this.name); }
} ));
// But, yes, “this” is no mose the current DOMElement, but the EventListener itself
return el.handleEvent.apply(el, arguments);
with the returned function was too clean, isn’t it?
good hint tho ;-) thanks