Notifications

No notifications

/Phase 2

Events & Handlers

Events & Handlers — Reacting to User Interaction

Events are signals fired by the browser when something happens — a click, a keypress, a scroll, a form submission. Event listeners let your JavaScript respond to these signals.

Adding Event Listeners

element.addEventListener("click", handler);
element.removeEventListener("click", handler);

Always use addEventListener over inline onclick — it supports multiple listeners and options.

Common Event Types

CategoryEvents
Mouseclick, dblclick, mouseenter, mouseleave
Keyboardkeydown, keyup, keypress (deprecated)
Formsubmit, input, change, focus, blur
Windowload, resize, scroll, DOMContentLoaded
Touchtouchstart, touchmove, touchend

The Event Object

Every handler receives an event object with useful properties:

PropertyDescription
e.targetThe element that originated the event
e.currentTargetThe element the listener is attached to
e.typeEvent type ("click", "keydown", etc.)
e.preventDefault()Stop default behavior (e.g., form submit)
e.stopPropagation()Stop event from bubbling up

Event Bubbling

Events bubble up from the target to the root:

Click on <button> inside <div> inside <body>:

button → div → body → html → document

Event Delegation

Instead of adding listeners to every child, add ONE listener to the parent and check e.target:

list.addEventListener("click", (e) => {
  if (e.target.matches("li")) {
    handleItemClick(e.target);
  }
});

This is efficient, works with dynamically added elements, and uses less memory.

On this page

Detailed Theory

What "Events" Actually Mean

The web is reactive. The user clicks, types, scrolls, resizes, drags a file in — each of those is an event that the browser fires. Your job in JavaScript is simply: "when *X* happens, run *this* function."

That function is called an event handler (or *listener*).

The One Function You Need: addEventListener

const button = document.querySelector("#save");

button.addEventListener("click", () => { console.log("Saved!"); });

Three pieces:

1. The element you're listening on (button). 2. The event name as a string ("click"). 3. The handler — a function that runs each time it fires.

That's the entire mental model. Everything else is just more event names and details.

Common Events You'll Use Daily

