I'm a big fan of event delegation. It's more performant than adding multiple event listeners and more maintainable than adding/removing event listeners as the DOM changes. Today, I want to show you how to write clean delegated event handlers using object methods.

Suppose we have some HTML as follows. There's a counter with an initial value of 0 and buttons to increment/decrement the value:

<p>Counter: <span id="counter" aria-live="polite">0</span></p>

<p id="buttons">
<button type="button" aria-controls="counter" data-handler="increment">
Increment
</button>

<button type="button" aria-controls="counter" data-handler="decrement">
Decrement
</button>
</p>

If you're unfamiliar with event delegation, you might be tempted to select both buttons and add a separate event listener to each one:

const counter = document.getElementById("counter");

const incBtn = document.querySelector("[data-handler='increment']");
const decBtn = document.querySelector("[data-handler='decrement']");

let count = counter.textContent;

function increment() {
counter.textContent = ++count;
}

function decrement() {
counter.textContent = --count;
}

incBtn.addEventListener("click", increment);
decBtn.addEventListener("click", decrement);

For such a simple example, this is fine. But if you needed to add more functionality, it would become burdensome to keep adding event listeners, and also less performant. Each event listener would take up a little more space in memory. We can improve this using event delegation.

If statements

We won't select both buttons. We'll select their closest common ancestor and add a single event listener to that instead:

const buttons = document.getElementById("buttons");
buttons.addEventListener("click", handleClick);

In our delegated event handler, we'll get the value of the data-handler attribute on the element that was clicked. We can use the event.target property to get a reference to the element that was clicked, and its .dataset property to manipulate its custom data attributes. Using destructuring assignment, we can get the value of the dataset.handler property and assign it to the handler constant. Then all we have to do is check the value of the handler constant and call the appropriate function:

function handleClick(event) {
const { handler } = event.target.dataset;

if (handler === "increment") {
increment();
} else if (handler === "decrement") {
decrement();
}
}

This is better, but it's not ideal that we have to keep checking the value of the handler constant. Imagine if we had more than two conditions. We'd have to keep rewriting the same if (handler === x) pattern over and over again!

Switch statements

Whenever I'm checking several possible values for the same expression, I consider using a switch statement:

function handleClick(event) {
const { handler } = event.target.dataset;

switch (handler) {
case "increment":
increment();
break;
case "decrement":
decrement();
}
}

Using a switch statement, we no longer have to keep rewriting the same pattern. But we can still do better. Note that the case names are identical to the function names... We can use this to our advantage!

Object methods

First, we'll set our increment() and decrement() functions as methods of a clickHandlers object. Because the functions have the same names as the object methods, we can use the ES6 shorthand for property definitions. And because function declarations are hoisted, we can place the clickHandlers declaration before the function declarations:

const clickHandlers = { increment, decrement };

function increment() {
counter.textContent = ++count;
}

function decrement() {
counter.textContent = --count;
}

As before, we'll get the value of the clicked element's data-handler attribute. If the value is undefined, or if the clickHandlers object doesn't have a method with that name, we'll use a guard clause to stop the function execution. Otherwise, we'll call the method on the clickHandlers object:

function handleClick(event) {
const { handler } = event.target.dataset;
if (!handler || !clickHandlers[handler]) return;
clickHandlers[handler]();
}

This is cleaner and more maintainable. If we want to add more click handlers, all we have to do is set them as methods of the clickHandlers object. We don't have to modify the handleClick() function at all.

View a demo on StackBlitz.