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
< view plain text >
  1. // this is how it is
  2. document.addEventListener(
  3.     "click",
  4.     function (evt) { /* stuff */ },
  5.     false
  6. );
  7.  
  8. // this is how it could be as well
  9. var listener = {
  10.     handleEvent: function (evt) {
  11.  
  12.         this === listener; // true
  13.  
  14.         // and evt === classic event object
  15.  
  16.     }
  17. };
  18.  
  19. 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
< view plain text >
  1. document.addEventListener("click", {
  2.     handleEvent: function (evt) {
  3.         // 1 shot callback event example
  4.         switch (evt.target.nodeType) {
  5.             case 1:
  6.             case 9:
  7.                 evt.target.removeEventListener(
  8.                     evt.type,
  9.                     this, // here we are!
  10.                     false
  11.                 );
  12.                 break;
  13.         }
  14.     }
  15. }, 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.

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

Here a usage example:

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

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
< view plain text >
  1. lst.handleEvent({
  2.     target: document.querySelector("#myid"),
  3.     type: "click",
  4.  
  5.     // custom properties
  6.     pageX: 0,
  7.     pageY: 0,
  8.  
  9.     // stubbed methods
  10.     preventDefault: function () {},
  11.     stopPropagation: function () {}
  12. });

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:

  • The alternative way
    eVirtualCompany, a consultancy for IT contractors, was launched last month to provide freelancers with an alternative to...
  • Alternatives to RAID
    RAID has been around for a long time and done a good job of protecting data. But high-capacity drives and new performance demands have spurred...
  • Alternative ways to consolidate your servers
    Exploring alternatives to server consolidation besides virtualization, site expert Andrew Kutz answers this user...
  • $$Return Alternative
    +++--
    3.9 rating from 9 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.