Chapter 8.4☕ 18 min read

Events: Capture, Bubble & Event Object Deep Dive

Click kiya — event neeche utarta hai (Capture), phir upar udta hai (Bubble). target vs currentTarget samjho, stopPropagation se mat khelo.

01HTML Attributes vs DOM Properties — Pehle Samjho Dono Mein Farq

Every DOM event travels through 3 phases before it's done: Capture, Target, and Bubble. Understanding this flow is the key to mastering event handling.

Capture Phase: The event starts at the window and travels DOWN through ancestors until it reaches the target element. Think of it as a waterfall cascading down the DOM tree.

Target Phase: The event reaches the actual element that was clicked or interacted with. Here, both capture and bubble listeners on the target fire in registration order.

Bubble Phase: The event travels UP from the target through all ancestors back to window. Most event handlers are registered for this phase.

// Visualize the 3 phases
// DOM: <div id="grandparent"> → <div id="parent"> → <button id="child">

document.getElementById('grandparent').addEventListener('click', 
  (e) => console.log('1. Grandparent CAPTURE'), 
  { capture: true }
);

document.getElementById('parent').addEventListener('click', 
  (e) => console.log('2. Parent CAPTURE'), 
  { capture: true }
);

document.getElementById('child').addEventListener('click', 
  (e) => console.log('3. Child TARGET (capture listener)')
);

document.getElementById('child').addEventListener('click', 
  (e) => console.log('4. Child TARGET (bubble listener)')
);

document.getElementById('parent').addEventListener('click', 
  (e) => console.log('5. Parent BUBBLE')
);

document.getElementById('grandparent').addEventListener('click', 
  (e) => console.log('6. Grandparent BUBBLE')
);

// Click the button → Output order:
// 1. Grandparent CAPTURE → 2. Parent CAPTURE → 3. Child TARGET → 
// 4. Child TARGET → 5. Parent BUBBLE → 6. Grandparent BUBBLE

Most events bubble, but some don't: focus, blur, mouseenter, mouseleave. These stop at the target element.

Use addEventListener(event, handler, { capture: true }) to listen during capture phase. Default is bubble phase.

02getAttribute vs property access — Kab Kya Use Karein?

The addEventListener method and the Event Object give you full control over event handling. Let's break down every property.

addEventListener syntax: element.addEventListener(event, handler, options)

Options object:

capture: true — Listen in capture phase instead of bubble

once: true — Auto-remove handler after first fire (great for one-time actions)

passive: true — Promise you won't call preventDefault (improves scroll performance)

signal: abortController.signal — Cancel listener via AbortController

// The Event Object
document.getElementById('parent').addEventListener('click', (e) => {
  console.log(e.type);           // "click"
  console.log(e.target);         // The ACTUAL element clicked (e.g., button)
  console.log(e.currentTarget);  // The element THIS handler is on (parent)
  console.log(e.bubbles);        // true (click bubbles)
  console.log(e.cancelable);     // true (can preventDefault)
  console.log(e.eventPhase);     // 1=capture, 2=target, 3=bubble
  console.log(e.isTrusted);      // true (real click, not dispatchEvent)
  console.log(e.timeStamp);      // Milliseconds since page load
});

// addEventListener options
button.addEventListener('click', handler, {
  capture: false,  // Listen in bubble phase (default)
  once: true,      // Auto-remove after first click!
  passive: true,   // Promises not to call preventDefault (scroll perf)
  signal: abortController.signal // Cancel via AbortController
});

// Remove with AbortController
const controller = new AbortController();
button.addEventListener('click', handler, { signal: controller.signal });
controller.abort(); // Listener removed! No removeEventListener needed!
target vs currentTarget: e.target is the element that triggered the event — it NEVER changes during propagation. e.currentTarget is the element whose handler is currently running — it changes at each level of the bubble phase. Use e.currentTarget when you need to know "where is THIS handler attached."

isTrusted property: true means a real user action triggered the event. false means it was dispatched via JavaScript (element.dispatchEvent()).

03classList — Add, Remove, Toggle & Contains

These three methods control event behavior — know exactly what each one does.

stopPropagation(): Stops the event from moving to the next element in the capture/bubble chain. Other handlers on the SAME element still fire!

stopImmediatePropagation(): Stops the event AND prevents other handlers on the SAME element from firing. More aggressive than stopPropagation.

preventDefault(): Prevents the browser's default action — link navigation, form submission, checkbox toggle. Only works if event.cancelable is true.

// stopPropagation vs stopImmediatePropagation
button.addEventListener('click', (e) => {
  console.log('Handler 1');
  e.stopPropagation(); // Stops bubble to parent
  // Handler 2 and 3 on THIS element STILL fire!
});
button.addEventListener('click', (e) => {
  console.log('Handler 2'); // Still runs!
});
button.addEventListener('click', (e) => {
  console.log('Handler 3'); // Still runs!
});
// Parent handler does NOT run (propagation stopped)

// stopImmediatePropagation — kills everything
button.addEventListener('click', (e) => {
  console.log('Handler 1');
  e.stopImmediatePropagation(); // NO other handlers fire!
});
button.addEventListener('click', (e) => {
  console.log('Handler 2'); // NEVER runs!
});

