On this page
Events and delegation
Event model
JavaScript uses an event system to respond to user interactions and other occurrences in the browser. Events propagate in three phases:
- Capture — From
windowdownward to the element - Target — The element that originated the event
- 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 removedpassive: true— Indicates it will not callpreventDefault(), improving performance on scroll and touch eventscapture: 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:
submiton forms — Prevents page reloadclickon links — Prevents navigationkeydown— 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
- Add an event with options: Create an HTML button and register a
clickevent with the option{ once: true }. Verify that the handler only executes once. - Capture a form with preventDefault: Create a form with a
namefield and a submit button. UseaddEventListener('submit', ...)withe.preventDefault()to capture the data withFormDataand log it to the console without reloading the page. - Implement event delegation: Create a
<ul>list with several<li>elements that contain a delete button. Register a single listener on the<ul>and usee.target.closest('.btn-delete')to detect clicks and remove the corresponding<li>.
In the next lesson we will learn about promises and asynchronous programming.
// === 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');
}
});
// === 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
Sign in to track your progress