The Art of JavaScript Event Delegation
JavaScript events are the catalysts behind most web functionality. Interacting with literally any website means firing away a ton of events. Have you ever thought about how these events get assigned? Are developers just tirelessly adding event functionality to every single element? What about dynamically created content, how do events get added to that? Well, there are a few different approaches to address these questions.
Nested Event Listeners
One approach is to assign an event listener every single time an element is created.
For our example code we will be creating buttons and adding event listeners to them. Our goal is to change the colors of individual buttons on click. Our starter HTML is just this:
<ul id="button-container"></ul>
In our JavaScript file, we will be selecting the container, adding buttons to it by iterating over an array of colors, and appending each one to the container. This is arbitrary and just intended to mimic creating elements dynamically based on incoming data.
const colors = ["pink", "blue", "green"]const buttonContainer = document.querySelector("#button-container")function createColorfulButtons(colors) {
colors.forEach(function(color) { const colorLi = document.createElement("li")
colorLi.innerHTML = `<button> ${color} </button>`
buttonContainer.append(colorLi)
})
}
Time for events! With this approach, we will be putting the event listener directly inside our forEach
. Inside the listener, we will be toggling a styling class (defined in our CSS file):
function createColorfulButtons(colors) {
colors.forEach(function(color) {
const colorLi = document.createElement("li")
colorLi.innerHTML = `<button> ${color} </button>`
buttonContainer.append(colorLi)
//create an individual event listener for each button!
const colorButton = colorLi.querySelector("button")
colorButton.addEventListener("click", function() {
colorButton.classList.toggle(`${color}`)
})
})
}
Great! So now each of our buttons has an individual event listener that will fire when it’s clicked.
This works as intended and solves our problem (woo!), but what are some potential issues with this technique? Well, what if this was an app with hundreds of dynamically created elements? We would be creating an individual event listener for every… single… one. Not DRY at all. Is there a better way?
Event Delegation
Taking the same example as before, let’s try out an approach called event delegation. Instead of creating individual event handlers for each element we want to target, we will be assigning one event handler to a parent element and specifying conditions for which elements will be impacted by that event.
Our createColorfulButtons()
, HTML, and CSS are exactly the same, but this time let’s add an event listener outside the scope of our forEach
and directly to our buttonContainer
.
buttonContainer.addEventListener("click", function(e){
console.log(e.target)
})
If you try clicking on the different buttons, you’ll see that e.target
refers to the element being clicked on, and that our console.log
will print that specific element.
For example, if we click directly on the button that says “pink”, it will print <button> pink </button>
. If we click on the area outside the button, on on of our li
elements, the following will be printed into the console: <li><button> pink </button></li>
.
So our event knows which element is being clicked, perfect! How can we set a condition that will only target certain elements? In this example, we want to only target buttons, and not the li
elements they are inside of. Let’s try a good old conditional:
buttonContainer.addEventListener("click", function(e){ if(e.target.tagName === "BUTTON") {
console.log(e.target)
}})
Now when you try clicking on anything besides a button, nothing gets printed to the console. We’ve isolated our buttons! You might be wondering about where the .tagName
is coming from. .tagName
is one of many properties that can be used on an element (and therefore on e.target
), check out the complete list here (I touch more on it at the end of this post). Now that we can access only our buttons, let’s add the code we want to run on click:
buttonContainer.addEventListener("click", function(e){
if(e.target.tagName === "BUTTON") {
let buttonText = e.target.innerText;
e.target.classList.toggle(buttonText)
}
})
See the demo below:
Rejoice, we’ve successfully implemented individual events using event delegation!
You can stop reading here, but I want to point out some other useful element properties and methods, besides .tagName
.
.className
: a string representing the class of the element. Another way to isolate a certain group of elements..id
: a string representing the id of the element..closest()
: returns the closest ancestor of the element that matches the passed in selector (for example a class or id). This can be really useful, for example maybe youre.target
is a delete button, and you want to remove the closestli
without removing all theli
s in the process..matches()
: returns boolean based on whether the element matches the passed in selector. We could’ve used this instead of.tagName
in our example!
There’s way more and they’re extremely useful so check out the documentation here.
Some related resources to take a look at: