In web development, adding event listeners to elements with JavaScript is one of the most common bits of code a developer will write. While it is a mostly straightforward process to bind events to elements, I’ve found that many developers struggle with what to do when a page is dynamic and elements are not “static”. I’ve stumbled upon many bugs where event listeners were added on page load only to be blown away when elements were added/removed from the page. Let’s say, for example, I’m creating a page to manage a list of users on my site and I would like to add a delete button for each user. On page load, this would be relatively straightforward:

HTML

<button class="userDelete" data-user-id="1">User 1</button>
<button class="userDelete" data-user-id="2">User 2</button>
<button class="userDelete" data-user-id="3">User 3</button>

JavaScript

let userDeleteButtons = document.querySelectorAll(“button.userDelete”);

for(let deleteButton of userDeleteButtons) {
    DeleteButton.addEventListener(“click”, function() {
        //make AJAX call to delete user, remove row in DOM
    });
}

But what if we’d like to add search functionality to this user admin page? No problem! Just take the search criteria, query the database, get the filtered list of users, and update the DOM. However, when the search is complete and we try to click on a delete button for a user, nothing happens! Or, what if we want to add functionality to dynamically add a user by filling out some fields, making an AJAX call to create the user in the database, and plop the HTML for the new user in the DOM?

I’m sure this type of scenario sounds all too familiar. In these types of situations where elements are destroyed and new ones are placed in the DOM, one of the most common solutions I’ve seen is to rebind the event listeners once the DOM is updated. In our user admin page example, it might look something like this:

JavaScript

function InitializeDeleteButtons() {
   //Code to add event listeners to the delete buttons
}

//Initialize the delete buttons on page load
document.addEventListener(“DOMContentLoaded”, InitializeDeleteButtons);

//When search is performed, re-initialize the delete buttons
function search() {
   //Perform the search. DOM will be refreshed.

  InitializeDeleteButtons();
}

This method certainly works. But, from experience, I find that it leads to bugs down the road. Now what if we want to add the functionality to add users via this admin page? When the user is added, we’ll have to remember to add the event listener there as well. And what if a year from now we need to add some functionality to refresh the list whenever we do a bulk import of users? Again, we’d need to remember to re-initialize the event listeners. While this works, it is not very resilient code. While you might remember that you have to re-initialize the event listeners, the next developer that works on this page likely will not.

So what is the alternative? Fortunately, this is rather easy to handle with event delegation. With event delegation, the event listener is added to a parent element on the page rather than the element itself.

HTML

<div id="searchResultsContainer">
    <button class="userDelete" data-user-id="1">User 1</button>
    <button class="userDelete" data-user-id="2">User 2</button>
    <button class="userDelete" data-user-id="3">User 3</button>
</div>

JavaScript

//This element will stay fixed
let container = document.getElementById(“searchResultsContainer”);
container.addEventListener(“click”, function() {
    
    //Check to see if the Event Target was one of the user delete buttons
    if(e.target && e.target.classList.contains(“userDelete”)) {
        let userId = parseInt(e.target.dataset("userId"));
        //make AJAX call to delete user, remove row in DOM
    }
});

How does this work? When one of the delete buttons is clicked, any click event listeners bound to the delete button will be fired. In addition, the click event “bubbles up” through the DOM tree and any events bound to parent elements will also fire. When the event listener fires for our parent element, the “event target” will be the delete button that was clicked. In other words, the event that we would have normally assigned to the delete button is “delegated” to the parent element. Hence, “Event Delegation”!

As a side note, event delegation is even easier to implement with jQuery:

$(“#searchResultsContainer”).on(“click”, “.userDelete”, function() {
   let userId = $(this).data(“userId”);
   //make AJAX call to delete user, remove row in DOM
});

Leave a Reply

Your email address will not be published. Required fields are marked *