Events — Listeners, Forms & Delegation
Listening for Events
const btn = document.querySelector("#save");
btn.addEventListener("click", () => {
console.log("saved!");
});Common events: click, input (every keystroke), change (after editing finishes), submit, keydown, mouseover, scroll, DOMContentLoaded. Never use inline onclick="" attributes — keep behaviour in JS files.
The Event Object
Your handler receives an object describing what happened:
input.addEventListener("input", (e) => {
console.log(e.target.value); // e.target = the element that fired
});
document.addEventListener("keydown", (e) => {
if (e.key === "Escape") closeModal();
if (e.ctrlKey && e.key === "s") {
e.preventDefault(); // stop the browser's save dialog
save();
}
});Forms — the preventDefault Pattern
Browsers reload the page on form submit. Intercept it:
const form = document.querySelector("#signup");
form.addEventListener("submit", (e) => {
e.preventDefault(); // stop the reload
const data = new FormData(form);
const email = data.get("email");
if (!email.includes("@")) {
showError("Enter a valid email");
return;
}
submitToServer({ email });
});Listen on the form’s submit, not the button’s click — submit also fires on Enter.
Bubbling — Events Travel Up
A click on a button inside a card inside the body fires on all three, innermost first. That’s bubbling, and it enables the most useful event pattern of all:
Event Delegation
Instead of attaching 100 listeners to 100 list items, attach one to their parent and check what was clicked:
const list = document.querySelector("#todo-list");
list.addEventListener("click", (e) => {
const deleteBtn = e.target.closest(".delete");
if (deleteBtn) {
deleteBtn.closest("li").remove();
}
});Delegation works for elements added later too — dynamic lists just work. This is the pattern frameworks use internally.
Removing Listeners & Options
function onScroll() { ... }
window.addEventListener("scroll", onScroll);
window.removeEventListener("scroll", onScroll); // must be the SAME function
btn.addEventListener("click", once, { once: true }); // auto-removes after one run