Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

Event bubbling and delegation

When a page has a hundred buttons, attaching a handler to each one is tedious, and for buttons added later it does not work at all. A property of events helps here: they travel up from an element to its parents. A technique called delegation is built on this.

Bubbling

An event first fires on the element where it happened, and then on each of its parents in turn — all the way up to document. This is called bubbling. Click the button:

<div id="box">
  <button>Click</button>
</div>
 
<script>
  document.querySelector('#box').addEventListener('click', () => {
    console.log('The click reached the div');
  });
</script>

The handler is on the div, not on the button, yet the click on the button bubbled up to it.

target and currentTarget

In a handler on a parent it is important to tell apart two event properties: event.target is the element that was clicked, and event.currentTarget is the element the handler is attached to:

<div id="box">
  <button>Button</button>
</div>
 
<script>
  document.querySelector('#box').addEventListener('click', (event) => {
    console.log(event.target.tagName); // BUTTON
    console.log(event.currentTarget.tagName); // DIV
  });
</script>

Delegation

Delegation means one handler on a common parent instead of handlers on every element. Through event.target it finds out where exactly the click was:

<ul id="menu">
  <li>Home</li>
  <li>Catalogue</li>
  <li>Contacts</li>
</ul>
 
<script>
  document.querySelector('#menu').addEventListener('click', (event) => {
    if (event.target.tagName === 'LI') {
      console.log(`Selected item: ${event.target.textContent}`);
    }
  });
</script>

New elements

The main advantage: delegation also works for elements that appear later. The handler is on the list, so nothing has to be attached to new items:

<button id="add">Add a task</button>
<ul id="tasks"></ul>
 
<script>
  const tasks = document.querySelector('#tasks');
  let count = 0;
 
  document.querySelector('#add').addEventListener('click', () => {
    count++;
    const item = document.createElement('li');
    item.textContent = `Task ${count}`;
    tasks.append(item);
  });
 
  tasks.addEventListener('click', (event) => {
    if (event.target.tagName === 'LI') {
      event.target.remove();
    }
  });
</script>

Add a few tasks, then click any of them — it gets removed.

Stopping bubbling

The event.stopPropagation() method stops bubbling: the parents never hear about the event. It is used rarely, when a nested element must behave in its own way:

<div id="card">
  Product card
  <button>Add to favourites</button>
</div>
 
<script>
  document.querySelector('#card').addEventListener('click', () => {
    console.log('Opening the product');
  });
 
  document.querySelector('button').addEventListener('click', (event) => {
    event.stopPropagation();
    console.log('Added to favourites');
  });
</script>

Common mistake

Checking event.target when the button contains other tags. A click on an icon or a highlighted word puts that tag into target, not the button. It is more reliable to find the element you need with closest():

<div id="toolbar">
  <button data-action="save"><b>Save</b></button>
</div>
 
<script>
  document.querySelector('#toolbar').addEventListener('click', (event) => {
    const button = event.target.closest('button');
 
    if (button) {
      console.log(button.dataset.action); // save
    }
  });
</script>

Thanks for reading!

Donate to the site — we work hard for you.