20 minLesson 18 of 35
DOM & Browser APIs
Event Listeners & Event Delegation
Event Listeners & Event Delegation
Events are how web pages respond to user actions — clicks, keystrokes, form submissions, mouse movements. Understanding how events work under the hood, especially event bubbling and delegation, is what separates junior from senior web developers.
Adding Event Listeners
const button = document.querySelector("#submit");
// addEventListener is the modern approach
button.addEventListener("click", function(event) {
console.log("Clicked!", event);
});
// Arrow function shorthand
button.addEventListener("click", (e) => handleClick(e));
// Separate handler (best for removing listeners later)
function handleClick(event) {
console.log("Button clicked");
}
button.addEventListener("click", handleClick);
// Remove a listener — you need the exact same function reference
button.removeEventListener("click", handleClick);
The Event Object
button.addEventListener("click", (event) => {
event.type; // "click"
event.target; // the element that was clicked
event.currentTarget; // the element the listener is on
event.timeStamp; // when it happened (milliseconds)
// Mouse events
event.clientX; // cursor X relative to viewport
event.clientY; // cursor Y relative to viewport
event.button; // 0=left, 1=middle, 2=right
// Keyboard events
event.key; // "Enter", "a", "ArrowUp"
event.code; // "KeyA", "Enter" (physical key)
event.shiftKey; // modifier key states
event.ctrlKey;
event.altKey;
event.metaKey; // Cmd on Mac, Windows key on PC
// Prevent default browser behavior
event.preventDefault(); // stop form submit, stop link navigation
// Stop event from bubbling up
event.stopPropagation();
});
Common Event Types
// Mouse
el.addEventListener("click", handler);
el.addEventListener("dblclick", handler);
el.addEventListener("mousedown", handler);
el.addEventListener("mouseup", handler);
el.addEventListener("mouseover", handler); // fires on child elements too
el.addEventListener("mouseenter", handler); // only fires on the element itself
el.addEventListener("mouseleave", handler);
el.addEventListener("mousemove", handler);
// Keyboard (on document or input elements)
document.addEventListener("keydown", handler); // fires while held
document.addEventListener("keypress", handler); // deprecated
document.addEventListener("keyup", handler); // fires on release
// Form
form.addEventListener("submit", handler);
input.addEventListener("change", handler); // after value changes & loses focus
input.addEventListener("input", handler); // every keystroke
input.addEventListener("focus", handler);
input.addEventListener("blur", handler); // loses focus
// Window/document
window.addEventListener("load", handler); // page fully loaded
document.addEventListener("DOMContentLoaded", handler); // DOM ready (before images)
window.addEventListener("resize", handler);
window.addEventListener("scroll", handler);
window.addEventListener("beforeunload", handler); // user leaving page
Event Bubbling
Events bubble up through the DOM — from the target element all the way to the document:
// HTML: <div id="outer"><div id="inner"><button>Click</button></div></div>
document.querySelector("button").addEventListener("click", () => {
console.log("button"); // fires first
});
document.querySelector("#inner").addEventListener("click", () => {
console.log("inner"); // fires second
});
document.querySelector("#outer").addEventListener("click", () => {
console.log("outer"); // fires third
});
// Clicking the button logs: "button", "inner", "outer"
// Stop bubbling
button.addEventListener("click", (e) => {
e.stopPropagation(); // inner and outer don't receive the event
handleButtonClick();
});
Event Delegation
Instead of adding listeners to every child, add ONE listener to a parent:
// ❌ Bad — listener on every item (slow, doesn't work for dynamic items)
document.querySelectorAll(".list-item").forEach(item => {
item.addEventListener("click", handleItemClick);
});
// ✅ Good — one listener on the parent
const list = document.querySelector(".list");
list.addEventListener("click", (e) => {
// Find the closest list-item ancestor of what was clicked
const item = e.target.closest(".list-item");
if (!item) return; // click was on the list but not an item
handleItemClick(item);
});
Why delegation is powerful:
- Works for dynamically added elements
- Uses far less memory
- One place to handle all item events
// Full example: delegated actions
const table = document.querySelector("#users-table");
table.addEventListener("click", (e) => {
const row = e.target.closest("tr[data-user-id]");
if (!row) return;
const userId = row.dataset.userId;
if (e.target.matches(".edit-btn")) editUser(userId);
if (e.target.matches(".delete-btn")) deleteUser(userId);
if (e.target.matches(".view-btn")) viewUser(userId);
});
Preventing Default Behavior
// Prevent form submission
form.addEventListener("submit", (e) => {
e.preventDefault(); // stops page reload
validateAndSubmit(form);
});
// Prevent link navigation
link.addEventListener("click", (e) => {
e.preventDefault();
router.navigate(link.href); // use custom router instead
});
// Prevent context menu on right-click
canvas.addEventListener("contextmenu", (e) => e.preventDefault());
Once and Passive Options
// Options object as third argument
button.addEventListener("click", handler, {
once: true, // auto-removes after first fire
passive: true, // tells browser the handler won't call preventDefault (perf boost)
capture: true // capture phase instead of bubble phase
});
// Passive listeners are especially important for scroll/touch
window.addEventListener("scroll", onScroll, { passive: true }); // 60fps!
window.addEventListener("touchstart", onTouch, { passive: false }); // can preventDefault
Performance: Throttle and Debounce
Some events fire constantly (scroll, resize, mousemove). Throttle/debounce limits how often your handler runs:
// Debounce — wait until the user stops
function debounce(fn, delay) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// Only search after user stops typing for 300ms
const search = debounce((query) => fetchResults(query), 300);
input.addEventListener("input", (e) => search(e.target.value));
// Throttle — fire at most once per interval
function throttle(fn, limit) {
let inThrottle;
return (...args) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
// Handle scroll at most 60fps
const onScroll = throttle(() => updateScrollProgress(), 16);
window.addEventListener("scroll", onScroll, { passive: true });
Next lesson: Forms — validation, submission, and handling all input types in JavaScript.
📱
Get Notes Free →Get this course's notes on Telegram!
Free cheat sheets, summaries & practice exercises