Effective Event Binding with jQuery
If you've used jQuery much at all, then you're probably already familiar with event binding. It's fairly basic stuff, but dig a little deeper and you'll find opportunities to make your event-driven code less brittle and more manageable.
A Better Selector Strategy
Let's start with a basic example. Here's the HTML for a nav menu that can be toggled on or off:
<button class="nav-menu-toggle">Toggle Nav Menu</button>
<nav>
<ul>
<li><a href="/">West Philadelphia</a></li>
<li><a href="/cab">Cab Whistling</a></li>
<li><a href="/throne">Throne Sitting</a></li>
</ul>
</nav>
And here's some JavaScript to toggle the nav menu when the button is clicked:
$('.nav-menu-toggle').on('click', function() {
$('nav').toggle();
});
This is probably the most common approach. It works, but it's brittle. The JavaScript depends on the button element having the nav-menu-toggle
class. It would be very easy for another developer, or even a forgetful you in the future, to not realize this and remove or rename the class while refactoring.
The heart of the problem is that we're using CSS classes for both presentation and interaction. This violates the separation of concerns principle, making maintenance more error-prone.
Let's try a different approach:
<button data-hook="nav-menu-toggle">Toggle Nav Menu</button>
<nav data-hook="nav-menu">
<ul>
<li><a href="/">West Philadelphia</a></li>
<li><a href="/cab">Cab Whistling</a></li>
<li><a href="/throne">Throne Sitting</a></li>
</ul>
</nav>
This time we're using a data attribute (data-hook
) to identify elements. Any changes involving CSS classes will no longer affect the JavaScript, giving us better separation of concerns and sturdier code.
We just need to update the jQuery selectors to use data-hook
instead:
$('[data-hook="nav-menu-toggle"]').on('click', function() {
$('[data-hook="nav-menu"]').toggle();
});
Notice I opted to use data-hook
for the nav
element as well. You don't have to, but I like the insight it provides: anytime you see data-hook
, you know that element is referenced in JavaScript.
Some Syntactic Sugar
I'll admit that the data-hook
selectors aren't the prettiest. Let's fix that by extending jQuery with a custom function:
$.extend({
hook: function(hookName) {
var selector;
if(!hookName || hookName === '*') {
// select all data-hooks
selector = '[data-hook]';
} else {
// select specific data-hook
selector = '[data-hook~="' + hookName + '"]';
}
return $(selector);
}
});
With that in place, we can rewrite the JavaScript:
$.hook('nav-menu-toggle').on('click', function() {
$.hook('nav-menu').toggle();
});
Much better. We can even have a list of space-separated hook names on an element:
<button data-hook="nav-menu-toggle video-pause click-track">Toggle Nav Menu</button>
And find any hook name within:
$.hook('click-track'); // returns the button as expected
We can also select all hook elements on the page:
// both are equivalent
$.hook();
$.hook('*');
Avoid Anonymous Function Expressions
The examples so far have been using an anonymous function expression as the event handler. Let's rewrite the code to use a declared function instead:
function toggleNavMenu() {
$.hook('nav-menu').toggle();
}
$.hook('nav-menu-toggle').on('click', toggleNavMenu);
This makes the line of code that does the event binding much easier to read. The toggleNavMenu
function name conveys intent and is a good example of self-documenting code.
We also gain reusability, since other areas of code can use toggleNavMenu
as well, should the need arise.
Lastly, this is a big win for automated testing, since declared functions are much easier to unit test than anonymous function expressions.
Working with Multiple Events
jQuery offers convenient syntax for handling multiple events. For example, you can specify a space-separated list of events to be handled by a single event handler:
$.hook('nav-menu-toggle').on('click keydown mouseenter', trackAction);
If you need to handle multiple events with different event handlers, you can use object notation:
$.hook('nav-menu-toggle').on({
'click': trackClick,
'keydown': trackKeyDown,
'mouseenter': trackMouseEnter
});
On the flip side, you can also unbind multiple events at the same time:
// unbinds keydown and mouseenter
$.hook('nav-menu-toggle').off('keydown mouseenter');
// nuclear option: unbinds everything
$.hook('nav-menu-toggle').off();
As you can imagine, careless unbinding has the potential for tremendous unwanted side effects. Read on for techniques to mitigate this.
Unbinding with Care
It's not unusual to bind multiple event handlers for the same event on an element. Let's revisit that button from earlier:
<button data-hook="nav-menu-toggle video-pause click-track">Toggle Nav Menu</button>
Different areas of code could have a stake in what happens when the button is clicked:
// somewhere in the nav code
$.hook('nav-menu-toggle').on('click', toggleNavMenu);
// somewhere in the video playback code
$.hook('video-pause').on('click', pauseCarltonDanceVideo);
// somewhere in the analytics code
$.hook('click-track').on('click', trackClick);
Regardless of the different selectors used, the button now has three click event handlers on it. Now imagine our analytics code is done caring about the button:
// no good
$.hook('click-track').off('click');
Oops, that actually removes all click event handlers, not just trackClick
. We should be more discerning and specify the particular event handler to remove:
$.hook('click-track').off('click', trackClick);
Another option is to use namespaces. Any event can be qualified with a namespace while binding or unbinding, giving you finer control:
// binds a click event in the "analytics" namespace
$.hook('click-track').on('click.analytics', trackClick);
// unbinds only click events in the "analytics" namespace
$.hook('click-track').off('click.analytics');
You can even use multiple namespaces:
// binds a click event in both the "analytics" and "usability" namespaces
$.hook('click-track').on('click.analytics.usability', trackClick);
// unbinds any events in either the "analytics" OR "usability" namespaces
$.hook('click-track').off('.usability .analytics');
// unbinds any events in both the "analytics" AND "usability" namespaces
$.hook('click-track').off('.usability.analytics');
Note that the order of the namespaces doesn't matter. Namespaces are not hierarchial.
If you have complex functionality that requires binding various events across multiple elements, then namespaces are an easy way to group them together for quick clean-up:
// free all elements on the page of any "analytics" event handling
$('*').off('.analytics');
Namespaces are particularly useful when writing plug-ins, since you can ensure your plug-in is a good citizen that only unbinds its own event handlers.
Parting Words
jQuery event binding is great because it's simple to start, but packs plenty of functionality when you need it. Hopefully I've shared a trick or two that helps you write event-driven JavaScript that is sturdier, clearer, and more manageable.
Thanks for reading!