February 18, 2019
Share this:
JavaScript Beginner

Adding Event Listeners to Dynamic Content with Event Delegation

Have you ever added a button to a page with JavaScript expecting it to function fully, but when you clicked it nothing happened? You've double checked your code - you're creating the event listeners, the selectors are correct, but it's still not working!

Chances are, if you've hit this situation, you're using a piece of code like this:

// Find all the .remove-button, then put a click event listener on each of them
document.querySelectorAll('.remove-button').forEach(function(elem) {
	elem.addEventListener('click', function() {
		this.closest('.todo-item').remove();
	});
});

Ostensibly, this is the correct way of adding an event listener to an element – You're targeting everything with the class .remove-button, and triggering a function that remove the parent item on click – so why doesn't this work for content you've added via JavaScript?

Simply put, this fails because some (or all) of the  .remove-button elements don't exist when document.querySelectorAll is run.

One technique for handling this situation is to add and remove event listeners every time you add or remove content - but if your content is constantly changing, this can be a nightmare to manage.

As it turns out, there is a much easier way of adding event listeners to content that doesn't exist yet - via a technique called event delegation.

Here is what happens if we change the code above to use event delegation:

// Instead of targeting an element, we target a static parent -- I've chosen
// body since it's always there, but in bigger applications it's better to 
// target a more specific parent so that this doesn't get bloated and cause
// performance issues
document.querySelector('body').addEventListener('click', function(event) {
	// event.target is the clicked item
	if (!event.target) { return; }

	// Check if the event.target is a remove button
	if (event.target.matches('.remove-button')) {
		event.target.closest('.todo-item').remove();
	}
});

The concept here is simple: If the content we're trying to add listeners to doesn't exist yet, add a listener to something that does - specifically, a parent element which will let events bubble up.

There is one major additions to make this change work: since the event listener will fire on every click to the parent, and not just the ones we want to respond to, we have to match the event.target (the element that has been clicked) against a selector to make sure that the user has actually clicked the element we want to respond to.

Normally, this pattern would seem backward but this performs exceptionally well with content we're adding later on. Since we are delegating our event response to a parent element, it doesn't matter what was loaded on the page initially - it will run on any .remove-button element regardless of when it was added to the page.

If you're using a library, you should check to see if it has any built-in functionality to do event delegation for you (like jQuery) - chances are they provide additional features you can use, like advanced delegation and element matching.

If you're struggling with making content you're adding later function correctly, try moving the event listener to a parent, and watch event delegation work.

Share this: