On this page

Events and delegation

12 min read TextCh. 3 — DOM and events

Event model

JavaScript uses an event system to respond to user interactions and other occurrences in the browser. Events propagate in three phases:

  1. Capture — From window downward to the element
  2. Target — The element that originated the event
  3. Bubbling — From the element upward to window

By default, listeners are executed during the bubbling phase.

addEventListener

It is the standard way to register an event handler:

element.addEventListener(type, handler, options);

Common event types

Event Fires when...
click The element is clicked
dblclick Double click
mouseenter / mouseleave The cursor enters/leaves the element
keydown / keyup A key is pressed/released
input An input value changes
change An input loses focus after changing
submit A form is submitted
scroll The page is scrolled
focus / blur An element gains/loses focus

Useful options

  • once: true — The handler executes only once and is automatically removed
  • passive: true — Indicates it will not call preventDefault(), improving performance on scroll and touch events
  • capture: true — Listens during the capture phase instead of bubbling

The Event object

Each handler receives an Event object with information about the event:

Property Description
event.target Element that originated the event
event.currentTarget Element with the listener
event.type Event type ('click', 'keydown', etc.)
event.key Key pressed (on keyboard events)
event.preventDefault() Prevents the default behavior
event.stopPropagation() Stops propagation

preventDefault()

Some events have default browser behavior. preventDefault() cancels it:

  • submit on forms — Prevents page reload
  • click on links — Prevents navigation
  • keydown — Prevents browser shortcuts

Event delegation

Instead of adding a listener to each individual element, add a single listener to the parent container. When an event occurs on a child, it bubbles up to the parent where you catch it.

Advantages of delegation

  • Performance — A single listener instead of hundreds
  • Dynamic elements — Works with elements added after registering the listener
  • Less memory — Fewer function references

The closest() pattern

element.closest(selector) searches up the DOM tree until it finds an ancestor matching the selector. It is key for delegation because event.target may be a child of the element you are interested in (for example, an icon inside a button).


Practice

  1. Add an event with options: Create an HTML button and register a click event with the option { once: true }. Verify that the handler only executes once.
  2. Capture a form with preventDefault: Create a form with a name field and a submit button. Use addEventListener('submit', ...) with e.preventDefault() to capture the data with FormData and log it to the console without reloading the page.
  3. Implement event delegation: Create a <ul> list with several <li> elements that contain a delete button. Register a single listener on the <ul> and use e.target.closest('.btn-delete') to detect clicks and remove the corresponding <li>.

In the next lesson we will learn about promises and asynchronous programming.

closest() is your friend
The closest(selector) method searches up the DOM tree until it finds a matching ancestor. It is essential for event delegation when buttons contain icons or other child elements.
Do not use onclick in HTML
Avoid attributes like onclick="function()" in HTML. Using addEventListener separates behavior from structure, is more maintainable, and allows multiple handlers on the same element.
javascript
// === ADDING EVENTS ===
const button = document.querySelector('.btn');

// Click event
button.addEventListener('click', (event) => {
  console.log('Clicked on:', event.target);
  console.log('Type:', event.type);
});

// Event with options
button.addEventListener('click', handleClick, {
  once: true,   // executes only once
  passive: true, // improves scroll/touch performance
});

// Remove event
function handleClick(e) {
  console.log('Clicked');
}
button.addEventListener('click', handleClick);
button.removeEventListener('click', handleClick);

// Prevent default behavior
const form = document.querySelector('form');
form.addEventListener('submit', (e) => {
  e.preventDefault(); // prevents page reload
  const data = new FormData(form);
  console.log('Name:', data.get('name'));
});

// Keyboard events
document.addEventListener('keydown', (e) => {
  if (e.key === 'Escape') {
    console.log('Escape pressed');
  }
  if (e.key === 's' && (e.ctrlKey || e.metaKey)) {
    e.preventDefault();
    console.log('Ctrl+S captured');
  }
});
javascript
// === EVENT DELEGATION ===

// BAD: one listener per button
// document.querySelectorAll('.btn-delete').forEach(btn => {
//   btn.addEventListener('click', () => { ... });
// });

// GOOD: a single listener on the container
const list = document.querySelector('.todo-list');

list.addEventListener('click', (e) => {
  // Find the closest button (even if a child was clicked)
  const btnDelete = e.target.closest('.btn-delete');
  if (btnDelete) {
    const item = btnDelete.closest('.todo-item');
    const id = item.dataset.id;
    console.log('Delete item:', id);
    item.remove();
    return;
  }

  // Toggle completed
  const checkbox = e.target.closest('.todo-check');
  if (checkbox) {
    const item = checkbox.closest('.todo-item');
    item.classList.toggle('completed');
  }
});

// Works with dynamically added elements
const newItem = document.createElement('li');
newItem.className = 'todo-item';
newItem.dataset.id = '99';
newItem.innerHTML = `
  <input type="checkbox" class="todo-check">
  <span>New task</span>
  <button class="btn-delete">X</button>
`;
list.appendChild(newItem);
// The container event captures clicks on this new item