Chapter 16 of 28 · JavaScript for Beginners — Complete Free Tutorial

Events: Delegation, Bubbling & Listeners

Events are how the page talks back: clicks, keystrokes, submissions, scrolls, finished loads. Mastering addEventListener, the event object, propagation and delegation is what makes dynamic interfaces robust — and delegation is the single most important pattern in this chapter.

Adding a listener

const btn = document.querySelector('#save');

btn.addEventListener('click', (event) => {
  console.log('Saved!', event.type);   // → Saved! click
});

Multiple listeners can share one element; each fires independently. The old onclick = fn property replaces itself — addEventListener accumulates.

The event object

PropertyMeaning
typeEvent name ('click', 'keydown'…)
targetElement that originated the event
currentTargetElement the handler is attached to
preventDefault()Cancel the default browser action
stopPropagation()Stop the event from travelling further
key / codeKeyboard events: pressed key value / physical key
clientX, clientYPointer coordinates (viewport)

Common events

EventFires when
click / dblclickPointer click / double click
mouseenter / mouseleavePointer enters / leaves (no bubbling)
inputValue changes while typing (live)
changeValue committed (blur, checkbox toggle)
submitForm submitted
keydown / keyupKey pressed / released
DOMContentLoadedHTML fully parsed
focus / blurElement gains / loses focus

Bubbling and capturing

Events travel: capture (top → target), the target itself, then bubble (target → up). By default your listener runs during bubbling:

document.body.addEventListener('click', (e) => {
  console.log(`bubbled: ${e.target.tagName}`);
});

// document.querySelector('#child').click();
// Expected output: bubbled: DIV   (handler on body still fires)

Third argument options: addEventListener('scroll', fn, { passive: true }), { once: true } (auto-remove), { capture: true } (listen on the way down).

Event delegation: one listener for many elements

Instead of a listener per row — impossible for rows created later — attach one listener to a stable ancestor and use e.target.closest():

const list = document.querySelector('.tasks');

list.addEventListener('click', (e) => {
  const removeBtn = e.target.closest('.task__remove');
  if (!removeBtn) return;                       // not our button

  removeBtn.closest('li').remove();             // works for every current AND future row
});

Expected output: every remove button works — including ones rendered after page load — with exactly one listener, one closure, minimal memory. This scales from 10 rows to 10,000.

Delegation also powers table rows, cards, dropdown items — any repeating UI.

preventDefault: stopping browser behaviour

document.querySelector('form').addEventListener('submit', (e) => {
  e.preventDefault();                // stop the page reload
  // … validate + handle with JavaScript (chapter 17)
});

document.querySelector('a[href^="#"]').addEventListener('click', (e) => {
  // custom smooth-scroll instead of instant jump
});

Keyboard input

document.querySelector('#search').addEventListener('keydown', (e) => {
  if (e.key === 'Escape') { e.target.value = ''; }
  if (e.key === 'Enter'  && e.metaKey) { submitSearch(e.target.value); }
});

e.key is the character/meaning ('Enter', 'a', 'ArrowUp') — locale-independent; e.code is the physical key. For shortcuts, check modifiers with e.metaKey/e.ctrlKey.

Removing listeners

function onSave() { console.log('saved'); }
btn.addEventListener('click', onSave);
btn.removeEventListener('click', onSave);   // needs the SAME function reference

// once: true — self-removing
btn.addEventListener('click', handle, { once: true });

Anonymous arrows cannot be removed later — use named functions when the listener has a lifecycle (cleanup in SPAs, closing modals).

Custom events (a taste)

const bus = new EventTarget();
bus.addEventListener('user:login', (e) => console.log(e.detail?.name));
bus.dispatchEvent(new CustomEvent('user:login', { detail: { name: 'Asha' } }));

Common mistakes

  • One listener per dynamic row — memory bloat + broken for future rows; use delegation.
  • Forgetting preventDefault on form submit — instant page reload wipes your JS state.
  • Comparing e.target when you mean e.currentTarget — target is where the click landed (maybe a nested <span>); use closest().
  • stopPropagation everywhere — it breaks delegated ancestors; fix your handler logic instead of silencing the whole tree.
  • Listening to scroll without { passive: true } — blocks smooth scrolling performance.
  • Removing the wrong reference — removeEventListener needs the identical function.

Practice checklist

  1. Delegate clicks on a dynamically-added list: log which item's button was pressed.
  2. Build a live search box with the input event filtering an in-page list.
  3. Intercept a form submit with preventDefault and show the values in the page instead of reloading.
  4. Add an Escape handler that clears an input — verify with a real key press.

Forms deserve their own full treatment — next: Forms & User Input.

Quiz: Events: Delegation, Bubbling & Listeners

5 questions · 70% to pass

Frequently Asked Questions

JavaScript is the programming language of the web. It adds behaviour to pages: responding to clicks and input, validating forms, fetching data from APIs, updating the DOM without reloading, and much more. It also runs on servers (Node.js), in desktop and mobile apps, and in build tooling.

They are unrelated languages that share a similar name for historical marketing reasons. Java is a statically-typed compiled language; JavaScript is a dynamically-typed interpreted language designed for the browser. Knowing one does not imply knowledge of the other.

Helpful but not mandatory — HTML gives you the structure JavaScript manipulates and CSS the styling it toggles. If you know basic tags, classes and selectors, you have enough to start; the DOM chapters assume only that much.

Use const by default and let when the value must change. Avoid var entirely in modern code: it is function-scoped instead of block-scoped, hoists differently, and can create accidental globals. This course uses const and let throughout.

The Document Object Model is the browser’s in-memory tree of objects representing your HTML. JavaScript reads and modifies that tree — selecting elements, changing text, adding nodes, attaching event listeners — and the browser re-renders the page to match.

== compares after converting types (so 5 == "5" is true), while === requires the same value AND the same type (5 === "5" is false). Loose equality has famously confusing rules — use === and !== everywhere unless you have a specific reason not to.

JavaScript is single-threaded, so long-running work (timers, fetches, events) is handled by the browser and its callbacks are queued. The event loop moves queued callbacks onto the call stack only when it is empty, with promise callbacks (microtasks) taking priority over timers. It is why asynchronous code never freezes the page.

A Promise is an object representing a future value — pending, fulfilled or rejected — that can be chained and combined (Promise.all). async/await is syntax sugar over promises that lets asynchronous code read top-to-bottom with ordinary try/catch error handling.

Yes. Frameworks are built on exactly these fundamentals — components are functions, state is your variable discipline, effects are events, rendering is DOM work. Developers who learn vanilla first understand what the framework is doing and debug far faster.

Type out the examples instead of copying, build small end-to-end projects (expense tracker, weather app, quiz), handle every UI state (loading, empty, success, error), and revisit fundamentals while reading real code. Consistency beats volume — a little most days outpaces occasional marathons.