If you have any thoughts on my blog or articles and you want to let me know, you can either post a comment below(public) or tell me via this i_kkkp@163.com

Browser Event Delegation

Preface

Capturing and bubbling allow us to implement one of the most powerful event handling patterns, namely the Event Delegation pattern.

The idea is that if we have many elements being handled in a similar way, we don’t need to assign a handler for each element — instead, we place a single handler on their common ancestor.

In the handler, we access event.target to see where the event actually occurred and handle it accordingly.

Introduction to Event Delegation

<!DOCTYPE HTML>
<html>

<body>
  <link type="text/css" rel="stylesheet" href="bagua.css">

  <table id="bagua-table">
    <tr>
      <th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
    </tr>
    <tr>
      <td class="nw"><strong>Northwest</strong>
        <br>Metal
        <br>Silver
        <br>Elders
      </td>
      <td class="n"><strong>North</strong>
        <br>Water
        <br>Blue
        <br>Change
      </td>
      <td class="ne"><strong>Northeast</strong>
        <br>Earth
        <br>Yellow
        <br>Direction
      </td>
    </tr>
    <tr>
      <td class="w"><strong>West</strong>
        <br>Metal
        <br>Gold
        <br>Youth
      </td>
      <td class="c"><strong>Center</strong>
        <br>All
        <br>Purple
        <br>Harmony
      </td>
      <td class="e"><strong>East</strong>
        <br>Wood
        <br>Blue
        <br>Future
      </td>
    </tr>
    <tr>
      <td class="sw"><strong>Southwest</strong>
        <br>Earth
        <br>Brown
        <br>Tranquility
      </td>
      <td class="s"><strong>South</strong>
        <br>Fire
        <br>Orange
        <br>Fame
      </td>
      <td class="se"><strong>Southeast</strong>
        <br>Wood
        <br>Green
        <br>Romance
      </td>
    </tr>
  </table>

  <script>
    let table = document.getElementById('bagua-table');
    let selectedTd;

    table.onclick = function(event) {
      let target = event.target;

      while (target != this) {
        if (target.tagName == 'TD') {
          highlight(target);
          return;
        }
        target = target.parentNode;
      }
    }

    function highlight(node) {
      if (selectedTd) {
        selectedTd.classList.remove('highlight');
      }
      selectedTd = node;
      selectedTd.classList.add('highlight');
    }
  </script>

  <style>
    #bagua-table th {
      text-align: center;
      font-weight: bold;
    }

    #bagua-table td {
      width: 150px;
      white-space: nowrap;
      text-align: center;
      vertical-align: bottom;
      padding-top: 5px;
      padding-bottom: 12px;
    }

    #bagua-table .nw {
      background: #999;
    }

    #bagua-table .n {
      background: #03f;
      color: #fff;
    }

    #bagua-table .ne {
      background: #ff6;
    }

    #bagua-table .w {
      background: #ff0;
    }

    #bagua-table .c {
      background: #60c;
      color: #fff;
    }

    #bagua-table .e {
      background: #09f;
      color: #fff;
    }

    #bagua-table .sw {
      background: #963;
      color: #fff;
    }

    #bagua-table .s {
      background: #f60;
      color: #fff;
    }

    #bagua-table .se {
      background: #0c3;
      color: #fff;
    }

    #bagua-table .highlight {
      background: red;
    }
  </style>
</body>

</html>

Dynamic Event Handling with Event Delegation

This table has 9 cells, but it could have 99 or 9999 cells, and it wouldn’t matter. Our task is to highlight the clicked <td> when the user clicks on it.

Instead of assigning an onclick handler to each <td> (potentially many), we can set a “catch-all” handler on the <table> element. It uses event.target to determine the clicked element and highlights it.

The JavaScript code is as follows:

let selectedTd;

table.onclick = function(event) {
  let target = event.target; // Where was the click?

  if (target.tagName != 'TD') return; // Not on TD? We don't care then

  highlight(target); // Highlight it
};

