JavaScript Interview Prep
DOM & Browser APIs

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 addEventListener really works, the options most developers skip (once, passive, capture), the critical difference between event.target and event.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


Event Handling & Event Delegation thumbnail


What You'll Learn

  • The addEventListener options 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:

  1. Dynamic elements — new todos automatically work without re-attaching listeners.
  2. Memory efficient — 1 listener instead of N listeners.
  3. Simpler cleanup — remove one listener to clean up everything.

Event Handling & Event Delegation visual 1


Common Mistakes

  • Passing an anonymous function to addEventListener and then trying to removeEventListener — 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.target with event.currentTarget during delegation — target is whatever was clicked (a child), currentTarget is 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.target with .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.target is the element that originally triggered the event (what was actually clicked). e.currentTarget is the element the listener is attached to. In event delegation, these are different — target is the child, currentTarget is 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 removeEventListener cleans up everything.

Q: What do the once and passive options on addEventListener do?

once: true auto-removes the listener after it fires one time. passive: true promises the handler won't call preventDefault(), 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.

On this page