Introduction
Let’s start with an example. Suppose we have a navigation menu with several links, and we want to implement a smooth scrolling effect when the user clicks on any of the links.
<nav class="nav">
<ul class="nav__links">
<li class="nav__item">
<a class="nav__link" href="#about">About</a>
</li>
<li class="nav__item">
<a class="nav__link" href="#projects">Projects</a>
</li>
<li class="nav__item">
<a class="nav__link" href="#testimonials">Testimonials</a>
</li>
</ul>
</nav>
A bad approach
A naive approach would be to loop over all the links and add a click event listener to each one of them, like this:
document.querySelectorAll('.nav__link').forEach(function(el) {
el.addEventListener('click', function(e) {
e.preventDefault();
const id = this.getAttribute('href');
document.querySelector(id).scrollIntoView({
behavior: 'smooth'
});
});
});
This works, but it has a problem: we are adding as many event listeners as there are links, which can affect the performance and memory usage of our web page. Imagine if we have hundreds or thousands of links, that would be a lot of unnecessary event listeners!
How can we solve this problem? The answer is event delegation.
A better approach
Event delegation is a technique that allows us to attach a single event listener to a common parent element, and then use the event object to determine which child element triggered the event. This way, we can handle multiple events with a single event listener, and avoid adding too many event listeners to the DOM.
To implement event delegation, we need to do two things:
- Add a single event listener to the common parent element. In our case, that would be the element with the class
nav__links
. - Use the event object to check which child element originated the event. In our case, we only want to handle the events that come from the elements with the class
nav__link
.
Here is how our code would look like with event delegation:
document.querySelector('.nav__links').addEventListener('click', function(e) {
e.preventDefault();
if (!e.target.classList.contains('nav__link')) return
const id = e.target.getAttribute('href');
document.querySelector(id).scrollIntoView({
behavior: 'smooth'
});
});
-
The event can be triggered by any child element or the parent element itself.
-
We are only interested in the events that come from the elements with the class
nav__link
-
Matching strategy: we use the
classList
property of the event target to check if it contains the classnav__link
The matching strategy is the most important part here, make sure to adjust it to your own code.
For example if one of ournav__link
has more that one child, the above matching strategy wont work.<a class="nav__link" href="#section--3"> <span>[Child]</span> Testimonials </a>
In these cases the JavaScript selector
closest()
comes very handy// ... const navLinkEl = e.target.closest('.nav__link'); const id = navLinkEl.getAttribute('href'); // ....
As you can see, we have reduced the number of event listeners from N (the number of links) to 1 (the parent element). This improves the performance and memory usage of our web page, and also makes our code more maintainable and scalable.
Conclusion
Event delegation is a powerful technique that can help us optimize our JavaScript code and handle multiple events with a single event listener. It is based on the concept of event bubbling, which means that an event that occurs on a child element will propagate up to its parent elements until it reaches the document object. By using event delegation, we can take advantage of this behavior and handle events at a higher level in the DOM hierarchy.
I hope you enjoyed this blog post and learned something new about event delegation. If you have any questions or feedback, please leave a comment below. Thank you for reading!