Event delegation

Hey ,

I'm thrilled to help you learn JavaScript. Unfortunately, you've landed on a page where you cannot access with your current purchase.

Please upgrade (use this link) access this content.

I'm super eager to help you learn more!

Event delegation

Let’s say you have a list of items. You want to listen to a click event on each item. One way is to attach an event listener to every item.

<ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <li>Item 4</li>
  <li>Item 5</li>
</ul>
const items = Array.from(document.querySelectorAll('li'))
items.forEach(item => item.addEventListener('click', e => {
  // Do something with item
}))

But what if you had one thousand items? If you do the above, you’ll create one thousand event listeners. That’s not the best way.

A better way is to use the event delegation pattern.

Event delegation pattern

The event delegation pattern makes use of event propagation. It works like this:

  1. you attach one event listener to an ancestor element
  2. ancestor element listens to all events in descendant elements

Note: the event delegation pattern only works for events that bubble.

<ul>
  <li>Item 1</li>
  <li>Item 2</li>
  <li>Item 3</li>
  <li>Item 4</li>
  <li>Item 5</li>
</ul>
const list = document.querySelector('ul')
list.addEventListener('click', e => {
  // Do something when list is clicked on
})

Determining the event target

The element that fires the event is called the event target. It can be found with the target property.

list.addEventListener('click', e => console.log(e.target))
The clicked target can be found with event.target
The clicked target can be found with event.target

Avoid misfires

The event delegation pattern is sensitive to all events fired from the listening element onwards. In this case, you can also fire the callback when you click on the list itself.

Event delegation pattern is sensitive to all events
Event delegation pattern is sensitive to all events

To prevent such misfires from happening, we need to check if the target element matches the element we’re looking for. We can do so with the matches method.

matches checks if the element matches the selector we provided. You should be familiar with it’s syntax.

element.matches(selector)

matches will either return true or false. It will be true if the element matches the selector.

In this case, we only want to do something if a user clicks on a list item. We can check whether the event target is a list item with matches.

list.addEventListener('click', e => {
  if (e.target.matches('li')) {
    // Do something
  }
})

Dealing with nested elements

Let’s say you want to listen to a click event on a <button>. This button has an SVG and some text embedded in it.

<button>
  <svg> <!-- Gear icon --> </svg>
  <span>Click me!</span>
</button>
const button = document.querySelector('button')
button.addEventListener('click', e => {
  console.log(e.target)
})

Watch what happens if you click on the gear icon or the text.

event.target can fire for inner elements if you're not careful
event.target can fire for inner elements if you're not careful

When we click on the gear icon, the SVG shows up in the console. that’s because the event.target (which was the clicked element) is the SVG itself. Ditto for the text.

Most of the time, we’re not looking for the SVG or the text. We’re looking for the button element instead. There are two methods to always ensure we get the button element.

  1. Set pointer-events to none
  2. Use closest

Pointer events

pointer-events is a CSS property that determines how an element respond to mouse events. (click is a mouse event). If you set pointer-events of an element to none, it will not respond to mouse events.

In this case, we can set pointer-events of all descendant elements to none. We can do this with the following CSS:

/* Preventing events from bubbling in CSS */
button * {
  pointer-events: none;
}

Note: I placed this CSS in the reset.css file.

Closest

closest searches the DOM upwards for an element that matches the selector. This search includes the element itself.

element.closest(selector)
  • If it finds an element that matches the selector, it returns the element.
  • If it doesn’t find any elements, it returns undefined

If search for button, we can ensure we work with the button element even though the user clicked on the SVG (or text).

button.addEventListener('click', e => {
  const button = event.target.closest('button')
  if (button) {
    // Activates when user clicked any element within `button`
  }
})

Pointer events or closest?

In practice, you would normally add click events to <button>s. In this case, pointer-events: none is easier to use because it will work for all your buttons

Exercise

Here’s a list of famous people. Do the following:

  1. Create an event listener that uses the event delegation pattern
  2. Log the element if the target matches li
  3. Try using both pointer events and closest to filter the event target
<ul>
  <li><a href="#">Benjamin Franklin</a></li>
  <li><a href="#">Thomas Edison</a></li>
  <li><a href="#">Franklin Roosevelt</a></li>
  <li><a href="#">Napoleon Bonaparte</a></li>
  <li><a href="#">Abraham Lincoln</a></li>
</ul>

Answers

// With `closest`
const list = document.querySelector('ul')
list.addEventListener('click', ev => {
  if (ev.target.closest('li')) {
    console.log(ev.target)
  }
})

With Pointer Events:

/* CSS */
li a {
  pointer-events: none;
}
// JS
const list = document.querySelector('ul')
list.addEventListener('click', ev => {
  if (ev.target.matches('li')) {
    console.log(ev.target)
  }
})