The idea
A web page mostly waits. Nothing happens until the user does something: clicks, types, submits, presses a key. Each of those is an event, and JavaScript runs your code when one happens through an event listener. "When this button is clicked, add the item to the cart" is the basic sentence of every interactive page.
As in the previous module, each example builds its own small page. button.click() and dispatchEvent simulate what a user would do, so the examples run without anyone clicking.
addEventListener
A page that only changes when it first loads isn't very interactive. You want things to happen when the customer does something: clicks "Add to cart", types in the search box, submits a form. You tell an element "when this happens, run this function":
document.body.innerHTML = `<button id="add">Add to cart</button> <span id="count">0</span>`;
const button = document.querySelector("#add");
const count = document.querySelector("#count");
button.addEventListener("click", () => {
count.textContent = Number(count.textContent) + 1;
console.log("Cart:", count.textContent);
});
button.click();
button.click();
Cart: 1
Cart: 2
element.addEventListener(type, handler) registers a function to run every time that event happens on that element. Pass the function itself; don't call it: addEventListener("click", addToCart), not addToCart().
You'll see onclick="..." attributes in old tutorials. Avoid them: they mix JavaScript into HTML, allow only one handler, and are blocked by many security policies.
The event object
Every handler receives an event object describing what happened:
document.body.innerHTML = `<button class="buy" data-id="7">Buy <strong>now</strong></button>`;
const buy = document.querySelector(".buy");
buy.addEventListener("click", (event) => {
console.log(event.type);
console.log(event.currentTarget.dataset.id); // the element the listener is on
console.log(event.target.tagName); // the element actually clicked
});
buy.querySelector("strong").click();
click
7
STRONG
event.currentTarget is the element you attached the listener to; event.target is the innermost element that was actually clicked (here, the <strong> inside the button). The difference matters for the delegation pattern below.
Typing: the input event
A search box that updates the results as you type feels much faster than one where you have to press a button. The input event fires every time the field's value changes. The last two lines here simulate someone typing, since there's no keyboard in the example:
document.body.innerHTML = `<input id="search" aria-label="Search"> <p id="echo"></p>`;
const search = document.querySelector("#search");
const echo = document.querySelector("#echo");
search.addEventListener("input", () => {
echo.textContent = `Searching for "${search.value.trim()}"`;
});
search.value = " gayo ";
search.dispatchEvent(new Event("input"));
console.log(echo.textContent);
Searching for "gayo"
input fires on every change to a field's value (each keystroke, paste or delete); change fires when the user commits a change (leaves the field, picks an option). Read the current value from element.value, which is always a string.
Forms: submit and preventDefault
When someone submits a form, the browser's default behaviour is to send the data to the server and load a new page. For a newsletter sign-up, that's jarring: you'd rather show "Thanks!" right there on the same page. You catch the submit event and stop the default behaviour:
document.body.innerHTML = `
<form id="newsletter">
<input name="email" value="[email protected]" aria-label="Email">
<button>Subscribe</button>
</form>
<p id="message"></p>`;
const form = document.querySelector("#newsletter");
form.addEventListener("submit", (event) => {
event.preventDefault(); // stay on this page
const data = new FormData(form);
document.querySelector("#message").textContent = `Thanks, ${data.get("email")}!`;
console.log("prevented:", event.defaultPrevented);
});
form.requestSubmit();
console.log(document.querySelector("#message").textContent);
prevented: true
Thanks, [email protected]!
- Listen for
submiton the form, notclickon the button. Then pressing Enter in a field works too. - By default, submitting a form loads a new page.
event.preventDefault()stops that, so JavaScript can handle the data (validate it, send it withfetch, show a message). new FormData(form)collects every named field;.get(name)reads one.
preventDefault works for other defaults too: following a link, checking a checkbox, scrolling with arrow keys.
One listener for many elements: delegation
A list of 50 products with 50 "Add" buttons doesn't need 50 listeners. Events bubble: after firing on the clicked element, they travel up through each parent. So one listener on the container can handle them all:
document.body.innerHTML = `
<ul id="products">
<li>Arabica Gayo <button data-id="1">Add</button></li>
<li>Toraja Kalosi <button data-id="2">Add</button></li>
</ul>`;
const list = document.querySelector("#products");
const added = [];
list.addEventListener("click", (event) => {
const button = event.target.closest("button[data-id]");
if (!button) return; // a click elsewhere in the list
added.push(Number(button.dataset.id));
});
list.querySelectorAll("button")[1].click();
list.querySelector("li").click(); // clicking the text, not a button
list.querySelectorAll("button")[0].click();
console.log(added);
[2, 1]
event.target.closest(selector) walks up from the clicked element to the nearest match (or null). Delegation also works for elements added later, such as products rendered after a search, which separate listeners wouldn't.
Keyboard events
document.body.innerHTML = `<div id="dialog">Added to cart ✓</div>`;
document.addEventListener("keydown", (event) => {
if (event.key === "Escape") {
document.querySelector("#dialog").hidden = true;
console.log("Dialog closed");
}
});
document.dispatchEvent(new KeyboardEvent("keydown", { key: "Escape" }));
Dialog closed
event.key holds the key's name: "Enter", "Escape", "ArrowDown", "a".
el.addEventListener("click" | "input" | "submit" | "keydown", handler); the handler gets an event object (target, currentTarget, key). Handle forms on submit with preventDefault(). For many similar elements, use one delegated listener with event.target.closest(...).
Why this matters for clean code
Keep handlers short: read what happened from the event, call a well-named function that does the work (addToCart(id)), then update the page. Business logic inside a handler can't be reused or tested; in a function, it can. Delegation keeps the number of listeners constant no matter how big the page grows.
Recap
addEventListener(type, fn): pass the function, don't call it. Avoidonclickattributes.event.target(what was clicked) vsevent.currentTarget(where the listener is).inputfor live typing,changefor committed changes;element.valueis a string.- Forms: listen for
submit, callevent.preventDefault(), read fields withFormData. - Delegation: one listener on a parent +
event.target.closest(...). keydownwithevent.key.