Events
addEventListener, bubbling and capturing, delegation for dynamic lists, and forms without page reloads.
Listening
btn.addEventListener('click', event => {
console.log(event.target); // what was clicked
console.log(event.currentTarget); // where the listener lives
});
// remove requires the same function reference
function handler() {}
el.addEventListener('click', handler);
el.removeEventListener('click', handler);| Common event | Fires when |
|---|---|
click | Mouse click or keyboard activation |
input | Value changes as the user types |
change | Value committed (blur for text) |
submit | Form submitted |
keydown | Key pressed |
DOMContentLoaded | HTML parsed |
Bubbling and capturing
An event travels three phases: capture down from the root, the target, then bubble back up. Listeners default to the bubble phase.
el.addEventListener('click', fn); // bubble (default)
el.addEventListener('click', fn, true); // capture
el.addEventListener('click', fn, { once: true, passive: true });
event.stopPropagation(); // stop further travel
event.preventDefault(); // stop the default actionβ οΈ
stopPropagation breaks analytics and delegated handlers globally. Prefer checking event.target in a delegate handler instead.Event delegation
Instead of attaching a listener to every row, attach one to a stable parent and ask what was clicked. This works for elements added later and uses far less memory.
list.addEventListener('click', e => {
const btn = e.target.closest('[data-action]');
if (!btn) return; // click was elsewhere
handle(btn.dataset.action, btn.dataset.id);
});Forms without reload
form.addEventListener('submit', async e => {
e.preventDefault(); // stop navigation
const data = new FormData(form);
const payload = Object.fromEntries(data);
await fetch(form.action, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(payload)
});
});π‘
Bind to the form's
submit, not the button's click β you keep Enter-key behaviour and validation for free.FAQ
Why does my handler run twice?
Usually the listener was added twice (a re-render without cleanup), or the event bubbles into another listener. Check both.
target vs currentTarget?
target is the deepest element clicked; currentTarget is the element whose listener is executing. With delegation they routinely differ.Related
Working with the DOM Async JavaScript and fetch
Last refreshed 2026-09-17.