Jump to Table of Contents

Intro to Event Delegation

Understanding the problem

Consider the following Todo List widget:

Todo List
  1. Read YUI documentation
  2. Build awesome web app
  3. Profit!

All tasks are given a "remove" button. When new tasks are added, they should get a remove button that removes that task. Here's the markup for this:

<fieldset id="todo-example">
<legend>Todo List</legend>
<ol>
    <li><button class="delete-todo">remove</button>Read YUI documentation</li>
    <li><button class="delete-todo">remove</button>Build awesome web app</li>
    <li><button class="delete-todo">remove</button>Profit!</li>
</ol>
<input id="todo"> <button id="add-todo" type="button">add</button>
</fieldset>

In the old days, you would have four click subscriptions:

  1. The remove button for #1
  2. The remove button for #2
  3. The remove button for #3
  4. The add button for creating new tasks

When a user types in a new task and clicks the add button, a new <li> and corresponding <button> are created, and a fifth click subscription is added, one for the new button. The callback for the remove buttons could be unique for each button, or a generic function that determined which item to remove based on some other info from the event or button.

When a user clicks on one of the remove buttons, the item is removed. The associated click event subscription is left in the system, taking up memory. So to solve this, maybe the event subscription is detached before the item is removed. Now there are four initial subscriptions and additional logic to properly detach subscriptions before items are removed.

Over time, the number of items on the todo list grows, and so the number of subscriptions in the system, and thus memory consumed, grows with it. Additionally, if at some point, the entire list needs to be cleared, that's a lot of subscriptions to detach before it's ok to flush the list's innerHTML.

What is event delegation?

Event delegation is a way to reduce the number of subscriptions used to support this system. In the example case, only two click subscriptions are needed: one for the add button, and one for every remove button click. The second one is the delegated subscription. Here's how to think about it:

The key to event delegation is understanding that a click on a remove button is also a click on

  • the list item that the button is in
  • the list itself
  • the <fieldset> that the list is in
  • etc up to the <body> and finally the document[1]

Instead of subscribing to the button's "click" event, you can subscribe to the list's "click" event[2].

You clicked somewhere, but where?

When you click anywhere on the document, the browser dispatches a click event that is assigned an e.target property corresponding to the element that triggered the event. For example, if you click on "Profit!", the event originated from the <li> with "Profit!" in it, so e.target will be that <li> element[3].

With these two bits of information, we can create a single click subscription to respond to every button click in the Todo list.

function handleClick(e) {
    // look at e.target
}

Y.one('#todo-example ol').on('click', handleClick);

Now since there are no subscriptions tied directly to the individual buttons, we can add new items to the list without needing to add more subscriptions. Similarly, we can remove items or even clear the list's innerHTML without needing to detach subscriptions because there aren't any subscriptions inside the list to clear.

More work in the event subscriber

Since any click inside the list is now triggering the event subscriber, it will be executed for button clicks, but also for clicks on the task item's text (e.g. "Profit!"). To make sure this click happened on a button, we need to inspect e.target to make sure it is a button.

function handleClick(e) {
    if (e.target.get('tagName').toLowerCase() === 'button') {
        // remove the item
    }
}

This can start to get tricky when you're triggering on an element that can contain children. For example, if there were no buttons, but instead you wanted to remove items just by clicking on the <li>, you'd need to check if e.target was an <li>. But if it's not, you have to look at e.target's parentNode and potentially that node's parentNode and so on, because e.target will always refer to the most specific element that received the click. This can amount to a lot of filtering code wrapping the item removal logic, which hinders the readability of your app.

Let node.delegate(...) do the work for you

This is where node.delegate(...) comes in. node.delegate(...) boils down the filtering logic to a css selector, passed as the third argument. The subscribed callback will only execute if the event originated from an element that matches (or is contained in an element that matches) this css selector. This allows the code to power our Todo widget to look like this:

YUI().use('node-event-delegate', 'event-key', function (Y) {
    var todoList = Y.one('#todo-example ol'),
        newTask = Y.one('#todo');
    
    // clicks inside the todo list on a <button> element will cause the
    // button's containing <li> to be removed
    todoList.delegate('click', function () {
        this.ancestor('li').remove();
    }, 'button');

    // Adding a new task is only appending a list item
    function addTodo() {
        todoList.append(
            '<li><button class="delete-todo">remove</button>' +
                newTask.get('value') +
            '</li>');

        newTask.set('value', '');
    }

    Y.one('#add-todo').on('click', addTodo);
    newTask.on('key', addTodo, 'enter'); // enter also adds todo (see event-key)
});

Footnotes

  1. If there are click subscriptions at multiple points in the DOM heirarchy, they will be executed in order from most specific (the button) to least specific (document) unless e.stopPropagation() is called along the line. This will prevent subscriptions from elements higher up the parent axis from executing.
  2. We're using the "click" event here, but this all applies to other events as well.
  3. Actually the event originated from the text node inside the <li>, but IE reports the origin (the srcElement in IE) as the <li>, which is probably what developers want, anyway. YUI fixes e.target to bet the element for browsers that report it as the text node.