Effective Event Binding with jQuery

Originally published at: http://www.sitepoint.com/effective-event-binding-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();
});

Continue reading this article on SitePoint

2 Likes

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

thx… but attribute selectors are very slow:
http://jsperf.com/class-vs-data-attribute-selector-performance

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

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.

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.

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

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

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

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.

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

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

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:

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

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

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.

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

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.

Hence, the matter is subjective

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.

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

This topic was automatically closed 91 days after the last reply. New replies are no longer allowed.