DailyJS

Let's Make a Framework: Events Part 2

Alex R. Young

Subscribe

@dailyjs

Facebook

Google+

tutorials frameworks events web lmaf

Let's Make a Framework: Events Part 2

Posted by Alex R. Young on .
Featured

tutorials frameworks events web lmaf

Let's Make a Framework: Events Part 2

Posted by Alex R. Young on .

Welcome to part 10 of Let's Make a Framework, the ongoing series about
building a JavaScript framework. This part continues looking at events.

If you haven't been following along, these articles are tagged with
lmaf. The project we're creating is called Turing and is available on GitHub:
turing.js.

This part explores an approach to event handling used by most
frameworks. The implementation is a basic one that will be expanded next
week.

Event Registration

Last week I explained how event registration works when using attributes
and properties like onclick. This is a simple way to manage
cross-browser events, but isn't quite suitable for a framework.

For one thing, managing multiple events for the same type can become
confusing:

element.onclick = function() { alert('Hello World!'); return false; };
element.onclick = function() { alert('This was the best example I could think of'; return false; };
// Guess what happens when the element is clicked?

To get around this, some developers use a registry or cache to track and
remove events. For a discussion and examples of this, take a look at
addEvent recording contest
on QuirksBlog.

W3C and Microsoft

The Level 2 Events
Specification

tried to encourage browser developers to provide a better API, but in
the end wasn't supported by Microsoft. From my experiences of
framework-less JavaScript I can tell you that this model is pretty
sound.

The quest for browser support using this API has resulted in lots of
similar event handling implementations. Most conditionally use the W3C
or Microsoft APIs.

W3C Event Handling

Adding an event handler to an element looks like this:

element.addEventListener('click', function() { }, false);

Type is W3C's terminology for event names, like 'click'. The third
parameter determines if capturing should be initiated. We're not going
to worry about capturing here. The event handler is passed an
event object, which has a target property.

Events can be removed with removeEventListener(), with the
same parameters. It's important that the callback matches the one the
event was registered with.

Events can be fired programatically with dispatchEvent().

Microsoft

Before getting disgruntled about Microsoft breaking the universe again,
their implementation almost stands up:

var handler = function() { };
element.attachEvent('onclick', handler);
element.detachEvent('onclick', handler);

// Firing events
event = document.createEventObject();
return element.fireEvent('on' + type, event)

This is mostly similar to W3C's recommended API, except 'on' is used for
event type names.

The two main issues with Microsoft's implementation are memory
leaks

and the lack of a target parameter. Most frameworks handle
memory leaks by caching events and using onunload to
register a handler which clears events.

As for target, jQuery's fix method maps IE's
proprietary srcElement
property
,
and Prototype does something similar when it extends the
event object:

Object.extend(event, {
  target: event.srcElement || element,
  relatedTarget: _relatedTarget(event),
  pageX: pointer.x,
  pageY: pointer.y
});

Capabilities and Callbacks

Our event handling code would be a lot simpler without Microsoft's API,
but it's not a huge problem for the most part. Capability detection is
simple in this case because there's no middle-ground -- browsers either
use W3C's implementation or they're IE. Here's an example:

if (element.addEventListener) {
  element.addEventListener(type, responder, false);
} else if (element.attachEvent) {
  element.attachEvent('on' + type, responder);
}

The same approach can be repeated for removing and firing events. The
messy part is that the event handlers need to be wrapped in an anonymous
function so the framework can correct the target property.
The process looks like this:

  1. The event is set up by the framework:
    turing.events.add(element, type, handler)
  2. The handler is wrapped in a callback that can fix browser
    differences
  3. The event object is passed to the original event handler
  4. When an event is removed, the framework matches the passed in event
    handler with ones in a "registry", then pulls out the wrapped handler

Both jQuery and Prototype use a cache to resolve IE's memory leaks. This
cache can also be used to help remove events.

Valid Elements

Before adding an event, it's useful to check that the supplied element
is valid. This might sound strange, but it makes sense in projects where
events are dynamically generated (and also with weird browser
behaviour).

The nodeType is checked to make sure it's not a text node
or comment node:

function isValidElement(element) {
  return element.nodeType !== 3 && element.nodeType !== 8;
}

I got this idea from jQuery's
source
.

API Design

At the moment the Turing API reflects W3C's events:

  • Add an event: turing.events.add(element, type, callback)
  • Remove an event: turing.events.remove(element, type, callback)
  • Fire: turing.events.fire(element, type)
  • An event object is passed to your callback with the target property fixed

Tests

Like previous parts of this project, I started out with tests. I'm still
not quite happy with the tests though, but they run in IE6, 7, 8,
Chrome, Firefox and Safari.

var element = turing.dom.get('#events-test a')[0],
    check = 0,
    callback = function(e) { check++; return false; };

should('add onclick', function() {
  check = 0;
  turing.events.add(element, 'click', callback);
  turing.events.fire(element, 'click');
  turing.events.remove(element, 'click', callback);
  return check;
}).equals(1);

I use a locally-bound variable called check which counts
how many time an event has fired using the event's handler.

The tests also ensure that attaching global handlers work (on
document).

Conclusion

The code is in the GitHub repo
for you to experiment with (and supply fixes!)

Although there's still more to do, the W3C/Microsoft approach works
well. I actually experimented with implementations that manage lists of
traditional event handlers rather than using the more modern API, and I
spent several hours looking at Prototype and jQuery's APIs before
arriving at this point.

You'll notice production frameworks contain a lot more code than Turing
-- jQuery actually implements its own bubbling system, and both frameworks have to deal with inconsistencies for certain types of
events. I've left those browser behaviour patches out for now in the
interest of brevity.

If you'd like to expand your JavaScript knowledge on this subject, here
are the references I used: