Part 4: What is Event Delegation in JavaScript?

This is Part IV of my I Need To Learn More JavaScript series.

Event Delegation

To understand Event Delegation you must first understand event listeners.

I hate defining things by using the words in its name… but having gotten that caveat out of the way, an event listen listener is something that listens for an event.

An event in JavaScript is defined as “things that happen to HTML elements,” and there are a BOAT LOAD of them.

Here are some of the common events:

  1. onchange:: An HTML element has been changed
  2. onclick:: The user clicks an HTML element
  3. onmouseover:: The user moves the mouse over an HTML element
  4. onmouseout:: The user moves the mouse away from an HTML element
  5. onkeydown:: The user pushes a keyboard key
  6. onload:: The browser has finished loading the page

addEventListener()

To add an event listener on an HTML element you use the addEventListener() method.

An Example of the addEventListener() Method
const character = document.getElementById("disney-character");
character.addEventListener('click', showCharactersName);

The first part document.getElementById is the event target — in our case it is an HTML element. But in JavaScript, the event target can be a plethora of things. It can be an HTML element in the document (like our example above), it can be the document itself (i.e. a web page loaded in the browser), or the even window; a top-level object in Client Side JavaScript which encompasses everything. However in most use cases, it is an HTML element. The second part is the actual event listener.

The eventListener above works like this:

When a user clicks the HTML element with the id disney-character the event listener fires and calls the showCharactersName function.

Event listeners are set on page load. So when you first open an website, the browser downloads, reads, and executes the JavaScript.

const character = document.getElementById("disney-character");
character.addEventListener('click', showCharactersName);

In our code above, on page load, the event listener finds an HTML element with the id disney-character and sets a click event listener on that HTML element.

The problem occurs when the element is added to the DOM after the initial page load.

Event Delegation

Event Delegation solves this problem. To understand Event Delegation, we need to look below at our list of Disney Characters.

Thank you Wes Bos for the format!

This list has some basic functionality. For our purposes, you can add characters to the list, and you can check the boxes next to the characters name.

This list is also dynamic. The inputs (Mickey, Minnie, Goofy) were added AFTER the initial page load, and subsequently didn’t have event listeners attached to them.

Let’s take a look at the JavaScript:

const checkBoxes = document.querySelectorAll(‘input’);
checkBoxes.forEach(input => input.addEventListener(‘click’, ()=> alert(‘hi!’)));
//an alert should fire when I click on the inputs (Mickey, Minnie, or Goofy)

But lets’ take a look at the HTML AT PAGE LOAD:

<ul class=”characters”>
</ul>

Now let’s take a look at the HTML AFTER PAGE LOAD (from local storage, API call, etc):

<ul class=”characters”>
<li>
<input type=”checkbox” data-index=”0" id=”item0">
<label for=”item0">Mickey</label>
</li>

<li>
<input type=”checkbox” data-index=”1" id=”item1">
<label for=”item1">Minnie</label>
</li>

<li>
<input type=”checkbox” data-index=”2" id=”item2">
<label for=”item2">Goofy</label>
</li>
</ul>
** The input were placed on the DOM after page load and DID NOT have event listeners bound to them. 

If you try to click on the inputs (the characters — Mickey, Minnie, or Goody), you would expect an alert to pop up that says ‘hi!’, but because they weren’t there at page load, they didn’t get the event listeners bound to them, and subsequently nothing happens.

The alert ‘hi’ does not appear!

So Bret.. how do we fix this problem?

Event Delegation.

The whole idea behind event delegation is that instead of listening for a change on the inputs directly, we should look for an HTML element that is going to be on the page on page load.

In our example — the unordered list with the class name characters is on the page at page load. We can attached the event listener there!

<ul class=”characters”> // PARENT - ALWAYS ON THE PAGE
<li>
<input type=”checkbox” data-index=”0" id=”char0"> //CHILD 1
<label for=”char0">Mickey</label>
</li>

<li>
<input type=”checkbox” data-index=”1" id=”char1"> //CHILD 2
<label for=”char1">Minnie</label>
</li>

<li>
<input type=”checkbox” data-index=”2" id=”char2"> //CHILD 3
<label for=”char2">Goofy</label>
</li>
</ul>

It is best to think of event delegation as responsible parents and negligent children. The parents are basically god, and the children have to listen to whatever the parents say. The beauty is if we add more children (more inputs), the parents stay the same.

Let’s attach the event listener.

<ul class=”characters”>
</ul>
<script>
function toggleDone (event) {
console.log(event);
}
  const characterList = document.querySelector('.characters');
characterList.addEventListener('click', toggleDone);
</script>

So now that we have an event listener set on the unordered list, characters and not the individual children, what happens if we click an input (Mickey, Minnie, or Goofy) after page load and console.log that event.target.

Console.log(event.target)

By running console.log(event.target) — we see the following:

event.target

The event.target is a reference to the object that dispatched the event. Or in other words, it identifies the HTML element on which the event occurred.

The event in our case is the click! The object on which the event occurred is the <input/>.

** A label is considered by of the input object — that is why we see both. **

Console.log(event.currentTarget)

If we console.log(event.currentTarget) — we see something different.

event.currentTarget

The event.currentTarget identifies the current target for the event, as the event traverses the DOM. It always refers to the element to which the event listener has been attached. In our case the event listener was attached to the unordered list, characters, so that is what we see in our console.

Writing Event Delegation in JavaScript

Because we now know that the event.target identifies the HTML elements (multiple in our case) on which the event occurred, and we also know what element we want the user to click (the input element), solving this in JavaScript is relatively easy.

//Event Delegation
function toggleDone (event) {
if (!event.target.matches(‘input’)) return;
console.log(event.target);
//We now have the correct input - we can manipulate the node here
}

Basically the code above states, if the event target returned DOES NOT match the input element, exit the function.

If the event target returned DOES match the input element, console.log the event.target and execute subsequent JavaScript code on that child node.

This is important, now we can be confident that a user clicked the correct child node, even though the inputs were added to the DOM after the initial page load.

Clicking on inputs
Returns the correct inputs — note the video above does not correspond to this console .. but you get the idea!

Event Bubbling

If you want to stop reading here — by all means please do! We just covered the basics of event delegation. But for a deeper understanding of why event delegation works, we need to understand Event Bubbling.

What REALLY happens when you make a click?

Whenever a user makes a click it ripples up all the way up to the top of the DOM and triggers clicks on all the parent elements of the element you clicked. You don’t always see these clicks, because you aren’t always listening (with an event listener) for a click on these elements, but this bubbling up does happen.

This is called event bubbling or event propagation.

Because of its bubbling nature, event propagation basically means that anytime you click one of our inputs on the DOM, you are effectively clicking the entire document body.

Here is an example in action:

<div class=”one”>
<div class=”two”>
<div class=”three”>
</div>
</div>
</div>
<script>
const divs = document.querySelectorAll('div');
  function logClassName(event) {
console.log(this.classList.value);
}
  divs.forEach(div => div.addEventListener('click', logClassName));
</script>

Above we have three divs: DIV #1, DIV #2, DIV #3. Each DIV has it’s own event listener, and when we click on a DIV in browser, we ask it to console.log its class name by executing the function, logClassName().

Thanks Wes Bos again!

Above is what we see in the browser. Notice how my mouse is clicking DIV #3. As one would expect, when I click DIV #3, I expected to see the class name logged in the console (to the right). But when I click DIV #3, I also DIV #2 and DIV #3. This is event bubbling! We see the class name logged, because we added event listeners to each one.

Source: https://javascript.info/ — clicking on DIV #3 bubbles up to 2, and 1.

So back to our example — we had only one event listener, and it was set on our unordered list characters. YET when we clicked a child of that parent HTML element, the HTML element input, it fired the event listener we set that was bound to the unordered list.

Because of event bubbling you can place an event listener on a single parent HTML element that lives above a HTML child, and that event listener will get executed whenever an event occurs on any of its child nodes — even if these node children are added to the page after the initial load!

<ul class=”characters”> // PARENT -- This is where the listener is!
<li>
<input type=”checkbox” data-index=”0" id=”char0"> //CHILD 1
<label for=”char0">Mickey</label>
</li>

<li>
<input type=”checkbox” data-index=”1" id=”char1"> //CHILD 2
<label for=”char1">Minnie</label>
</li>

<li>
<input type=”checkbox” data-index=”2" id=”char2"> //CHILD 3
<label for=”char2">Goofy</label>
</li>
</ul>
<script>
const characterList = document.querySelector('.characters');
characterList.addEventListener('click', toggleDone);
</script>

In Conclusion — Why Use Event Delegation?

Without event delegation you would have to rebind the click event listener to each new input loaded to the page. Coding this is complicated and burdensome. For one, it would drastically increase the amount of event listeners on your page, and more event listeners would increase the total memory footprint of your page. Having a larger memory footprint decreases performance… and poor performance is a bad thing. Second, there can be memory leak issues associated with binding and unbinding event listeners and removing elements from the dom. But that is beyond the scope of this article!

That’s all I have for this round — keep chugging people!