JavaScript
DOM 1.2: Events & Live Interaction — event flow, bubbling, capturing, event delegation, FormData, Observers, layout reflow, and performance debouncing.
1. What is an Event?
An Event is a signal sent by the browser notifying your code that something just occurred — such as a button click, a keyboard stroke, scrolling the viewport, or a form submission. JavaScript listens for these events and executes handler functions in response.
Think of an event listener like a smart doorbell:
- The Event (Visitor rings the bell): A user clicks a button or types into an input.
- The Listener (
addEventListener): The sensor installed by the door tuned to the bell signal. - The Handler Function: The chime sound and notification sent to your smartphone telling you who is at the door.
2. Three Ways to Attach Events
// ❌ 1. Inline HTML attribute (Violates separation of concerns - avoid!)
// <button onclick="handleClick()">Click</button>
// ⚠️ 2. DOM Property Handler (Only supports ONE handler; overwrites previous ones)
btn.onclick = () => console.log("Handler A");
btn.onclick = () => console.log("Handler B"); // Replaces and erases Handler A!
// ✅ 3. addEventListener (Modern standard: supports multiple independent listeners & options)
btn.addEventListener("click", () => console.log("Handler A"));
btn.addEventListener("click", () => console.log("Handler B")); // Both run seamlessly!
Removing Event Listeners
To remove a listener with removeEventListener(), you must pass the exact named function reference. Anonymous functions cannot be removed because they have different references in memory:
function onBtnClick() { console.log("Clicked!"); }
// ✅ Adding and removing with named function:
btn.addEventListener("click", onBtnClick);
btn.removeEventListener("click", onBtnClick);
// 🚀 Modern Cleanup with AbortController (Cancels multiple listeners at once!):
const controller = new AbortController();
window.addEventListener("resize", () => console.log("Resized"), { signal: controller.signal });
window.addEventListener("scroll", () => console.log("Scrolled"), { signal: controller.signal });
controller.abort(); // Instantly removes both listeners simultaneously!
3. The Event Object & Targets
Every event listener automatically receives an Event object (e) containing metadata about what occurred:
btn.addEventListener("click", (e) => {
console.log("Event Type:", e.type); // "click"
console.log("Target (Clicked element):", e.target);
console.log("CurrentTarget (Attached element):", e.currentTarget);
console.log("Coordinates:", e.clientX, e.clientY);
e.preventDefault(); // Stops browser's default action (e.g. form reload, link jump)
e.stopPropagation(); // Stops event from traveling up the ancestor tree
});
e.target (The Originator)
The exact, deepest element that received the raw click or trigger (e.g., the <span> inside a button).
e.currentTarget (The Owner)
The element to which the addEventListener is currently attached (e.g., the parent <button> or <ul>).
4. Event Flow: Capturing vs Bubbling
When an element is clicked inside nested containers, the event does not just fire on that single node. It travels in three sequential phases:
1. CAPTURE PHASE: window ➔ document ➔ body ➔ container ➔ (travels DOWN to target)
2. TARGET PHASE: button (the actual clicked node)
3. BUBBLE PHASE: button ➔ container ➔ body ➔ document ➔ window (bubbles back UP)
By default, all event listeners run during the Bubbling phase. To listen in the Capture phase, pass { capture: true }.
5. Event Delegation (The Essential Performance Pattern)
The Problem: If you have a table or list with 1,000 items, attaching 1,000 separate event listeners consumes excessive memory and fails for items added dynamically in the future.
The Solution (Event Delegation): Attach ONE single listener to the parent container, and use e.target.closest("selector") to detect which child was clicked as the event bubbles up:
const list = document.createElement("ul");
["Learn JavaScript", "Master DOM", "Build React App"].forEach(text => {
const li = document.createElement("li");
li.textContent = text;
list.append(li);
});
document.body.append(list);
// ✅ ONE delegated listener manages ALL current and future list items:
list.addEventListener("click", (e) => {
const item = e.target.closest("li");
if (!item || !list.contains(item)) return; // Ignore clicks outside valid items
item.classList.toggle("completed");
console.log("Toggled task:", item.textContent);
});
// Dynamically added item works automatically without adding new listeners!
const newLi = document.createElement("li");
newLi.textContent = "Deploy to Production";
list.append(newLi);
newLi.click(); // Triggers the parent's delegated handler!
- Master DOM Selection
- Learn Event Bubbling
- Prepare for React State
6. Modern Forms & Keyboard Events
Handling Forms with FormData
const form = document.createElement("form");
form.innerHTML = `
<input name="email" value="alex@dev.io">
<input name="role" value="Developer">
<button type="submit">Submit</button>
`;
form.addEventListener("submit", (e) => {
e.preventDefault(); // Stop default browser page reload!
// Extract all form values cleanly as a plain JS object:
const formData = new FormData(form);
const data = Object.fromEntries(formData);
console.log("Submitted payload:", data); // { email: "alex@dev.io", role: "Developer" }
});
form.requestSubmit();
Keyboard Shortcuts (key vs code)
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") {
console.log("Escape pressed: Closing modal");
}
if ((e.ctrlKey || e.metaKey) && e.key.toLowerCase() === "s") {
e.preventDefault(); // Stop browser's default Save HTML dialog
console.log("Custom save triggered!");
}
});
7. Modern DOM Observers & Performance
| Observer API | Monitors | Real-World Placement Use Case |
|---|---|---|
IntersectionObserver |
Element visibility in the viewport | Lazy-loading images, infinite scrolling feeds, reveal animations |
MutationObserver |
DOM tree modifications (nodes added/removed, attributes) | Third-party widgets, chat window mutations, state trackers |
ResizeObserver |
Element dimensions and border-box sizing | Responsive charts, dynamic data grids, container queries |
Debounce vs Throttle
// 1. Debounce: Waits until activity PAUSES for X milliseconds (Search inputs, resize end)
function debounce(fn, delay = 300) {
let timer;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// 2. Throttle: Executes at most ONCE per X milliseconds (Scroll tracking, game loops)
function throttle(fn, limit = 200) {
let inThrottle = false;
return (...args) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => inThrottle = false, limit);
}
};
}
8. Hands-On Practice: Delegated Todo Manager (Bridge to React)
Build a self-contained Todo manager using **Event Delegation**: create a list <ul> that listens for clicks, toggles the done class on task items, and deletes items when their delete button (data-action="delete") is clicked.