Back to Tutorials
🎯 Intermediate

Event Handling

7 min read | Events

Event Handling

Events are how a page reacts to the world: clicks, key presses, form submissions, scrolling, incoming messages. JavaScript's event system is a subscription model — you register a function, the browser calls it when the event fires. This tutorial covers registering listeners, reading the event object, delegation, and the mistakes that account for most broken buttons on the internet.

Step 1: Adding Event Listeners

const button = document.querySelector("#myButton");
button.addEventListener("click", (event) => {
  console.log("Button clicked!", event);
});
// Options give you fine control
button.addEventListener("click", handler, {
  once: true,      // auto-remove after the first call
  capture: false,  // run during the bubbling phase (default)
  passive: true    // promise not to call preventDefault() — faster scrolling
});

Why addEventListener beats onclick: the older element.onclick = fn property holds exactly one handler — assigning a second silently overwrites the first. addEventListener stacks any number of independent handlers and supports options like once and passive. The passive: true flag matters for scroll and touchmove: it tells the browser it may scroll immediately without waiting to see whether your handler cancels the event.

Step 2: The Event Object

Every handler receives an event object describing what happened:

element.addEventListener("click", (event) => {
  event.target;           // the element actually clicked (could be a child)
  event.currentTarget;    // the element this listener is attached to
  event.type;             // "click"
  event.preventDefault(); // stop the default browser action
  event.stopPropagation(); // stop the event from bubbling further up
});

target vs currentTarget is the distinction beginners miss: click the icon inside a button, and target is the icon while currentTarget is the button carrying the listener. preventDefault vs stopPropagation are also unrelated tools: the first cancels the browser's built-in behavior (following a link, submitting a form), the second stops other listeners higher in the tree from hearing the event. Needing both is rare; picking the wrong one is common.

Step 3: Common Events in Practice

// Keyboard — react to specific keys
document.addEventListener("keydown", (e) => {
  if (e.key === "Enter") console.log("Enter pressed");
  if (e.key === "Escape") closeModal();
});
// Forms — intercept submission for validation or fetch()
form.addEventListener("submit", (e) => {
  e.preventDefault();               // stop the full-page reload
  const data = new FormData(form);
  console.log(Object.fromEntries(data));
});
// Live input — fires on every keystroke
input.addEventListener("input", (e) => {
  console.log(e.target.value);
});

Why preventDefault on submit: a form's default action navigates to its action URL, reloading the page and destroying your application state. Preventing it lets you validate and send the data with fetch instead — the foundation of every single-page app form.

Step 4: Event Delegation

Events bubble: after firing on the target, they travel up through every ancestor. Delegation exploits this — one listener on a parent handles clicks for any number of children, including ones added later:

const list = document.querySelector("#list");
list.addEventListener("click", (e) => {
  const item = e.target.closest("li");
  if (item && list.contains(item)) {
    console.log("Item clicked:", item.textContent);
  }
});

Why delegate: a 500-row table needs one listener, not 500; memory stays flat and rows created dynamically work automatically, because the listener sits on the parent that never changes. closest("li") handles clicks that land on elements nested inside the row.

Step 5: Removing Listeners and Custom Events

function onScroll() { console.log(window.scrollY); }
window.addEventListener("scroll", onScroll);
window.removeEventListener("scroll", onScroll); // must be the SAME reference
// Custom events let components talk without coupling
const evt = new CustomEvent("userLoggedIn", {
  detail: { userId: 123, username: "john" },
  bubbles: true
});
element.dispatchEvent(evt);
element.addEventListener("userLoggedIn", (e) => {
  console.log("User:", e.detail.username);
});

Why removal needs the same reference: listeners are matched by function identity. An inline arrow function creates a brand-new object each time, so it can never be removed. Store the handler in a variable, or use { once: true }, or an AbortController signal to detach many listeners at once.

Common Beginner Errors and Fixes

  • Calling the handler while registering: addEventListener("click", doThing()) runs doThing immediately. Fix: pass the reference doThing, or wrap: () => doThing(id).
  • Trying to remove an anonymous function: removeEventListener("click", () => {...}) never matches. Fix: keep a named reference to the exact function you added.
  • Forgetting preventDefault on form submit: the page reloads and your fetch never completes. Fix: call e.preventDefault() first thing in the submit handler.
  • Reading this inside an arrow handler: arrows inherit this from outside, so it is not the element. Fix: use e.currentTarget, which always works.
  • Listeners piling up: re-running setup code adds duplicate handlers, so one click logs three times. Fix: register once, or remove before re-adding, or use once.

Practice Exercise

Build a small interactive list with delegation:

  • Create a ul with five li items ("Task 1" through "Task 5") from JavaScript.
  • Attach one click listener to the ul that toggles a "done" class on whichever li was clicked.
  • Add a button that appends a new task — confirm clicking the new task works without adding any listener.
  • Add a keydown listener so pressing d toggles the last item.
  • Bonus: dispatch a CustomEvent named "taskToggled" (with the task text in detail) every time a task is toggled, and log it from a separate listener.
  • If step 3 works on the dynamically added task, you have understood delegation — the single most useful event pattern in real applications.