Activate your free membership today | Log-in

Tuesday, July 13th, 2010

An alternative way to addEventListener

Category: Examples, JavaScript, Standards, W3C

<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.

JAVASCRIPT:
  1.  
  2.  
  3. // this is how it is
  4. document.addEventListener(
  5.     "click",
  6.     function (evt) { /* stuff */ },
  7.     false
  8. );
  9.  
  10. // this is how it could be as well
  11. var listener = {
  12.     handleEvent: function (evt) {
  13.  
  14.         this === listener; // true
  15.  
  16.         // and evt === classic event object
  17.  
  18.     }
  19. };
  20.  
  21. 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:

JAVASCRIPT:
  1.  
  2. document.addEventListener("click", {
  3.     handleEvent: function (evt) {
  4.         // 1 shot callback event example
  5.         switch (evt.target.nodeType) {
  6.             case 1:
  7.             case 9:
  8.                 evt.target.removeEventListener(
  9.                     evt.type,
  10.                     this, // here we are!
  11.                     false
  12.                 );
  13.                 break;
  14.         }
  15.     }
  16. }, false);
  17.  

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.

JAVASCRIPT:
  1.  
  2. function createEventListener() {
  3.  
  4.     /*! Andrea Giammarchi for Ajaxian - Mit Style */
  5.  
  6.     // a function declaration reused internally
  7.     function notifyEvent(callback, i, stack) {
  8.         // use DOM Level 0 events strategy
  9.         //  to stop the loop if necessary
  10.         // checking if the result is exactly false
  11.         if (callback.call(
  12.             // the curent object as context
  13.             this,
  14.             // the classic event as first argument
  15.             event,
  16.             // the called callback (life easier)
  17.             callback,
  18.             // again the current context
  19.             // if the callback has been bound
  20.             this
  21.         ) === false) {
  22.             // if false, reassign the current stack ...
  23.             eventListener["@"+event.type] = stack.slice();
  24.             // ... and break the current forEach loop
  25.             // (or, for the record, whatever Array.extras)
  26.             stack.length = 0;
  27.         }
  28.     }
  29.  
  30.     var
  31.         // local scoped object, reachable internally
  32.         // usable as mixin so instances won't be polluted
  33.         // with all possible event types
  34.         // the type is prefixed in any case
  35.         // so that name clashes should be
  36.         // really rare however we use the object
  37.         eventListener = {
  38.  
  39.             // we attach to a proper stack
  40.             addEvent: function (type, callback) {
  41.                 var
  42.                     // try to retrieve the stack ...
  43.                     stack = (
  44.                         // if already there ...
  45.                         eventListener["@" + type] ||
  46.                         // otherwise we create it once
  47.                         (eventListener["@" + type] = [])
  48.                     ),
  49.                     // as addEventListener, don't attach
  50.                     // the same event twice
  51.                     i = stack.indexOf(callback)
  52.                 ;
  53.                 // so if it was not there ...
  54.                 if (-1 === i) {
  55.                     // FIFO order via stack
  56.                     stack.push(callback);
  57.                 }
  58.             },
  59.  
  60.             // called via addEventListener
  61.             // the "this" reference will be
  62.             // the eventListener object,
  63.             // or the current instance
  64.             // if used as "class" mixin
  65.             // or via Object.create / clone / merge
  66.             handleEvent: function (e) {
  67.                 // retrieve the stack
  68.                 var stack = eventListener["@" + e.type];
  69.                 // and if present ...
  70.                 if (stack) {
  71.                     // set temporarily the local event var
  72.                     event = e;
  73.                     // notify all registered callbacks
  74.                     // using current this reference
  75.                     // as forEach context
  76.                     stack.forEach(notifyEvent, this);
  77.                     // let the GC handle the memory later
  78.                     event = null;
  79.                 }
  80.             },
  81.  
  82.             // how we remove the event, if any ...
  83.             removeEvent: function (type, callback) {
  84.                 var
  85.                     // try to retrieve the stack ...
  86.                     stack = eventListener["@" + type],
  87.                     // find the index ...
  88.                     i
  89.                 ;
  90.                 // if the stack is present
  91.                 if (stack && ~(
  92.                     i = stack.indexOf(callback)
  93.                 )) {
  94.                     // remove it
  95.                     stack.splice(i, 1);
  96.                 }
  97.             }
  98.         },
  99.  
  100.         // I could have called this variable tmp
  101.         // but it's actually the current event
  102.         // once assigned ... so ...
  103.         event
  104.     ;
  105.  
  106.     // ready to go!
  107.     return eventListener;
  108.  
  109. }

Here a usage example:

JAVASCRIPT:
  1.  
  2. var lst = createEventListener();
  3.  
  4. /** mixin example (add a slash before this line to test)
  5. function MyEventListener() {}
  6. MyEventListener.prototype.addEvent = lst.addEvent;
  7. MyEventListener.prototype.handleEvent = lst.handleEvent;
  8. MyEventListener.prototype.removeEvent = lst.removeEvent;
  9. lst = new MyEventListener;
  10. // */
  11.  
  12. document.addEventListener("click", lst, false);
  13.  
  14. lst.addEvent("click", function click(e, callback, object){
  15.  
  16.     alert([
  17.         callback === click, // true
  18.         this === lst,       // true
  19.         this === object,    // true
  20.         e.type === "click"  // true
  21.     ]);
  22.  
  23.     // test that furthermore this
  24.     // callback won't be fired again
  25.     this.removeEvent("click", callback);
  26.  
  27.     // add delayed a callback
  28.     // without any valid reason :-)
  29.     setTimeout(function (self) {
  30.         // test addEvent again
  31.         self.addEvent("click", function () {
  32.             alert(2);
  33.         });
  34.     }, 0, this);
  35.  
  36.     // block the current notification
  37.     return false;
  38. });
  39.  
  40. // the event fired only the second click
  41. lst.addEvent("click", function () {
  42.     alert(1);
  43. });
  44.  
  45. /** de-comment the mixin example to test
  46. //   that no @click is attached ;-)
  47. for (var key in lst) {
  48.     alert(key);
  49. }
  50. // */
  51.  

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.

JAVASCRIPT:
  1.  
  2. lst.handleEvent({
  3.     target: document.querySelector("#myid"),
  4.     type: "click",
  5.  
  6.     // custom properties
  7.     pageX: 0,
  8.     pageY: 0,
  9.  
  10.     // stubbed methods
  11.     preventDefault: function () {},
  12.     stopPropagation: function () {}
  13. });
  14.  

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:

Posted by webreflection at 8:00 am
11 Comments

++++-
4.2 rating from 6 votes

11 Comments »

Comments feed TrackBack URI

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.

Comment by RaymanNL — July 13, 2010

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.

Comment by mattiacci — July 13, 2010

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

Comment by webreflection — July 13, 2010

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!

Comment by movax — July 13, 2010

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 :-)

Comment by webreflection — July 13, 2010

What does David Mark have to say about this!

Comment by Jesse — July 13, 2010

@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. ;)

Comment by DavidMark — July 14, 2010

iirc, this does not work in Safari 2.

Comment by deanedwards — July 15, 2010

Dean, I have said modern browsers :D

Comment by webreflection — July 15, 2010

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

Comment by Alkot — July 15, 2010

return el.handleEvent.apply(el, arguments);
with the returned function was too clean, isn’t it?

good hint tho ;-) thanks

Comment by webreflection — July 22, 2010

Leave a comment

You must be logged in to post a comment.