Event Delegation

Bubbling and capturing allows implementing an extremely powerful event handling pattern, known as event delegation.

The concept of event delegation is fairly simple, although, it can seem complicated at first sight.

The main idea is the following: while having multiple elements handled similarly, you don’t have to assign a handler for each of them. Instead, you can put a single handler on their common ancestor. It’s that simple.

To be more precise, event delegation allows avoiding to add event listeners to particular nodes. But, gives the opportunity of adding it to one parent. The event listener analyzes the bubbled events for finding appropriate child elements.

For a better understanding, let’s imagine a parent UL element, which includes child elements:

<!DOCTYPE html>
<html>
  <head>
  </head>
  <body>
    <ul id="parent-list">
      <li id="post_1">Post 1</li>
      <li id="post_2">Post 2</li>
      <li id="post_3">Post 3</li>
      <li id="post_4">Post 4</li>
      <li id="post_5">Post 5</li>
      <li id="post_6">Post 6</li>
    </ul>
  </body>
</html>

When each of the child elements is clicked, something needs to happen. It is possible to add a separate event listener to every LI element. But if the elements are frequently added or removed, adding or removing event listeners would become a nightmare. The most elegant solution is adding an event listener to the parent UL element. You may wonder, how to know what element is clicked when adding the event listener to the parent. The answer is not complicated: at the moment the event bubbles up to the UL element, you need to check the event object’s target property for gaining a reference to the actually clicked node.

Here is an illustration of the basic delegation:

<!DOCTYPE html>
<html>
  <body>
    <ul id="parent-list">
      <li id="item_1">Item 1</li>
      <li id="item_2">Item 2</li>
      <li id="item_3">Item 3</li>
      <li id="item_4">Item 4</li>
      <li id="item_5">Item 5</li>
      <li id="item_6">Item 6</li>
    </ul>
    <script>
      // Get the element, add a click listener...
      document.getElementById("parent-list").addEventListener("click", function(e) {
      	// e.target is the clicked element!
      	// If it was a list item
      	if(e.target && e.target.nodeName == "LI") {
      		// List item found!  Output the ID!
      		alert("List item " + e.target.id.replace("item_", "") + " was clicked!");
      	}
      });      
    </script>
  </body>
</html>

Begin at adding a click event listener to the parent element. At the moment the event listener is triggered, you should check the element for ensuring it’s the type to react to. If it is not the appropriate one, the event may be ignored.

Let’s consider a case, using parent DIV with a lot of children. In the example below, all you need to care about is an A tag along with the classA CSS class:

let div = document.createElement('div');
div.id = 'myDiv';
document.body.appendChild(div);

var a = document.createElement('a');
var link = document.createTextNode("Click on link");
a.appendChild(link);
a.id = 'a';
a.className = 'classA';
a.title = "Link";
a.href = "#";
div.appendChild(a);

// Get the parent DIV, add click listener...
document.getElementById("myDiv").addEventListener("click", function (e) {
  // e.target was the clicked element
  if (e.target && e.target.matches("a.classA")) {
    alert("Welcome to W3Docs!");
  }
});

You can see, whether the element matches the desired one by applying Element.matches API, as well.

Let’s check out another example.

Imagine you want to make a menu using the following buttons: “Save”, “Load”, “Search”. Also, there is an object with methods save, load, search. The most elegant solution to matching them is again adding a handler for the whole menu and data-action attributes for buttons, like this:

<button data-action="save">Click to Save</button>

The handler will read the attribute and execute the method, as follows:

<!DOCTYPE html>
<html>
  <body>
    <div id="buttonList">
      <button data-action="save">Save</button>
      <button data-action="load">Load</button>
      <button data-action="search">Search</button>
    </div>
    <script>
      class List {
        constructor(buttonElem) {
          this._buttonElem = buttonElem;
          buttonElem.onclick = this.onClick.bind(this); // (*)
        }      
        save() {
          alert('Saving...');
        }
     
        load() {
          alert('Loading...');
        }      
        search() {
          alert('Searching...');
        }
      
        onClick(event) {
          let action = event.target.dataset.action;
          if (action) {
            this[action]();
          }
        };
      }      
      new List(buttonList);
    </script>
  </body>
