JavaScript
Article

Effective Event Binding with jQuery

By Will Boyd

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!

Meet the author
Will's passion for web development started with GeoCities and hasn't stopped since. He currently works as an interactive developer in downtown Seattle. He also crafts front-end demos and blogs about web development.
Comments
brseixas

Great tips wink
This is very helpful when teaching fresh devs and even older devs.

yashilanka

really helpful article. thanks... i think this is much better way to handle jquery events.

lonekorean

The link you provided is out of date. Here is the latest version:
http://jsperf.com/class-vs-data-attribute-selector-performance/21

To your point, attribute selectors are relatively slower, but I wouldn't say slow. Looking at the benchmarks, they can still complete thousands if not tens of thousands of times per second.

Selector execution is rarely the bottleneck of a web app, since they typically only run once for setup or upon some user action. Unless you're spamming selectors non-stop (not a good idea) then the extra milliseconds won't amount to much.

Paul_Wilkins

Yes, attribute selectors are slow, but speed for those is only relevant when they account for more than a few percent of your processing time, which only occurs in very rare situations.

To put it another way, much larger performance gains are to be found elsewhere than in the selectors. The importance of using the attribute selector is to help protect from CSS changes, and to make the code easier for developers to understand.

zackw

What about jQuery's own data() function? http://api.jquery.com/data/

How does that work with event binding? Or does it?

lonekorean

jQuery's .data() allows you to read an element's data attribute values. It doesn't work as a selector though.

zackw

I thought that was likely the case. It's just that selecting elements via the attribute is pretty ugly:

$('[data-hook="nav-menu-toggle"]')

I thought maybe there was a better way by using data attributes but I guess not.

Paul_Wilkins

That's what prompted making a minor extension to jQuery, so that we can use the following hook technique instead:

$.hook('nav-menu-toggle')
lonekorean

Yep, as @Paul_Wilkins said, the $.hook function makes it not so ugly.

Side note, I also considered a jQuery selector extension that would let you write code like this:

$(':hook(nav-menu-toggle)')

It was nice because you could mix it into selectors, making it much more versatile. Sadly, performance was attrocious. About 100x slower in my tests. Here's the code, if you're curious:

$.extend($.expr[':'], {
	hook: function(el, index, meta) {
		return el.getAttribute('data-hook') && $(el).is('[data-hook~="' + meta[3] + '"]');
	}
});

If someone can find a way to make the performance not suck, I'll buy them a beer. smile

Jay_Hubert_Tauron

If anyone wants it smaller, I had a go at minifying it smile

$.extend({
    hook: function(h) {
        return !h||h==='*'?$('[data-hook]'):$('[data-hook~="'+h+'"]');
    }
});
Paul_Wilkins

I don't know about you, but I much rather prefer to have the code be more easily understandable at a glance.

$.extend({
    hook: function(hookName) {
        if (!hookName) {
            return;
        }
        if (hookName === '*') {
            return $('[data-hook]'):
        }
        return $('[data-hook~="' + hookName + '"]');
    }
});

Some may consider it a small thing, but the above code can be understood more quickly and easily.

Jay_Hubert_Tauron

Indeed, for readability I would have it like

$.extend({
    hook: function(h) {
        return (!h || h==='*') ? $('[data-hook]') : $('[data-hook~="'+h+'"]');
    }
});

but the matter is subjective and I prefer it like this, I find it quicker than reading a few lines because it takes longer to physically read
also, this plugin could just be put in a js file and included in a document when you want it

Mittineague

I think a lot depends on highlighting helping that to be readable.
I'm familiar with ternary so I can "read" it easily enough, but I see potential for trouble in that format.
eg. without highlighting it's not easy to tell at a quick glance whether or not the "+"s are concatenating or part of a math operation. That slows down the reading.
A variable named "h" might be easy enough to know what it is as long as my memory holds, but for someone else, or me later on, will it be easy to see that it represents
hookName
as opposed to
html
heading
or any number of other "h"s. That slows down the reading.

To me, potentially sacrificing readability to squeeze out some whitespace and some characters isn't worth the risk vs. any benefit. At least not for "in development" code.

Jay_Hubert_Tauron

Hence, the matter is subjective

lonekorean

This works, too:

$.extend({hook:function(h){return $('[data-hook'+(!h||h=='*'?'':'~="'+h+'"')+']');}});

But yeah, that is completely awful to read. When actually developing I prefer ternary ops, but I wrote it long form with comments for the sake of the article.

If you want self-documenting code, single character variables are not helpful. I think the only widely understood one is "i" for loops.

zhexiao

Thank you very much. It's helpful for me . That's really a better way to avoid DOM CLASS Bind.

Recommended

Learn Coding Online
Learn Web Development

Start learning web development and design for free with SitePoint Premium!

Get the lastest in JavaScript, once a week, for free.