Event bubbling and delegation
When a page has a hundred buttons, attaching a handler to each one is tedious, and for buttons added later it does not work at all. A property of events helps here: they travel up from an element to its parents. A technique called delegation is built on this.
Bubbling
An event first fires on the element where it happened, and then on each of its parents in turn — all the way up to document. This is called bubbling. Click the button:
<div id="box">
<button>Click</button>
</div>
<script>
document.querySelector('#box').addEventListener('click', () => {
console.log('The click reached the div');
});
</script>The handler is on the div, not on the button, yet the click on the button bubbled up to it.
target and currentTarget
In a handler on a parent it is important to tell apart two event properties: event.target is the element that was clicked, and event.currentTarget is the element the handler is attached to:
<div id="box">
<button>Button</button>
</div>
<script>
document.querySelector('#box').addEventListener('click', (event) => {
console.log(event.target.tagName); // BUTTON
console.log(event.currentTarget.tagName); // DIV
});
</script>Delegation
Delegation means one handler on a common parent instead of handlers on every element. Through event.target it finds out where exactly the click was:
<ul id="menu">
<li>Home</li>
<li>Catalogue</li>
<li>Contacts</li>
</ul>
<script>
document.querySelector('#menu').addEventListener('click', (event) => {
if (event.target.tagName === 'LI') {
console.log(`Selected item: ${event.target.textContent}`);
}
});
</script>New elements
The main advantage: delegation also works for elements that appear later. The handler is on the list, so nothing has to be attached to new items:
<button id="add">Add a task</button>
<ul id="tasks"></ul>
<script>
const tasks = document.querySelector('#tasks');
let count = 0;
document.querySelector('#add').addEventListener('click', () => {
count++;
const item = document.createElement('li');
item.textContent = `Task ${count}`;
tasks.append(item);
});
tasks.addEventListener('click', (event) => {
if (event.target.tagName === 'LI') {
event.target.remove();
}
});
</script>Add a few tasks, then click any of them — it gets removed.
Stopping bubbling
The event.stopPropagation() method stops bubbling: the parents never hear about the event. It is used rarely, when a nested element must behave in its own way:
<div id="card">
Product card
<button>Add to favourites</button>
</div>
<script>
document.querySelector('#card').addEventListener('click', () => {
console.log('Opening the product');
});
document.querySelector('button').addEventListener('click', (event) => {
event.stopPropagation();
console.log('Added to favourites');
});
</script>Common mistake
Checking event.target when the button contains other tags. A click on an icon or a highlighted word puts that tag into target, not the button. It is more reliable to find the element you need with closest():
<div id="toolbar">
<button data-action="save"><b>Save</b></button>
</div>
<script>
document.querySelector('#toolbar').addEventListener('click', (event) => {
const button = event.target.closest('button');
if (button) {
console.log(button.dataset.action); // save
}
});
</script>