Preface
Capturing and bubbling allow us to implement one of the most powerful event handling patterns, namely the Event Delegation pattern.
The idea is that if we have many elements being handled in a similar way, we don’t need to assign a handler for each element — instead, we place a single handler on their common ancestor.
In the handler, we access event.target
to see where the event actually occurred and handle it accordingly.
Introduction to Event Delegation
<!DOCTYPE HTML>
<html>
<body>
<link type="text/css" rel="stylesheet" href="bagua.css">
<table id="bagua-table">
<tr>
<th colspan="3"><em>Bagua</em> Chart: Direction, Element, Color, Meaning</th>
</tr>
<tr>
<td class="nw"><strong>Northwest</strong>
<br>Metal
<br>Silver
<br>Elders
</td>
<td class="n"><strong>North</strong>
<br>Water
<br>Blue
<br>Change
</td>
<td class="ne"><strong>Northeast</strong>
<br>Earth
<br>Yellow
<br>Direction
</td>
</tr>
<tr>
<td class="w"><strong>West</strong>
<br>Metal
<br>Gold
<br>Youth
</td>
<td class="c"><strong>Center</strong>
<br>All
<br>Purple
<br>Harmony
</td>
<td class="e"><strong>East</strong>
<br>Wood
<br>Blue
<br>Future
</td>
</tr>
<tr>
<td class="sw"><strong>Southwest</strong>
<br>Earth
<br>Brown
<br>Tranquility
</td>
<td class="s"><strong>South</strong>
<br>Fire
<br>Orange
<br>Fame
</td>
<td class="se"><strong>Southeast</strong>
<br>Wood
<br>Green
<br>Romance
</td>
</tr>
</table>
<script>
let table = document.getElementById('bagua-table');
let selectedTd;
table.onclick = function(event) {
let target = event.target;
while (target != this) {
if (target.tagName == 'TD') {
highlight(target);
return;
}
target = target.parentNode;
}
}
function highlight(node) {
if (selectedTd) {
selectedTd.classList.remove('highlight');
}
selectedTd = node;
selectedTd.classList.add('highlight');
}
</script>
<style>
#bagua-table th {
text-align: center;
font-weight: bold;
}
#bagua-table td {
width: 150px;
white-space: nowrap;
text-align: center;
vertical-align: bottom;
padding-top: 5px;
padding-bottom: 12px;
}
#bagua-table .nw {
background: #999;
}
#bagua-table .n {
background: #03f;
color: #fff;
}
#bagua-table .ne {
background: #ff6;
}
#bagua-table .w {
background: #ff0;
}
#bagua-table .c {
background: #60c;
color: #fff;
}
#bagua-table .e {
background: #09f;
color: #fff;
}
#bagua-table .sw {
background: #963;
color: #fff;
}
#bagua-table .s {
background: #f60;
color: #fff;
}
#bagua-table .se {
background: #0c3;
color: #fff;
}
#bagua-table .highlight {
background: red;
}
</style>
</body>
</html>
Dynamic Event Handling with Event Delegation
This table has 9 cells, but it could have 99 or 9999 cells, and it wouldn’t matter. Our task is to highlight the clicked <td>
when the user clicks on it.
Instead of assigning an onclick
handler to each <td>
(potentially many), we can set a “catch-all” handler on the <table>
element. It uses event.target
to determine the clicked element and highlights it.
The JavaScript code is as follows:
let selectedTd;
table.onclick = function(event) {
let target = event.target; // Where was the click?
if (target.tagName != 'TD') return; // Not on TD? We don't care then
highlight(target); // Highlight it
};
function highlight(td) {
if (selectedTd) {
selectedTd.classList.remove('highlight');
}
selectedTd = td;
selectedTd.classList.add('highlight'); // Highlight the new td
}
This code doesn’t care how many cells are in the table. We can dynamically add/remove <td>
, and highlighting still works.
However, there is a flaw. Clicks might not happen on the <td>
itself but inside it.
In our example, if we inspect the HTML internals, we can see nested tags within <td>
, such as <strong>
:
In the table.onclick
handler, we should accept such event.target
and determine if the click occurred inside a <td>
.
Here is the improved code:
table.onclick = function(event) {
let td = event.target.closest('td'); // (1)
if (!td) return; // (2)
if (!table.contains(td)) return; // (3)
highlight(td); // (4)
};
Explanation:
elem.closest(selector)
method returns the closest ancestor that matches the selector. In our case, we look for<td>
starting from the target element.If
event.target
is not within any<td>
, the call returns immediately, as there’s nothing to do here.For nested tables,
event.target
might be a<td>
but outside the current table. So, we check if it’s a<td>
within our table. If yes, we highlight it.
Delegation Example: Actions in Markup
Event delegation has other uses, like handling actions in markup.
For instance, let’s say we want to create a menu with buttons for “Save,” “Load,” and “Search.” There’s an object with methods like save, load, and search. How do we match them?
The initial idea might be to assign a separate handler for each button. But there’s a more elegant solution. We can add a handler to the entire menu and add a data-action
attribute to buttons with method calls:
<button data-action="save">Click to Save</button>
The handler reads the attribute and executes the corresponding method. The working example is as follows:
<div id="menu">
<button data-action="save">Save</button>
<button data-action="load">Load</button>
<button data-action="search">Search</button>
</div>
<script>
class Menu {
constructor(elem) {
this._elem = elem;
elem.onclick = this.onClick.bind(this); // (*)
}
save() {
alert('saving');
}
load() {
alert('loading');
}
search() {
alert('searching');
}
onClick(event) {
let action = event.target.dataset.action;
if (action) {
this[action]();
}
};
}
new Menu(menu);
</script>
<body>
<div id="menu">
<button data-action="save">Save</button>
<button data-action="load">Load</button>
<button data-action="search">Search</button>
</div>
<script>
class Menu {
constructor(elem) {
this._elem = elem;
elem.onclick = this.onClick.bind(this); // (*)
}
save() {
alert('saving');
}
load() {
alert('loading');
}
search() {
alert('searching');
}
onClick(event) {
let action = event.target.dataset.action;
if (action) {
this[action]();
}
};
}
new Menu(menu);
</script>
</body>
Please note that this.onClick
is bound to this
in (*) line. This is crucial because otherwise, the inner this
would refer to the DOM element (elem
) instead of the Menu object, and this[action]
wouldn’t be what we need.
So, what benefits does delegation bring us here?
- We don’t need to write code to assign a handler for each button. Just create a method and place it in the markup.
- The HTML structure is very flexible, and we can add/remove buttons at any time.
We could also use classes like .action-save
, .action-load
, but the data-action
attribute is more semantically meaningful. We can also use it in CSS rules.
“Behavior” Pattern
We can also use event delegation to add “behavior” in a declarative way to elements with specific attributes and classes.
The behavior pattern consists of two parts:
- We add custom attributes to elements describing their behavior.
- A document-wide handler tracks events and executes the behavior if the event occurs on an element with a specific attribute.
Behavior: Counter
For example, the data-counter
attribute here adds a “click to increase” behavior to buttons.
<script>
document.addEventListener('click', function(event) {
if (event.target.dataset.counter != undefined) { // If this attribute exists...
event.target.value++;
}
});
</script>
Counter:
One more counter:
Summary
Event delegation is really cool! It’s one of the most useful patterns for DOM events.
It’s commonly used to add the same handling for many similar elements, but not limited to that.
Algorithm
- Place a handler on the container.
- In the handler — check the source element
event.target
. - If the event happens within an element of interest, handle it.
Benefits
- Simplifies initialization and saves memory: No need to add many handlers.
- Less code: When adding or removing elements, no need to add/remove handlers.
- DOM modifications: We can use
innerHTML
, etc., to add/remove elements in batches.
Event Delegation Limitations
Firstly, events must bubble. Some events do not bubble. Also, low-level handlers should not use event.stopPropagation()
.
Secondly, delegation might increase CPU load as the container-level handler responds to events anywhere in the container, regardless of whether we are interested in that event. However, the load is usually negligible, so we don’t consider it.