function highlight(td) {
  if (selectedTd) {
    selectedTd.classList.remove('highlight');
  }
  selectedTd = td;
  selectedTd.classList.add('highlight'); // Highlight the new td
}

This code doesn’t care how many cells are in the table. We can dynamically add/remove <td>, and highlighting still works.

However, there is a flaw. Clicks might not happen on the <td> itself but inside it.

In our example, if we inspect the HTML internals, we can see nested tags within <td>, such as <strong>:

In the table.onclick handler, we should accept such event.target and determine if the click occurred inside a <td>.

Here is the improved code:

table.onclick = function(event) {
  let td = event.target.closest('td'); // (1)

  if (!td) return; // (2)

  if (!table.contains(td)) return; // (3)

  highlight(td); // (4)
};

Explanation:

  • elem.closest(selector) method returns the closest ancestor that matches the selector. In our case, we look for <td> starting from the target element.

  • If event.target is not within any <td>, the call returns immediately, as there’s nothing to do here.

  • For nested tables, event.target might be a <td> but outside the current table. So, we check if it’s a <td> within our table. If yes, we highlight it.

Delegation Example: Actions in Markup

Event delegation has other uses, like handling actions in markup.

For instance, let’s say we want to create a menu with buttons for “Save,” “Load,” and “Search.” There’s an object with methods like save, load, and search. How do we match them?

The initial idea might be to assign a separate handler for each button. But there’s a more elegant solution. We can add a handler to the entire menu and add a data-action attribute to buttons with method calls:

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

The handler reads the attribute and executes the corresponding method. The working example is as follows:

<div id="menu">
  <button data-action="save">Save</button>
  <button data-action="load">Load</button>
  <button data-action="search">Search</button>
</div>

<script>
  class Menu {
    constructor(elem) {
      this._elem = elem;
      elem.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 Menu(menu);
</script>
<body>
  <div id="menu">
    <button data-action="save">Save</button>
    <button data-action="load">Load</button>
    <button data-action="search">Search</button>
  </div>

  <script>
    class Menu {
      constructor(elem) {
        this._elem = elem;
        elem.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 Menu(menu);
  </script>
</body>

Please note that this.onClick is bound to this in (*) line. This is crucial because otherwise, the inner this would refer to the DOM element (elem) instead of the Menu object, and this[action] wouldn’t be what we need.

So, what benefits does delegation bring us here?

  • We don’t need to write code to assign a handler for each button. Just create a method and place it in the markup.
  • The HTML structure is very flexible, and we can add/remove buttons at any time.

We could also use classes like .action-save, .action-load, but the data-action attribute is more semantically meaningful. We can also use it in CSS rules.

“Behavior” Pattern

We can also use event delegation to add “behavior” in a declarative way to elements with specific attributes and classes.

The behavior pattern consists of two parts:

  • We add custom attributes to elements describing their behavior.
  • A document-wide handler tracks events and executes the behavior if the event occurs on an element with a specific attribute.

Behavior: Counter

For example, the data-counter attribute here adds a “click to increase” behavior to buttons.

<script>
  document.addEventListener('click', function(event) {

    if (event.target.dataset.counter != undefined) { // If this attribute exists...
      event.target.value++;
    }

  });
</script>
Counter: One more counter:

Summary

Event delegation is really cool! It’s one of the most useful patterns for DOM events.

It’s commonly used to add the same handling for many similar elements, but not limited to that.

Algorithm

  • Place a handler on the container.
  • In the handler — check the source element event.target.
  • If the event happens within an element of interest, handle it.

Benefits

  • Simplifies initialization and saves memory: No need to add many handlers.
  • Less code: When adding or removing elements, no need to add/remove handlers.
  • DOM modifications: We can use innerHTML, etc., to add/remove elements in batches.

Event Delegation Limitations

Firstly, events must bubble. Some events do not bubble. Also, low-level handlers should not use event.stopPropagation().
Secondly, delegation might increase CPU load as the container-level handler responds to events anywhere in the container, regardless of whether we are interested in that event. However, the load is usually negligible, so we don’t consider it.

Browser Default Behaviors Bubbling and Capturing

Comments