Element 84 Logo

jQuery, .on(), and writing difficult JavaScript

12.21.2011

One of the cooler facilities that the jQuery JavaScript framework offers is the on() method. (If you’ve been working with jQuery for a while, you may know on() by an older version, live(). If you’ve not used on() before, you’re in for a treat… well, at least until you go crazy with it and someone else has to pick up your code base.

Working with the Future

At its simplest, on() lets you attach an event handler to a set of elements matching a selector. So you might see something like this:

$('.dataset').on('click', function(event) {

  var dataset = $(this).closest('.dataset-granules');
  var granules = dataset.find('.granules, .dataTables_wrapper');
  var visible = !granules.is(':visible');
  var search_by_id_button = dataset.find('a.search-by-id');

  if ( search_by_id_button.not('.ui-state-active-durable').size() > 0 ) {
    search_by_id_button.click();
    return;
  } // etc

});

Here, any elements with the class “dataset” is matched by the selector $(‘.dataset’). Then, on() attaches to all of those elements a click handler. No big deal; this is similar to the following code:

$('.dataset').click(function(event) {

  var dataset = $(this).closest('.dataset-granules');
  var granules = dataset.find('.granules, .dataTables_wrapper');
  var visible = !granules.is(':visible');
  var search_by_id_button = dataset.find('a.search-by-id');

  if ( search_by_id_button.not('.ui-state-active-durable').size() > 0 ) {
    search_by_id_button.click();
    return;
  } // etc

});

But on() adds a twist: in addition to matching any elements that have the class “dataset”, it will attach this event handler to any future elements that have that class. So if you create a new element and give it the class “dataset”, that element will immediately (at least in JavaScript terms) get the click event handler defined in on(). On top of that, if you have an existing element and add to its classes the class “dataset”, that element will also get the handler.

In short, on() is a future-aware version of assigning a handler to an element. Anything that goes into your document object model that is matched by the on() selector is assigned a handler. It’s a once-for-all assignment, if you will.

Distance Creates Difficulty

Here’s a controversial statement that will make some of you upset. (That’s good. If you’re not upset or otherwise emotionally engaged, you’re bored, and nobody wants to read a boring blog, right?)

I personally believe that the distance between an element or object and that element or object’s behavior is proportional to the difficulty you’ll have in understanding how that element or object functions.

Honestly, this is somewhat intuitive. It underscores why you put the logic for a given page in a single external JavaScript file, why you tend to group functions together, and why you have some sense of ordering and organization in your code. You typically don’t, for example, create an object, go do 20 other unrelated things, and then finally come back and set a property on that newly-created object. When you group related and ordered functionality, you make that functionality easier to follow, understand, and in general, maintain.

But on() lets you break thoroughly out of this paradigm. You can set up 10 different class-based on() calls, each with a different selector, in a core bit of JavaScript. Then, multiple pages and external JavaScript files away, you dynamically create an element. Boom! Thanks to one (or more!) on() assignments in that original JavaScript, your element magically gets all sorts of behavior.

In a word… magic!

And it can be tough to track this stuff down. First of all, if you don’t know about live() or on(), it often won’t even occur to you what could be happening. Second, even if you are aware, you’ll have to hunt down the on() calls. And since these often use class-based selectors, that’s not always easy. If the selector is a single class, like “.dataset”, it’s no problem. But suppose your selector is “.cart.add-all”. Good luck if your newly-created element has 10 classes, and “cart” and “add-all” are separated by three or four other classes in between!

JavaScript Programmers Against on()?

Distance creates difficulty. It’s not easy to figure out what’s going on. So why do this, then? Is on() the devil of JavaScript? Well, no, of course not.

on() is a really clever, useful way to create contracts in your code. It’s also great when you have multiple created objects of the same type. You really don’t want to have to manually assign them behavior if that behavior is pre-determined. on() is a perfect way to say, “Given an element that matches this selector, it should always behave this way.” And on() has teeth: it actually attaches that behavior, whether you like it or not. (You can turn this off with off(), although that can get a bit messy.)

on() is also great if you have a list of elements and you’re going to be adding, removing, and adding again to that list. You can ensure consistency because all those elements will have the same behavior, regardless of when they’re created. Even better, JavaScript programmers don’t have to remember to attach certain behaviors to new elements. on() gives them the right behavior for free.

ID users, disregard

A short semi-caveat to everything that’s come before. Most of what you’re reading simply doesn’t apply to the use of on() with an ID selector, like ‘#dataset-container’. And when it does apply, it’s not such a good thing, either.

The reason isn’t due to a technical limitation. It’s a function of the fact that you shouldn’t have but one element with a given ID on a given page. on() is at its heart about defining a set of elements that behave in a similar fashion, and while a set of one is still a set in mathematics, a set of one in JavaScript is best directly assigned behavior. Most experienced JavaScript developers have at least at some point created two elements with identical IDs and spent an hour chasing down some really hard-to-isolate buggy behaviors. Don’t do it.

Now, you can argue, sure; you could create an on() definition with an ID so that when a singular element comes along later with a matching ID, you get the behavior you want. You’re planning and coding ahead, right? Well, in a manner of sorts. But you’re also creating a lot of artificial distance between an element (created somewhere dynamically if it doesn’t exist when you define the behavior in on()), and that element’s behavior, in on(). As already mentioned, behavior that attaches to an element absent you writing code can be mysterious, if not downright magical. And magic is cool, but best when someone knows how the trick works.

If you create an element with an ID, that should be the singular creation of that element. So why not assemble that element’s behavior close to its creation? It’s easier to read and maintain. If you define functions elsewhere, and need to call those functions, that’s no problem. You’re still seeing an element’s creation, it’s behavior, and any functions it calls. It’s then trivial to search for that function and see what it does. You’ve still got the function call clearly nestled up next to the element on which it applies.

on() in the Final Analysis

So assuming you get the basic idea of when to use on() and when to not use on(). Is it possible to marry the advantages of contract-like behavior without adding difficulty to the code base? What about the poor schlep who comes into your project and is totally mystified by things just “working,” as if by magic?

It’s a mixed bag. First, on() is not obscure. It’s reasonable to ask JavaScript developers to either come into a project knowing on(), or to take 15 minutes and read up on the function. It’s too common and useful a technique in jQuery to be ignored. So there’s nothing wrong with using or expecting other developers to know what on() does.

Second, realize that it’s easy to use on()–and more specifically, place on() in your code–in such a way that it’s difficult to connect it to the elements for which its intended. You can do some things to mitigate this, of course. If you tend to create elements to which you’ll attach behavior via on() in a particular function, try and place your on() assignment near that function. That small step at least decreases the distance between element creation and behavior assignment. And while you’re at it, remember that just because you know what behavior is assigned via on() doesn’t mean that other developers realize the same thing.

Third, and arguably most important–and therefore, of course, most difficult–keep some basic contract documentation. If you had a simple sheet of paper (digital, of course; we’re not savages here) that listed the selectors you had matched via on(), the files in which those on() assignments occurred, and a note about the assigned behavior, you’d dramatically increase the ease by which a new developer could enter into your JavaScript world. Just hand… err… email them that document and they’ve got a quick cheat sheet of what to expect, when to expect it, and even an easy mechanism for finding and looking at automatically-assigned behavior.

So use on(). Heck, use it a lot. Just realize it’s nowhere close to self-documenting. A little work here goes a long way. Besides, you’re better defining and documenting your system. It’s something we all hate, but also something we all recognize is important.