You probably meant to use event.currentTarget
instead of event.target
:
event.currentTarget
is the target of the current listener (further: current target).
event.target
is the target to which the event was dispatched (further: dispatching target).
Alternatively you can just reference the specific target directly since you use distinct listeners with distinct references (up
, down
) as a closure.
Instead of using distinct listeners for each element, you could also make use of event delegation (see below).
Also, see below for an explanation of event propagation and stopPropagation()
.
Event propagation
A single dispatched event may invoke multiple listeners.
The order of invocation for the listeners is specified to happen in phases:
- Capturing phase:
Capturing listeners are invoked in tree-order; from root to dispatching target.
- Target phase:
Non-capturing listeners of the dispatching target are invoked.
- Bubbling phase:
Non-capturing listeners are invoked in reverse tree-order; from (excluding) dispatching target to root.
Additionally, listeners of a target are invoked in the order in which they are added.
This sequence (of listener invocations) is called event propagation.
At any point may a listener stop this propagation from reaching the next listener, e.g. via event.stopPropagation()
:
Example of stopping propagation early:
const outer = document.getElementById("outer");
const inner = document.getElementById("inner");
// A non-capturing listener
inner.addEventListener("click", evt => logId(evt.currentTarget));
outer.addEventListener("click", evt => {
evt.stopPropagation();
logId(evt.currentTarget);
}, { capture: true }); // Attach a capturing listener
function logId(element) {
console.log(element.id);
}
#outer {background-color: blue}
#inner {background-color: red}
div {
padding: .2rem;
padding-block-start: 3.8rem;
}
<div id="outer">
<div id="inner"></div>
</div>
Event delegation
Listeners of common ancestors of two distinct elements will be invoked due to event propagation (see above).
And events hold references to the dispatching target and the current target. This allows listeners to be more abstract than otherwise possible.
Together, these aspects allow events to be handled by one listener on a (but usually the first) common ancestor (further: delegator) for multiple distinct elements. This is called event delegation.
Sidenote: This is most easily realized if the relevant elements are siblings, but this is not a requirement.
(Any descendant of) the delegator may be the dispatching target. This also means that no relevant element may be a target. For example, if the common ancestor itself is the dispatching target, then no relevant element is targeted.
We need to assert that the event happened in a relevant element. Otherwise the event should not be handled.
In most cases that assertion can be done by querying for the relevant element with calling closest()
on event.target
.
Advantages of using event delegation:
- "It allows us to attach a single event listener for elements that exist now or in the future":
- Less memory usage.
- Less mental overhead and simpler code when adding/removing elements.
- Allows "behaviour pattern": Elements with e.g. certain attributes will automatically inherit some behaviour.
- Allows separation of design (relevant elements + CSS) and application (delegator).
- Less likely to cause significant memory leaks:
- One listener means one closure at maximum. As opposed to potentially infinite listeners and therefore closures, this one closure is less likely to have a significant effect on memory usage.
Note that not all events bubble, meaning you cannot use event delegation to handle them.
Example
A typical listener for event delegation...
- Finds the first common ancestor to be used as a delegator.
- Attaches an abstract listener to the common ancestor:
- Assert that event happened in a relevant element; otherwise abort.
- Handles the event.
Let's say we have a table of products, and want to log the product row that was clicked on as an object. An implementation may look like this:
const tbody = document.querySelector("tbody"); // First common ancestor
tbody.addEventListener("click", evt => {
const tr = evt.target.closest("tr"); // Find reference to relevant element
if (tr === null) return; // Abort if event not in relevant element
// Usecase-specific code
logRow(tr);
});
function logRow(tr) {
const [idCell, nameCell, amountCell] = tr.children;
const row = {
id: idCell.textContent,
name: nameCell.textContent,
amount: Number(amountCell.textContent)
};
console.log(row);
}
<table>
<caption>Table of products</caption>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Amount in stock</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>Spaghetti</td>
<td>34</td>
</tr>
<tr>
<td>1</td>
<td>Peanuts</td>
<td>21</td>
</tr>
<tr>
<td>3</td>
<td>Crackers</td>
<td>67</td>
</tr>
</tbody>
</table>
Without event delegation (i.e. with distinct listeners), an implementation could look like this:
const tableRows = document.querySelectorAll("tbody>tr");
tableRows.forEach(tr => {
tr.addEventListener("click", () => {
// `event.currentTarget` will always be `tr`, so let's use direct reference
logRow(tr);
});
})
function logRow(tr) {
const [idCell, nameCell, amountCell] = tr.children;
const row = {
id: idCell.textContent,
name: nameCell.textContent,
amount: Number(amountCell.textContent)
};
console.log(row);
}
<table>
<caption>Table of products</caption>
<thead>
<tr>
<th>ID</th>
<th>Name</th>
<th>Amount in stock</th>
</tr>
</thead>
<tbody>
<tr>
<td>0</td>
<td>Spaghetti</td>
<td>34</td>
</tr>
<tr>
<td>1</td>
<td>Peanuts</td>
<td>21</td>
</tr>
<tr>
<td>3</td>
<td>Crackers</td>
<td>67</td>
</tr>
</tbody>
</table>
If you were to add or remove rows, then...
- ... the first example would just work.
- ... the second example would have to consider adding listeners to the new elements. If they are added e.g. via
innerHTML
or cloneNode()
, then this may become complicated.