</html>

It would be best if you noted that this.onClick is linked to this in (*). It is a significant point, as otherwise this inside it would reference the DOM element (elem) and not the menu object. And, this[action] would not be the desired result.

Let’s highlight the main advantages of event delegation:

  • It’s not necessary to write the code for assigning a handler to every button. You just need to make a method, putting it in the markup.
  • The structure of HTML is flexible: it is possible to add or remove buttons at any time.

The “behavior” Pattern

Event delegation can also be used to add “behaviors” to elements declaratively, using specific attributes and classes.

The pattern consists of two parts:

  1. Adding a custom attribute to an element, which describes its behavior.
  2. A document-wide handler can track events: if an event occurs on an attributed element- it acts.

Behavior: Counter

Let’s check out an example where the data-counter attribute adds a behavior:

<!DOCTYPE html>
<html>
  <body>
    <!--Counter-->
    <input type="button" value="1" data-counter>
    <!--One more counter:-->
    <input type="button" value="2" data-counter>
    <script>
      document.addEventListener('click', function(event) {
      
        if (event.target.dataset.counter != undefined) { // if the attribute exists...
          event.target.value++;
        }      
      });
    </script>
  </body>
</html>

And the same thing in javascript:

// create  first input tag
let input1 = document.createElement('input');
input1.id = 'input1';
input1.setAttribute('type', 'button');
input1.setAttribute('value', '1');
input1.setAttribute('data-counter', '');
document.body.appendChild(input1);
 
// create second input tag
let input2 = document.createElement('input');
input2.id = 'input1';
input2.setAttribute('type', 'button');
input2.setAttribute('value', '2');
input2.setAttribute('data-counter', '');
document.body.appendChild(input2);
 
document.addEventListener('click', function (event) {
  if (event.target.dataset.counter != undefined) { // if the attribute exists...
    event.target.value++;
  }
});

In the case above, clicking a button increases its value but not the buttons. You can use as many attributes with data-counter as you like. You have the option of adding new ones to HTML at any time. Applying the event delegation, “extends” HTML.

When you assign an event handler to the document object, you are recommended to use addEventListener, not document.on<event> always. That is because the latter may cause conflicts ( for example, new handlers can overwrite old ones).

Behavior: Toggler

In this part, we are going to look through another example of behavior. A click on an element with the data-toggle-id attribute may show or hide the element with the given id, as follows:

<!DOCTYPE html>
<html>
  <body>
    <button data-toggle-id="mail-id">
    Show the form
    </button>
    <form id="mail-id" hidden>
      Your mail: <input type="email">
    </form>
    <script>
      document.addEventListener('click', function(event) {
        let id = event.target.dataset.toggleId;
        if (!id) return;      
        let elem = document.getElementById(id);      
        elem.hidden = !elem.hidden;
      });
    </script>
  </body>
</html>

So, now there is no need to know JavaScript to add toggling functionality to an element. Simply, you can use the data-toggle-id attribute.

Summary

Event delegation is super helpful: it’s one of the handiest patterns for DOM elements.

Most of the time, it is used for adding the same handling for multiple similar elements.

The algorithm of event delegation is the following:

  1. the first step is putting a single handler on the container.
  2. the next step is checking the event.target source element in the handler.
  3. then, if the event happened inside the desired element, you can handle the event.

Event delegation has many benefits, such as:

  • It offers simple initialization and saves memory: there is no need to add multiple handlers.
  • It allows writing less code.
  • And, of course, DOM modifications. You can mass add or remove elements using innerHTML.

Unfortunately, there are some limitations within event delegation:

  1. First of all, the event has to be bubbling. But, some events can’t bubble. Besides, low-level handlers shouldn’t apply event.stopPropagation().
  2. Secondly, the delegation might add CPU load, as the container-level handler reacts to events anywhere in the container.



Do you find this helpful?

Related articles