A droplet of water rebounding from rippled water

As web pages become more complex and user interfaces deeper, trying to track events with individual event listeners presents a very real problem of scale, as each added button demands more coding. Thankfully, JavaScript has a built-in event system that, with a little bit of planning, can make development far easier.

If you have just a single button on a page, it makes sense to control it with a single eventListener:

<button id="pop">Pop</button>

And later in the page, a script:

document.getElementById("pop").addEventListener("click", function() {
    // do something
}

This works, but as more elements are added to the page, they get harder and harder to track:

<button id="snap">Snap</button>
<button id="crackle">Crackle</button>
<button id="pop">Pop</button>

But JavaScript has an interesting rule: after an event is triggered on an element, the event cascades upwards through elements surrounding it, in nesting order.

That is, if we surround our button elements with an element, say a <div>:

<div id="buttonPanel">
    <button id="snap">Snap</button>
    <button id="crackle">Crackle</button>
    <button id="pop">Pop</button>
</div>

A click on a button not only registers on each element, but will also be propagated outwards and upwards into the element surrounding it (#buttonPanel, in this case), and then further, ultimately all the way to the root html element and the window object. This is referred to as event propogation, or events bubbling upwards.

Perhaps a good way to think of it is to imagine that you are standing on the edge of a small pond into which someone is throwing pebbles. We could try to track the activity of every stone, which would take a lot of work and attention. Alternatively, we could notice when the ripples from each stone reach our position on the shore. By tracing the ripples back to their source, we can determine where and when a stone was thrown.

This stone, or the originating element - the element on which the event happened - is referred to as the target, and what happened to it - dropping into the pond - as an event.

In the example above, it makes sense to intercept the events happening to any of the buttons at the level of the <div> that surrounds them: a technique known as event delegation.

To determine which button was clicked, we pass the event (commonly referenced as the variable e) to a function, and look at its target. For the example above:

var buttonPanel = document.getElementById("buttonPanel");
buttonPanel.addEventListener("click", function(e) {
    console.log(e.target.id);
})

Clicking on the buttons in order would produce the following in the console:


> snap
> crackle
> pop

You’ll find that if you click to the right of the last button, you get the id of the <div> itself, since that’s what was clicked on. To concentrate on just the buttons, we can check the nodeName of the clicked element in an if statement:

var buttonPanel = document.getElementById("buttonPanel");
buttonPanel.addEventListener("click", function(e) {
    if (e.target.nodeName == "BUTTON") {
        console.log(e.target.id);
    }
})

This will protect the script from acting on any other clicks inside the <div>, other than those on button elements.

Conclusion

Intercepting event bubbling makes for far more efficient and versatile scripts, but it does require a little bit of altered thinking. The best way to learn to do so is by applying it in practical work, which I’ll demonstrate in the next article.

Photograph by Tad Zapasnik, licensed under Creative Commons

Enjoy this piece? I invite you to follow me at twitter.com/dudleystorey to learn more.