It all starts with attaching event listeners to elements. Say you have a button that increments a counter each time you click it.
You can add an inline onclick attribute and that will work across all browsers but will be violating the separation of concerns and the progressive enhancement.
So you should strive for attaching the listener in JavaScript, outside of any markup.
Say you have the following markup:
<button id=”clickme”>Click me: 0</button>
You can assign a function to the onclick property of the node, but you can do this only
once:
// suboptimal solution
var b = document.getElementById(‘clickme’),
count = 0;
b.onclick = function () {
count += 1;
b.innerHTML = “Click me: “ + count;
};
If you want to have several functions executed on click, you cannot do it with this pattern while maintaining loose coupling.
Technically you can check if onclick already
contains a function and if so, add the existing one to your own function and replace the onclick value with your new function. But a much cleaner solution is to use the addEventListener() method.
This method doesn’t exist in IE up to and including version 8, so you need attachEvent() for those browsers.
Without going into all the details right now, let’s just attach a listener to our button:
var b = document.getElementById(‘clickme’);
if (document.addEventListener) { // W3C
b.addEventListener(‘click’, myHandler, false);
} else if (document.attachEvent) { // IE
b.attachEvent(‘onclick’, myHandler);
} else { // last resort
b.onclick = myHandler;
}
Now when this button is clicked, the function myHandler() will be executed.
Let’s have this function increment the number in the “Click me: 0” button label.
To make it a little more interesting, let’s assume we have several buttons and a single myHandler() for all of them. Keeping a reference to each button node and a counter for the number
will be inefficient, given that we can get that information from the event object that is created on every click.
Let’s see the solution first and comment on it after:
function myHandler(e) {
var src, parts;
// get event and source element
e = e || window.event;
src = e.target || e.srcElement;
// actual work: update label
parts = src.innerHTML.split(“: “);
parts[1] = parseInt(parts[1], 10) + 1;
src.innerHTML = parts[0] + “: “ + parts[1];
// no bubble
if (typeof e.stopPropagation === “function”) {
e.stopPropagation();
}
if (typeof e.cancelBubble !== “undefined”) {
e.cancelBubble = true;
}
// prevent default action
if (typeof e.preventDefault === “function”) {
e.preventDefault();
}
if (typeof e.returnValue !== “undefined”) {
e.returnValue = false;
}
}
There are four parts in the event handler function:
• First, we need to gain access to the event object, which contains information about the event and the page element that triggered that event. This event object is passed to the callback event handler, but not when using the onclick property where it’s
accessible through the global property window.event instead.
• The second part is doing the actual work of updating the label.
• Next is canceling the propagation of the event. This is not required in this particular example, but in general if you don’t do it, then the event bubbles up all the way to the document root or even the window object.
Again we need to do it two ways:
the W3C standard way (stopPropagation()) and then differently for IE (usingcancelBubble).
• Finally, prevent the default action, if this is required.
Some events (clicking a link, submitting a form) have default actions, but you can prevent them by using preventDefault() (or for IE, by setting returnValue to false).
Event Delegation
The event delegation pattern benefits from the event bubbling and reduces the number of event listeners attached to separate nodes.
If there are 10 buttons inside a div element, you can have one event listener attached to the div as opposed to 10 listeners attached to each button.
Let’s have an example with three buttons inside a div So we’re working with the following markup:
<div id=”click-wrap”>
<button>Click me: 0</button>
<button>Click me too: 0</button>
<button>Click me three: 0</button>
</div>
Instead of attaching listeners to each button, you attach one to the “click-wrap” div.
Then you can use the same myHandler() function from the previous example with only one little change: you have to filter out clicks you’re not interested in.
In this case you look only for clicks on any button, so all other clicks in the same div could be ignored.
The change to myHandler() would be to check if the nodeName of the source of the event is a “button”:
// …
// get event and source element
e = e || window.event;
src = e.target || e.srcElement;
if (src.nodeName.toLowerCase() !== “button”) {
return;
}
// …
The drawback of the event delegation is the slightly more code to filter out the events that happen in the container that are not interesting for you.
But the benefits — performance and cleaner code — outweigh the drawbacks significantly, so it’s a highly recommended pattern.
Modern JavaScript libraries make it easy to use event delegation by providing convenient APIs.
For example YUI3 has the method Y.delegate(), which enables you to specify a CSS selector to match the wrapper and another selector to match the nodes you’re interested in.
This is convenient because your callback event handler function will actually never be called when the event happens outside the nodes you care about.
In this case, the code to attach an event listener would be simply:
Y.delegate(‘click’, myHandler, “#click-wrap”, “button”);
And thanks to the abstraction of all browser differences done in YUI and the fact that the source of the event is determined for us, the callback function will be much simpler:
function myHandler(e) {
var src = e.currentTarget,
parts;
parts = src.get(‘innerHTML’).split(“: “);
parts[1] = parseInt(parts[1], 10) + 1;
src.set(‘innerHTML’, parts[0] + “: “ + parts[1]);
e.halt();
}
Good Luck
Top comments (0)