// preventDefault — stop browser action, not propagation
link.addEventListener('click', (e) => {
  e.preventDefault(); // Don't navigate!
  console.log('Link clicked but no navigation');
  // Event STILL bubbles to parent! Only default action prevented.
});
📋 Rule: Use preventDefault freely. Use stopPropagation sparingly — it prevents parent elements from ever seeing the event, which breaks event delegation. Use stopImmediatePropagation almost never — it's a code smell.
04Data Attributes — dataset Ka Magic

Different event types have different behaviors. Know which events bubble and which don't.

Mouse Events: click, dblclick, mousedown, mouseup, mousemove. mouseover/mouseout BUBBLE. mouseenter/mouseleave do NOT bubble.

Keyboard Events: keydown (repeats when held!), keyup. event.key = character ("a", "Enter"). event.code = physical key ("KeyA", "Enter").

Focus Events: focus/blur do NOT bubble. focusin/focusout DO bubble. Use focusin/focusout for event delegation.

Input Events: input fires on ANY value change (typing, paste, cut). change fires on blur for text inputs, on selection for radio/checkbox.

// Mouse: over vs enter — BUBBLING difference
parent.addEventListener('mouseenter', () => console.log('enter'));  // NO bubble
parent.addEventListener('mouseover', () => console.log('over'));    // BUBBLES
// Moving from parent to child:
// mouseenter: fires ONCE on parent enter. No child event.
// mouseover: fires on parent, then on child, then on parent (leaving child)
// Result: mouseenter = 1 event. mouseover = 3+ events!

// Keyboard: key vs code
document.addEventListener('keydown', (e) => {
  console.log(e.key);   // "a" or "A" (depends on Shift/CapsLock)
  console.log(e.code);  // "KeyA" (always same physical key)
  // Use e.key for text input logic
  // Use e.code for game controls / shortcut keys
  if (e.key === 'Enter') handleSubmit();
  if (e.ctrlKey && e.key === 's') { e.preventDefault(); save(); }
});

// Focus: focus/blur DON'T bubble — use focusin/focusout
form.addEventListener('focusin', (e) => {
  e.target.classList.add('focused'); // Works! focusin bubbles.
});
form.addEventListener('focusout', (e) => {
  e.target.classList.remove('focused');
});

// Input vs Change
textInput.addEventListener('input', (e) => {
  console.log(e.target.value); // Fires on EVERY keystroke
});
textInput.addEventListener('change', (e) => {
  console.log(e.target.value); // Fires when user LEAVES the field
});

Touch Events: touchstart, touchmove, touchend. event.touches = all current touches, event.changedTouches = what changed in this event.

05Performance — Bulk Class Operations & attribute Namespace

Custom Events let you create your own event types with custom data. removeEventListener has a tricky gotcha that catches everyone.

Creating Custom Events: Use new CustomEvent(type, options). The detail property carries your custom data.

Dispatching: element.dispatchEvent(event) fires the event synchronously — all handlers run before dispatchEvent returns!

// Custom Events
const event = new CustomEvent('itemAdded', {
  detail: { id: 123, name: 'Biryani' },
  bubbles: true,
  cancelable: true
});

list.addEventListener('itemAdded', (e) => {
  console.log('Item added:', e.detail.name); // "Biryani"
});

list.dispatchEvent(event); // Synchronous! Handlers run immediately.
// isTrusted = false for dispatched events

The #1 removeEventListener Bug: You must pass the EXACT SAME function reference to removeEventListener that you passed to addEventListener. Arrow functions create NEW objects each time!

// ❌ The #1 removeEventListener Bug
element.addEventListener('click', () => console.log('hi'));
element.removeEventListener('click', () => console.log('hi'));
// TWO DIFFERENT function objects! Removal FAILS!
// () => {} creates a new arrow function every time.

// ✅ Fix: Store handler reference
const handler = () => console.log('hi');
element.addEventListener('click', handler);
element.removeEventListener('click', handler); // Same reference! Works!

// ✅ Or use AbortController (modern approach)
const controller = new AbortController();
element.addEventListener('click', handler, { signal: controller.signal });
controller.abort(); // Listener removed! Clean and simple.
dispatchEvent is synchronous: All event handlers run BEFORE dispatchEvent returns. This is different from real user events, which are dispatched asynchronously by the browser. Custom events with dispatchEvent have isTrusted = false, which you can check to distinguish programmatic from real events.

Lo kar liya — Key Points:

  • ✅ Every DOM event travels in 3 phases: Capture (top-down) → Target → Bubble (bottom-up)
  • ✅ target is the element that triggered the event (never changes); currentTarget is the element whose handler is running (changes during bubble)
  • ✅ stopPropagation stops the event from reaching other elements; stopImmediatePropagation also prevents other handlers on the same element
  • ✅ preventDefault stops the browser's default action (navigation, form submit) but does NOT stop propagation
  • ✅ mouseover/mouseout bubble; mouseenter/mouseleave do NOT bubble — use the bubbling versions for delegation
  • ✅ focus/blur don't bubble; use focusin/focusout instead for event delegation on forms
  • ✅ CustomEvent with detail carries custom data; dispatchEvent fires synchronously with isTrusted = false
  • ✅ removeEventListener requires the exact same function reference — arrow functions can't be removed unless stored in a variable
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase