An event is the browser telling your code that something happened: a click, a key press, a form submission, the page finishing loading. Listening for events is what turns a static document into an interface.
Learning objectives
- Attach handlers with
addEventListener. - Read the event object and stop default browser behaviour.
- Understand bubbling, and use delegation instead of one listener per element.
- Handle form, keyboard and input events correctly.
1. addEventListener
const button = document.querySelector("#save");
button.addEventListener("click", (event) => {
console.log("clicked", event.target);
});
event.target is the element that fired the event; event.currentTarget is the element the listener is attached to. They differ when events bubble.
2. Forms: preventDefault
A <form> submits to the server and reloads the page by default. Single-page apps intercept it:
const form = document.querySelector("#signup");
form.addEventListener("submit", (event) => {
event.preventDefault();
const data = new FormData(form);
const email = String(data.get("email") ?? "").trim();
if (!email.includes("@")) {
document.querySelector("#error").textContent = "Enter a valid email.";
return;
}
console.log({ email });
});
Always call event.preventDefault() before any await; after an await the event may already have been handled by the browser.
3. Input and keyboard events
const search = document.querySelector("#search");
search.addEventListener("input", (event) => {
console.log(event.target.value); // fires on every keystroke
});
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") closeDialog();
if (event.key === "Enter" && event.ctrlKey) submit();
});
Use input for live values. change only fires when the field loses focus, which feels broken in a search box.
4. Bubbling and delegation
Events bubble from the target up through its ancestors. That means one listener on a list can handle clicks on every item:
const list = document.querySelector("#tasks");
list.addEventListener("click", (event) => {
const button = event.target.closest("button[data-action]");
if (!button) return;
const item = button.closest("li");
if (button.dataset.action === "delete") item.remove();
if (button.dataset.action === "done") item.classList.toggle("done");
});
This works for items added after the listener was attached — which is exactly why delegation is the standard pattern for dynamic lists.
5. Removing listeners and cleanup
function onScroll() { /* ... */ }
window.addEventListener("scroll", onScroll);
// later
window.removeEventListener("scroll", onScroll);
The function reference must be identical. removeEventListener("scroll", () => {}) does nothing.
Common mistakes
- Forgetting
preventDefaultin a submit handler. - Attaching a listener inside a loop to every item instead of using delegation.
- Reading
event.target.valuewhen the target is a child element — useevent.currentTarget.value.
Practice
Write the character counter and the delegated to-do list described below. Both are small, and both patterns appear in every real application.