DOM, Events and Forms · Lesson 35 of 40
JavaScript Events and Event Delegation
An event is something that happens on the page: a click, a key press, a form submission, the page finishing loading. JavaScript reacts by attaching listeners – functions that run when the event fires. Understanding how events travel through the DOM leads to event delegation, a technique that keeps code fast and simple – and a classic interview question.
In this lesson
addEventListenerand the event object- Bubbling and capturing
- Event delegation
preventDefault, keyboard events, options and removing listeners
Listening for events
<button id="like-btn" type="button">👍 Like <span class="count">0</span></button>
const button = document.querySelector("#like-btn");
let likes = 0;
button.addEventListener("click", (event) => {
likes++;
button.querySelector(".count").textContent = likes;
button.classList.toggle("liked", likes > 0);
console.log(event.type, event.target.tagName, event.currentTarget.id);
});
Each click updates the count. Clicking exactly on the number logs click SPAN like-btn: event.target is the element actually clicked, while event.currentTarget is the element the listener is attached to.
| Common events | Fires when |
|---|---|
click, dblclick | an element is clicked (also Enter/Space on buttons) |
input, change | a field's value changes (on every keystroke / when committed) |
submit | a form is submitted |
keydown, keyup | a key is pressed / released |
focus, blur | an element gains / loses focus |
pointerdown, pointermove | mouse, pen or touch interaction |
scroll, resize | the page scrolls / the window resizes |
DOMContentLoaded, load | HTML parsed / everything (images too) loaded |
Avoid inline handlers
<button onclick="like()"> mixes behaviour into HTML, allows only one handler, needs global functions and is blocked by strict Content Security Policies. Use addEventListener.
Bubbling and capturing
When you click an element, the event doesn't only fire there. It travels down from window to the target (capturing), fires on the target, then travels back up through every ancestor (bubbling). Listeners run during bubbling by default.
A click on a button inside li inside ul
- Capture phasewindow → document → … → ul → li
- Target phasethe button itself
- Bubble phaseli → ul → body → document → window
document.querySelector("ul").addEventListener("click", () => console.log("ul (bubble)"));
document.querySelector("ul").addEventListener("click", () => console.log("ul (capture)"), { capture: true });
document.querySelector("li").addEventListener("click", () => console.log("li"));
document.querySelector("button").addEventListener("click", () => console.log("button"));
// Clicking the button logs: ul (capture), button, li, ul (bubble)
event.stopPropagation() stops the event travelling further. Use it sparingly – other code (analytics, "click outside to close" menus) often relies on events bubbling.
Event delegation
Instead of attaching a listener to every item, attach one listener to their common parent and check event.target. It works for items added later, too:
<ul id="todo-list">
<li data-id="1"><span>Revise closures</span> <button class="done">✓</button> <button class="delete">✕</button></li>
<li data-id="2"><span>Practise SQL joins</span> <button class="done">✓</button> <button class="delete">✕</button></li>
</ul>
const list = document.querySelector("#todo-list");
list.addEventListener("click", (event) => {
const button = event.target.closest("button"); // the clicked button, if any
if (!button || !list.contains(button)) return;
const item = button.closest("li");
const id = item.dataset.id;
if (button.matches(".delete")) {
item.remove();
console.log("Deleted", id);
} else if (button.matches(".done")) {
item.classList.toggle("completed");
console.log("Toggled", id);
}
});
Interview angle: why delegation?
- Performance – one listener instead of hundreds.
- Dynamic content – new items work without attaching new listeners.
- Less memory and cleanup – nothing to remove when items are deleted.
It works because events bubble; closest() finds the relevant element even when the click lands on a child (an icon inside the button).
preventDefault
Some events have a built-in browser action: a link navigates, a form submits and reloads the page, a key types a character. event.preventDefault() cancels that action so your code can take over:
document.querySelector("#search-form").addEventListener("submit", async (event) => {
event.preventDefault(); // no page reload
const query = new FormData(event.currentTarget).get("q").trim();
if (query) await showResults(query);
});
document.querySelector(".external-warning").addEventListener("click", (event) => {
if (!confirm("You are leaving LearnFrenzy. Continue?")) event.preventDefault();
});
Keyboard events
document.addEventListener("keydown", (event) => {
// Ctrl+K or Cmd+K opens search
if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === "k") {
event.preventDefault();
document.querySelector("#search").focus();
}
if (event.key === "Escape") closeModal();
});
Use event.key ("Enter", "Escape", "ArrowUp", "a") – the older keyCode is deprecated.
Listener options and cleanup
// Runs only once, then removes itself
button.addEventListener("click", showWelcomeTour, { once: true });
// passive: promise not to call preventDefault – lets the browser scroll smoothly
window.addEventListener("scroll", updateProgressBar, { passive: true });
// Remove a listener: pass the SAME function reference
function onResize() { console.log(window.innerWidth); }
window.addEventListener("resize", onResize);
window.removeEventListener("resize", onResize);
// Remove many listeners at once with an AbortController
const controller = new AbortController();
window.addEventListener("resize", onResize, { signal: controller.signal });
document.addEventListener("keydown", onKey, { signal: controller.signal });
controller.abort(); // both removed – handy when a component is destroyed
Common mistake: removing an anonymous function
el.removeEventListener("click", () => {...}) does nothing – it's a new function, not the one you added. Keep a reference, or use { signal }.
Practice
Try it yourself
Build a character counter: as the user types in a
<textarea maxlength="200">, show "n/200" below it and add awarnclass after 180.Show solution
const area = document.querySelector("#bio"); const counter = document.querySelector("#bio-count"); area.addEventListener("input", () => { const n = area.value.length; counter.textContent = `${n}/200`; counter.classList.toggle("warn", n > 180); });
Quick check
event.target vs event.currentTarget?
target is the element where the event started; currentTarget is the element whose listener is running.
What is event delegation?
Handling events for many children with one listener on a parent, using bubbling and event.target.
How do you stop a form from reloading the page?
Call event.preventDefault() in the submit handler.
Summary
- addEventListener attaches handlers; the event object describes what happened.
- Events capture down and bubble up; delegation uses bubbling for one efficient listener.
- preventDefault cancels browser actions; use once, passive and signal for clean listeners.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.