Events — Bubbling, Delegation & Custom Events
Page-কে interactive করুন — বিশ্বকে আবার build না করেই
1. addEventListener
btn.addEventListener("click", e => {
console.log("clicked", e.target);
});
// Options
btn.addEventListener("click", handler, {
once: true, // run only once, then auto-remove
capture: false, // listen during capture phase
passive: true, // promise not to call preventDefault (better scroll perf)
});
// Remove — must use the SAME function reference
btn.removeEventListener("click", handler);
2. The Three Phases — Capture, Target, Bubble
3. Event Delegation
Instead of attaching a listener to every <li>, attach one listener on the parent and use e.target to identify which child was clicked. Performance + dynamic children for free.
// Sandbox simulation of delegation
const bus = {
listeners: [],
on(target, fn) { this.listeners.push({ target, fn }); },
emit(event) {
// Event bubbles target → parent
let el = event.target;
while (el) {
this.listeners
.filter(l => l.target === el)
.forEach(l => l.fn(event));
el = el.parent;
}
}
};
const ul = { tag: "ul", parent: null };
const li = { tag: "li", text: "item 1", parent: ul };
// One listener on the parent — handles all children
bus.on(ul, e => console.log("clicked:", e.target.text));
bus.emit({ type: "click", target: li });
Real DOM equivalent:
document.querySelector("ul").addEventListener("click", e => {
const li = e.target.closest("li");
if (!li) return;
console.log("clicked:", li.textContent);
});
4. preventDefault & stopPropagation
// Prevent navigation on a link click
link.addEventListener("click", e => {
e.preventDefault();
customRoute(link.href);
});
// Don't let parent listeners see this event
btn.addEventListener("click", e => {
e.stopPropagation();
});
// Stop ALL further listeners — including siblings on the same node
e.stopImmediatePropagation();
5. Common Events
| Event | Fired On |
|---|---|
click | Buttons, links, anything clickable |
input | Every keystroke in an input/textarea |
change | After an input loses focus with a new value |
submit | Forms (call e.preventDefault to stop reload) |
keydown / keyup | Keyboard |
mouseover / mouseout | Hover |
scroll | Element scrolled — throttle or use passive: true |
DOMContentLoaded | HTML parsed, scripts not yet loaded |
load | Everything (images, CSS) loaded |
6. CustomEvent
// Sandbox-safe pattern: an EventTarget
const bus = new EventTarget();
bus.addEventListener("user:login", e =>
console.log("got user:", e.detail));
bus.dispatchEvent(new CustomEvent("user:login", {
detail: { id: 42, name: "Arif" }
}));
Real DOM elements are also EventTargets, so the same pattern works on any element.
7. Glossary (শব্দকোষ)
| Term | Meaning | বাংলায় |
|---|---|---|
| Event | An object describing a user/system action (click, input, resize). | User/system-এর কোনো action-এর object। |
addEventListener | Attaches a handler to an element for an event type. | Element-এ event-এর জন্য handler যোগ করে। |
| Capture phase | Event flowing top-down before reaching the target. | Target-এ পৌঁছানোর আগে উপর থেকে নিচে flow। |
| Bubble phase | Event flowing back up to ancestors after the target. | Target-এর পরে নিচ থেকে উপরে flow। |
event.target | The deepest element that triggered the event. | সবচেয়ে গভীর element যেখান থেকে event এসেছে। |
event.currentTarget | The element whose listener is currently running. | যে element-এর listener এখন চলছে। |
| Delegation | One listener on a parent handles many children. | Parent-এ এক listener — সব children handle করে। |
preventDefault | Cancels the browser's default action (e.g. link nav). | Browser-এর default action আটকায়। |
stopPropagation | Stops the event from reaching ancestors. | Ancestor পর্যন্ত event পৌঁছাতে দেয় না। |
CustomEvent | App-defined event with a detail payload. | App-এর নিজস্ব event — detail-এ data। |
passive | Listener promise: won't call preventDefault; better scroll perf. | preventDefault না call করার প্রতিশ্রুতি — scroll-এ দ্রুত। |
e.target = যেখান থেকে event এসেছে; e.currentTarget = যে listener এখন চলছে। Scroll/touch listener-এ { passive: true } দিন — উল্লেখযোগ্যভাবে smoother।
8. Practice Problems
- Sketch a click handler that logs the clicked element.
✨ Show Answer
btn.addEventListener("click", e => console.log(e.target)); - Prevent a form's default submit.
✨ Show Answer
form.addEventListener("submit", e => { e.preventDefault(); // ... handle in JS }); - Use delegation: handle clicks on any <li> inside <ul>.
✨ Show Answer
ul.addEventListener("click", e => { const li = e.target.closest("li"); if (!li || !ul.contains(li)) return; console.log("clicked:", li.textContent); }); - Build an EventTarget bus and emit a custom event with detail.
✨ Show Answer
a4.jsconst bus = new EventTarget(); bus.addEventListener("hi", e => console.log("got", e.detail)); bus.dispatchEvent(new CustomEvent("hi", { detail: "hello" })); - Use
{ once: true }to attach a one-time listener.✨ Show Answer
btn.addEventListener("click", run, { once: true }); - Why is event delegation faster than attaching N listeners?
✨ Show Answer
Answer: One listener replaces N listeners — less memory, less work when the DOM changes. Newly added children are handled automatically because they bubble through the same parent. With per-child listeners, each new element needs its own registration.
- Stop a click on a button from reaching the parent (showing the trade-off).
✨ Show Answer
btn.addEventListener("click", e => { e.stopPropagation(); // breaks any delegation higher up }); - Listen for keydown and log the key name.
✨ Show Answer
document.addEventListener("keydown", e => console.log(e.key)); - Build a tiny pub/sub object using EventTarget.
✨ Show Answer
a9.jsconst hub = new EventTarget(); const on = (n, f) => hub.addEventListener(n, e => f(e.detail)); const emit = (n, d) => hub.dispatchEvent(new CustomEvent(n, { detail: d })); on("chat", m => console.log("msg:", m)); emit("chat", "হ্যালো"); - Why is
passive: truerecommended for scroll handlers?✨ Show Answer
Answer: The browser knows you won't call
preventDefault, so it can start scrolling immediately without waiting for your handler — visibly smoother on touch devices. - Remove a listener after it runs three times.
✨ Show Answer
a11.jsconst hub = new EventTarget(); let n = 0; function handler() { console.log("call", ++n); if (n >= 3) hub.removeEventListener("x", handler); } hub.addEventListener("x", handler); for (let i = 0; i < 5; i++) hub.dispatchEvent(new Event("x")); - In one paragraph, explain why
preventDefaultdoesn't work on a passive listener.✨ Show Answer
Answer: A passive listener is a promise to the browser that you won't cancel the default. The browser can therefore optimize by not waiting for your handler to finish before scrolling. If you call
preventDefaultinside, the browser ignores it and emits a console warning. - Show how
e.currentTargetdiffers frome.target.✨ Show Answer
Answer:
e.targetis whatever the user actually interacted with (deepest node).e.currentTargetis the element whose listener is currently running. With delegation,currentTargetis the parent andtargetis the clicked child. - Build a runnable simulator that bubbles a "click" through three nodes.
✨ Show Answer
a14.jsconst nodes = ["target", "section", "body", "document"]; nodes.forEach(n => console.log("bubbling through:", n));
Summary — Module 26
Use addEventListener with options for once/passive. Events bubble — exploit it with delegation. preventDefault blocks the browser's default; stopPropagation blocks ancestors. Custom events ride on EventTarget for cross-component messaging.