Tuesday, March 11, 2014

On .on(), or Event Delegation Explained

Just as I'm quite sure there are straightforward concepts unfamiliar to me in technologies and architectures with which I have little or no experience, so too are there such concepts which I take for granted each day but which befuddle otherwise capable developers who simply haven't had exposure to them. (Of course, this one also befuddles developers who really should understand it by now, but I digress.) So perhaps I can "do the universe a solid" and attempt to share that which I have and, for the most part, have been oblivious to the fact that there are others who do not have it.

In this case, I'm speaking of jQuery's .on() function. We've all seen the recommendations to use that, though the more I see such recommendations on, say, Stack Overflow answers the more I discover that there's no shortage of developers who treat .on() as a magic spell. An incantation which, when invoked, solves their problem. Or at least should, and when it doesn't they are again confused.

Allow me to start by stating a couple of unshakable truths regarding this sorcery:
  1. .on() is not magic.
  2. Event handlers are attached to elements, not to selectors.
  3. When an event handler is attached to an element, the selector is evaluated once at that time and never again.
Some of that sounded pretty matter-of-fact as far as blanket statements go, and indeed perhaps I could wordsmith it better. But I wanted to sound that way, because I want to get your attention. You may point out that .on() uses event delegation which clearly means the selector is evaluated again at another time. However, if you know what you're talking about then you know that's kind of a misleading statement because you're referring to a different selector. And if you don't know what I meant by that statement then this article is for you.

First, let's consider the following code:

<button class="editButton">Edit</button>
<script type="text/javascript">
$('.editButton').click(function () {
  // handle the button click event
});
</script>

A very simple and straightforward jQuery event handler. When the button is clicked, the handler code runs. In the highly dynamic world of AJAX and dynamic DOM structures, however, this straightforward approach doesn't always work. A developer then asks, "Why doesn't my click event handler execute for edit buttons that are added to the page via AJAX?" A very reasonable novice question, often met with an equally novice answer... "Just use .on() instead."

This might lead the developer to try this approach:

<button class="editButton">Edit</button>
<script type="text/javascript">
$('.editButton').on('click', function () {
  // handle the button click event
});
</script>

Now the developer is using .on(), but he didn't solve the problem. The magic spell didn't work. So the question is re-asked and further clarification is provided, again in the form of an incantation, "Use 'document' and add a second selector." This exchange leads the developer here:

<button class="editButton">Edit</button>
<script type="text/javascript">
$(document).on('click', '.editButton', function () {
  // handle the button click event
});
</script>

This "works" in the strictest definition of the word. (That is, when comparing "works" with "doesn't work" as an equally vague description of events.) It solves the problem of giving a man a fish so that he may eat today. And in a world where quarterly earnings are valued above long-term sustainability that may be enough. But as developers we don't necessarily live in that world. We have to stomach it from time to time, but our world is about long-term sustainability. The code we write needs to be understandable by whoever has to support it, including ourselves. That is, the developer needs to understand why this incantation "works."

Let's switch gears for a moment and discuss the DOM and events therein. We're all familiar with the fact that when you click something, it raises a click event. (There are plenty of other events as well, but for the purpose of this discussion we'll just focus on the most common of them... click.) Some of us may also be familiar with the fact that events "bubble up" in the DOM, which is at the heart of how .on() performs its "magic." Consider an HTML structure:

<body>
  <div>
    <button class="editButton">Edit</button>
  </div>
</body>

When you click on the button, the button invokes its click event and any handlers for that event attached to that element are thus invoked. But then the event "bubbles up" the structure. After the button invokes it event, the div invokes its click event, invoking any handlers attached to the div. Then the body invokes its click event for handlers attached to it. The top-level html tag also then invokes its click event, and the document object at the very top of the DOM invokes its click event.

That's a lot of click events for a single click. So how does this relate to the "working" incantation above? In that code, look at the element to which we're actually binding the click event:

<button class="editButton">Edit</button>
<script type="text/javascript">
$(document).on('click', '.editButton', function () {
  // handle the button click event
});
</script>

We're binding it to the document. Let's assume for a moment that the HTML being dynamically changed via AJAX calls is the div and the button. This means we could also have bound to the body or the html (though I don't think I've ever seen that latter one in the wild) and it would still "work." This is because any element in the DOM, when raising this event, propagates the event upwards through the structure of the DOM. (Unless, of course, propagation is explicitly stopped.)

Indeed, every click anywhere in the DOM is going to invoke the click event handler(s) on the document object. This is where that other selector in .on() comes in. It's a filter for the event propagation, indicating to jQuery that this particular function should only be invoked when the originating element matches that selector. That selector is evaluated when the event is invoked, whereas the selector for assigning the event was evaluated only once when the page was loaded.

Had we done this, our event handler would execute for any click anywhere on the page:

<button class="editButton">Edit</button>
<script type="text/javascript">
$(document).on('click', function () {
  // handle the button click event
});
</script>

This is also why the developer's first attempt to use .on() "worked" in the sense that his initially loaded button(s) still invoked the handler, because it still attached that handler to the selected elements. But as new elements are added to the DOM, those elements have no handlers attached to them and so it "didn't work" for them.

So basically, the event originates at the clicked element (in this case a button) and travels upward through the HTML elements all the way to the top. That event can be "handled" anywhere along the way. The sorcery of .on() then is simply that it's handled further up the chain instead of at the element itself. This allows us to dynamically alter elements within the document without losing the handler, because even though newly added elements don't have their own handlers assigned they do still "bubble up" their events to parent elements.

It's common to attach these handlers to the document object, though any common parent element will "work." Let's say you have a containing div and within that div you add/remove elements. As long as the div element isn't removed then you can keep your event handler there. (For a large and complex page it's probably best not to put all handlers on the document object, if for no other reason than saner organization of code.) Any common parent over the dynamic elements will "work."

It's not magic, and it's important to understand which parts of the code are evaluated at what time. The selector which targets the element(s) on which we're assigning a handler is evaluated once and never again. The selector which filters the child element(s) which originated the event is evaluated each time the event occurs.

No comments:

Post a Comment