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();
});
--ADVERTISEMENT--

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!

Replies

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

  2. 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.

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

  4. zackw says:

    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.

13 more replies