Let's Make a Framework: More on Chained Events
Welcome to part 35 of Let’s Make a Framework, the ongoing series about building a JavaScript framework.
If you haven’t been following along, these articles are tagged with lmaf. The project we’re creating is called Turing.
Last week I talked about NodeList and converting it into an Array. This week I’m going to improve the chained API for events.
Event Handler Shortcuts and Loop Scoping
The only DOM event handler I added to our chained API was click. Let’s add more of the events named after the HTML ‘on’ attributes. I used the list on MDC’s Event Handlers page as a reference, and set up aliases like this:
events.addDOMethods = function() {
if (typeof turing.domChain === 'undefined') return;
turing.domChain.bind = function(type, handler) {
var element = this.first();
if (element) {
turing.events.add(element, type, handler);
return this;
}
};
var chainedAliases = ('click dblclick mouseover mouseout mousemove ' +
'mousedown mouseup blur focus change keydown ' +
'keypress keyup resize scroll').split(' ');
for (var i = 0; i < chainedAliases.length; i++) {
(function(name) {
turing.domChain[name] = function(handler) {
return this.bind(name, handler);
};
})(chainedAliases[i]);
}
};
The technique I used to create an array from a string is found throughout jQuery. It’s handy because it has less syntax than lots of commas and quotes. I use the anonymous function to capture the name parameter for each alias, doing it with var name = chainedAliases[i] would bind name to the last value executed, which isn’t what we want.
In jQuery’s code they use jQuery.each to iterate over the event names, which actually reads better. I put our iterators in turing.enumerable.js and have been avoiding interdependence between modules, so I’m doing it the old fashioned way.
However, doing it this way does illustrate an interesting point about JavaScript’s lexical scoping and closures. Try this example in a prompt or browser console:
var methods = {},
items = ['a', 'b', 'c'];
for (var i = 0; i < items.length; i++) {
var item = items[i];
methods[item] = function() {
console.log('Item is: ' + item);
};
}
console.log('After the for loop, item is: ' + item);
for (var name in methods) {
console.log('Calling: ' + name);
methods[name]();
}
This will result in:
After the for loop, item is: c
Calling: a
Item is: c
Calling: b
Item is: c
Calling: c
Item is: c
Why is each item set to ‘c’ when each function is called? In JavaScript, variables declared in for are in the same scope rather than a new local scope. That means there aren’t three item variables, there is just one. And this is why jQuery’s version is more readable. I’ve edited this version of jQuery’s events.js to illustrate the point:
jQuery.each(aliases, function(i, name) {
jQuery.fn[name] = function(data, fn) {
return this.bind(name, data, fn);
};
});
Variables declared inside jQuery.each are effectively in a different scope, and of course the name parameter passed in on each iteration is the one we want.
Trigger vs. Bind
Calling jQuery().click() without a handler actually fires the event, which I’ve always liked. Can we do something similar?
We just need to check if there’s a handler in turing.domChain.bind:
if (handler) {
turing.events.add(element, type, handler);
} else {
turing.events.fire(element, type);
}
While I was looking at turing.domChain.bind I changed it to bind to all elements instead of the first one. I thought that way felt more natural.
You could do a quick test with this:
$t('p').click(function(event) {
event.target.style.backgroundColor = '#ff0000'
});
It’ll bind to all of the paragraphs instead of just the first one.
Conclusion
Generating sets of functions or method aliases iteratively is straightforward in JavaScript, but you need to be aware of scope and pay attention to closures. It’s easy to see why jQuery.each is in core.js — it makes framework code simpler, especially given how jQuery handles context.