Event Handling & Event Delegation
One Listener to Rule Them All
LinkedIn Hook
Attaching a click handler to every button on the page feels natural — until you add 500 buttons dynamically and the page starts leaking memory.
There's a cleaner pattern that senior engineers reach for without thinking: event delegation.
One listener on a parent. Every child event handled. New elements that didn't exist at page load? Automatically covered.
In this lesson you'll learn how
addEventListenerreally works, the options most developers skip (once,passive,capture), the critical difference betweenevent.targetandevent.currentTarget, and how to wire up a dynamic todo list with a single listener.If you've ever written
forEach(btn => btn.addEventListener(...))— this lesson is your upgrade.Read the full lesson -> [link]
#JavaScript #EventDelegation #Frontend #InterviewPrep #WebDevelopment #CodingInterview #DOM
What You'll Learn
- The
addEventListeneroptions that most developers never touch (once,passive,capture) - How to read the event object —
target,currentTarget,preventDefault,stopPropagation - How to replace N listeners with one using event delegation, even for elements that don't exist yet
The Concierge Pattern
Think of a hotel concierge. Instead of giving every room a personal assistant, one concierge at the front desk handles all requests. That's event delegation — one listener on a parent handles events from all its children.
addEventListener Basics
const btn = document.querySelector("#myBtn");
// Basic click handler
btn.addEventListener("click", function(event) {
console.log("Clicked!", event.target);
console.log("Event type:", event.type); // "click"
console.log("Timestamp:", event.timeStamp);
});
// With options
btn.addEventListener("click", handler, {
once: true, // auto-removes after first fire
passive: true, // tells browser handler won't call preventDefault()
capture: false // listen in bubble phase (default)
});
// Removing a listener (must pass same function reference)
btn.removeEventListener("click", handler);
Event Object Properties
element.addEventListener("click", function(e) {
e.target; // the element that was actually clicked
e.currentTarget; // the element the listener is attached to
e.type; // "click"
e.preventDefault(); // stop default behavior (form submit, link nav)
e.stopPropagation(); // stop event from bubbling up
e.clientX, e.clientY; // mouse position relative to viewport
e.key, e.code; // for keyboard events
});
Event Delegation Pattern
Instead of attaching listeners to every child element, attach ONE listener to the parent and use event.target to determine which child was clicked.
// BAD — listener on every button
document.querySelectorAll(".btn").forEach(btn => {
btn.addEventListener("click", handleClick);
});
// Problem: new buttons added later won't have listeners!
// GOOD — event delegation
document.querySelector("#button-container").addEventListener("click", function(e) {
if (e.target.matches(".btn")) {
handleClick(e);
}
});
// Works for existing AND future buttons!
Practical Example: Dynamic Todo List with Event Delegation
const todoApp = document.getElementById("todo-app");
const input = document.getElementById("todo-input");
// Single listener handles ALL todo interactions
todoApp.addEventListener("click", function(e) {
// Handle delete button
if (e.target.matches(".delete-btn")) {
e.target.closest(".todo-item").remove();
return;
}
// Handle toggle complete
if (e.target.matches(".todo-text")) {
e.target.classList.toggle("completed");
return;
}
// Handle edit button
if (e.target.matches(".edit-btn")) {
const todoItem = e.target.closest(".todo-item");
const text = todoItem.querySelector(".todo-text");
const newText = prompt("Edit todo:", text.textContent);
if (newText) text.textContent = newText;
}
});
// Add new todo
function addTodo(text) {
const item = document.createElement("div");
item.className = "todo-item";
item.innerHTML = `
<span class="todo-text">${text}</span>
<button class="edit-btn">Edit</button>
<button class="delete-btn">Delete</button>
`;
todoApp.appendChild(item);
// No need to attach new listeners — delegation handles it!
}
input.addEventListener("keypress", function(e) {
if (e.key === "Enter" && input.value.trim()) {
addTodo(input.value.trim());
input.value = "";
}
});
Why delegation wins:
- Dynamic elements — new todos automatically work without re-attaching listeners.
- Memory efficient — 1 listener instead of N listeners.
- Simpler cleanup — remove one listener to clean up everything.
Common Mistakes
- Passing an anonymous function to
addEventListenerand then trying toremoveEventListener— without the same function reference, the removal is a no-op. Store the handler in a named variable. - Attaching listeners to every child inside a loop when they all share behavior — delegate to a common parent and filter on
event.target.matches(...). - Confusing
event.targetwithevent.currentTargetduring delegation —targetis whatever was clicked (a child),currentTargetis always the element that owns the listener (the parent).
Interview Questions
Q: What is event delegation and why is it useful?
Event delegation is a pattern where you attach a single event listener to a parent element instead of individual listeners on each child. It works because events bubble up from the target to ancestors. Benefits: handles dynamically added elements, uses less memory, and simplifies code.
Q: How do you handle events for elements that don't exist yet?
Use event delegation. Attach the listener to a parent that already exists, then use
event.targetwith.matches()or.closest()to filter for the desired child elements. Since events bubble up, new children will trigger the parent's listener automatically.
Q: What is the difference between e.target and e.currentTarget?
e.targetis the element that originally triggered the event (what was actually clicked).e.currentTargetis the element the listener is attached to. In event delegation, these are different —targetis the child,currentTargetis the parent.
Q: Name 3 benefits of event delegation.
(1) Works for elements added to the DOM after the listener was registered, (2) uses a single listener instead of N listeners — less memory, (3) simpler teardown: one
removeEventListenercleans up everything.
Q: What do the once and passive options on addEventListener do?
once: trueauto-removes the listener after it fires one time.passive: truepromises the handler won't callpreventDefault(), which lets the browser scroll immediately on touch/wheel events without waiting — a major perf win for scroll handlers.
Quick Reference — Cheat Sheet
EVENT HANDLING & DELEGATION — QUICK MAP
addEventListener(type, handler, options):
options.once -> auto-removes after first fire
options.passive -> promises no preventDefault (scroll perf)
options.capture -> listen in capture phase (default: bubble)
Event object essentials:
e.target -> actually clicked element
e.currentTarget -> element that owns the listener
e.preventDefault() -> cancel default action
e.stopPropagation() -> stop bubbling/capturing
e.key / e.code -> keyboard
e.clientX / clientY -> pointer coords
Delegation recipe:
parent.addEventListener("click", e => {
if (e.target.matches(".child")) handleChild(e);
if (e.target.closest(".row")) handleRow(e);
});
Why delegate:
- works for future children
- 1 listener vs N
- simpler cleanup
Previous: DOM Manipulation Next: Event Bubbling vs Capturing
This is Lesson 11.2 of the JavaScript Interview Prep Course — 14 chapters, 87 lessons.