EventFires when
clickThe element is clicked
inputA form field's value changes (every keystroke)
changeA field loses focus after value changed
submitA
is submitted
keydown / keyupA key is pressed / released
mouseenter / mouseleaveMouse enters/leaves the element
scrollElement (or window) is scrolled
loadImage / page finished loading
DOMContentLoadedHTML is parsed (don't wait for images)

The Event Object

Your handler receives one argument — a rich event object describing what happened:

button.addEventListener("click", (e) => {
  console.log(e.target);        // the element clicked
  console.log(e.currentTarget); // the element the listener is on
  console.log(e.type);          // "click"
  e.preventDefault();           // cancel default behaviour
});

For specific events you get extras:

  • MouseEvente.clientX, e.clientY, e.button
  • KeyboardEvente.key ("Enter"), e.code ("Enter"), e.shiftKey
  • InputEvente.target.value

Removing a Listener

If you want to stop listening, you must pass the same function reference you used to add it:

function onClick() { /* … */ }
button.addEventListener("click", onClick);
button.removeEventListener("click", onClick);

This is why anonymous arrow functions can't be removed individually.

Form Handling — The Beginner's Friend

const form = document.querySelector("form");
form.addEventListener("submit", (e) => {
  e.preventDefault(); // stop the default page reload
  const data = new FormData(form);
  console.log(Object.fromEntries(data));
});

FormData + preventDefault is the classic combo for grabbing form values.

Beginner Mistakes to Skip

1. Forgetting e.preventDefault() on a — the page reloads and your data vanishes. 2. Re-binding listeners every render — leaks memory in long-lived apps. Add once, remove on cleanup. 3. Using onclick="…" HTML attributes — works but mixes concerns. Prefer addEventListener. 4. Listening on every list item instead of using *delegation* (next section). 5. Trying to remove an arrow function by re-defining it — that's a *new* reference.

Intermediate: Event Propagation — The Three Phases

When you click a button inside a card inside a section, the browser doesn't only fire on the button. The event walks the DOM in three phases:

1. Capturing → document ⤵ html ⤵ body ⤵ section ⤵ card ⤵ BUTTON
2. Target    →                                       BUTTON (the click hits)
3. Bubbling  → button ⤴ card ⤴ section ⤴ body ⤴ html ⤴ document

By default, listeners fire on the bubbling phase. To listen on the way down instead:

parent.addEventListener("click", handler, { capture: true });

Stop Codes

e.stopPropagation();          // don't bubble further
e.stopImmediatePropagation(); // also skip other listeners on this element
e.preventDefault();           // cancel the default browser action

Intermediate: Event Delegation — One Listener to Rule Them All

Imagine a list of 500 todo items, each with a delete button. Adding 500 listeners is wasteful and breaks when items are added later. Instead, listen on the parent and use event.target to find what was actually clicked:

list.addEventListener("click", (e) => {
  const deleteBtn = e.target.closest(".delete");
  if (!deleteBtn) return;
  const id = deleteBtn.dataset.id;
  removeTodo(id);
});

element.closest(selector) walks up from the click target until it finds a match — robust even when buttons contain nested icons.

Delegation shines for:

  • Long lists
  • Dynamically added content (SPA routes, AJAX results)
  • Tab systems, accordions, menus

Intermediate: Custom Events

You can invent your own events for clean cross-component communication:

const event = new CustomEvent("cart:update", {
  detail: { itemId: 42, quantity: 2 },
  bubbles: true,
});
cartButton.dispatchEvent(event);

document.addEventListener("cart:update", (e) => { console.log(e.detail.itemId); });

The cart button doesn't need to know who's listening, and the listener doesn't need to know who fired it. Loose coupling for free.

Intermediate: Passive Listeners (Scroll Performance)

Touch and scroll listeners can block scrolling because the browser has to wait to see if you'll call preventDefault(). Tell it you won't:

window.addEventListener("scroll", onScroll, { passive: true });

This single flag dramatically improves scroll smoothness on mobile.

Advanced: Throttle & Debounce

High-frequency events (scroll, resize, mousemove, input) can fire 60+ times per second. You almost never want to react that often.

  • Debounce: wait until the events stop, then run once. Great for live search.
  • Throttle: run at most once every X ms. Great for scroll handlers.
function debounce(fn, wait = 300) {
  let t;
  return (...args) => {
    clearTimeout(t);
    t = setTimeout(() => fn(...args), wait);
  };
}

searchInput.addEventListener("input", debounce((e) => { fetchResults(e.target.value); }, 250));

Advanced: AbortController for Bulk Cleanup

Modern way to remove many listeners at once — perfect for component lifecycle:

const ctrl = new AbortController();

window.addEventListener("scroll", onScroll, { signal: ctrl.signal }); window.addEventListener("resize", onResize, { signal: ctrl.signal }); button.addEventListener("click", onClick, { signal: ctrl.signal });

// Later — remove ALL three at once: ctrl.abort();

Cleaner than tracking each handler reference individually.

Advanced: PointerEvents — One API for Mouse + Touch + Pen

Instead of writing separate mouse and touch handlers, use pointerdown, pointermove, pointerup. They expose e.pointerType ("mouse" / "touch" / "pen") and unify the code path.

Advanced: Pointer Capture for Drag Handlers

When dragging, the cursor often leaves the element and the drag breaks. Lock pointer events to the element:

handle.addEventListener("pointerdown", (e) => {
  handle.setPointerCapture(e.pointerId);
});

All subsequent move/up events for that pointer go to handle, even outside its bounds.

Practice Path

1. Build a button counter — every click increments a number on screen. 2. Build a live search box that calls fetch only after the user has stopped typing for 300 ms (debounce). 3. Replace 50 individual

  • listeners with one delegated listener on the
      . 4. Add a global keyboard shortcut (e.g. Ctrl + K opens a modal) using document.addEventListener("keydown", …).