Step 07 of 21
The DOM & events
Find, read, change and create page elements with JavaScript, and respond to clicks, typing and form submissions.
HTML and CSS describe a page; JavaScript makes it react. Every interactive feature you have used – a menu that opens, a form that checks your input, a list that filters as you type – comes down to two skills: changing the page through the DOM, and running code when something happens through events. This lesson teaches both, finishing with event delegation, the technique that keeps interactive lists simple.
What the DOM is
When the browser parses HTML, it builds the Document Object Model: a tree of JavaScript objects, one per element, text run and comment. This HTML:
<ul id="menu">
<li class="item">Home</li>
<li class="item active">Shop</li>
</ul>becomes a tree you can walk and change:
ul#menu
├── li.item → text "Home"
└── li.item.active → text "Shop"Two consequences matter. First, the DOM is not your HTML file: the browser fixes errors while parsing (closing unclosed tags, adding a missing <tbody>), and your JavaScript changes the DOM, never the file. DevTools' Elements panel shows the live DOM. Second, everything starts from the global document object, which represents the whole page.
Your script needs the elements to exist before it can use them. Load it with defer, which runs it after the document has been parsed:
<script src="/app.js" defer></script>Finding elements
querySelector takes any CSS selector and returns the first matching element, or null. querySelectorAll returns all matches as a NodeList:
const menu = document.querySelector("#menu");
const active = document.querySelector("#menu .active");
const items = document.querySelectorAll(".item");
items.length; // 2
items.forEach((item) => console.log(item.textContent));
const asArray = [...items]; // spread into a real array for map/filterYou can also search inside an element, which is faster and avoids matching elsewhere on the page: menu.querySelectorAll("li"). To go up the tree, element.closest(selector) returns the nearest ancestor (or the element itself) that matches.
getElementById("menu") is an older, equivalent way to find one element by id.
Reading and changing elements
Text and HTML
const heading = document.querySelector("h1");
heading.textContent; // read the text
heading.textContent = "New title"; // replace it – any HTML is shown literallyinnerHTML reads and writes markup as a string. It is convenient for trusted, static snippets, but never put user-provided text into it: a "name" like <img src=x onerror="stealCookies()"> would be parsed and executed. That attack is called cross-site scripting (XSS). Use textContent for anything that came from a user or a server.
Attributes and properties
Elements expose most HTML attributes as properties:
const link = document.querySelector("a");
link.href; // "https://example.com/about" – resolved to an absolute URL
link.getAttribute("href"); // "/about" – exactly as written in the HTML
const input = document.querySelector("#email");
input.value; // what the user has typed right now
input.disabled = true;The difference matters for form fields: input.value is the current value, while getAttribute("value") is the initial value from the HTML.
Classes, data and visibility
Change appearance by toggling classes and let CSS do the styling:
const card = document.querySelector(".card");
card.classList.add("selected");
card.classList.remove("selected");
card.classList.toggle("expanded"); // add if missing, remove if present
card.classList.contains("expanded"); // true or falsedata-* attributes store small pieces of data on elements, and dataset reads them with camelCase names:
<button class="add" data-product-id="42">Add to basket</button>document.querySelector(".add").dataset.productId; // "42" – always a stringTo hide and show elements, set the hidden property (panel.hidden = true). You can also set inline styles with element.style.color = "red", but prefer classes: they keep styling in CSS and are easy to undo.
Creating and removing elements
To add content, create elements, fill them in, and insert them:
const list = document.querySelector("#results");
const products = [
{ id: 1, name: "Socks" },
{ id: 2, name: "Cap" },
];
const items = products.map((product) => {
const li = document.createElement("li");
li.textContent = product.name;
li.dataset.id = product.id;
return li;
});
list.replaceChildren(...items); // remove everything inside, then add these| Method | Effect |
|---|---|
parent.append(a, b) | Insert at the end of parent (elements or strings) |
parent.prepend(a) | Insert at the start of parent |
el.before(a) / el.after(a) | Insert as a sibling of el |
el.remove() | Remove el from the page |
parent.replaceChildren(...nodes) | Replace all of parent's children; with no arguments, empty it |
Building all the elements first and inserting them with a single call, as above, keeps the update in one place and lets the browser process the change in one go.
Events
An event is a signal that something happened: a click, a key press, a form submission, the page finishing loading. You subscribe with addEventListener:
const button = document.querySelector("#like");
let likes = 0;
button.addEventListener("click", (event) => {
likes += 1;
button.textContent = `Liked ${likes} times`;
});The handler receives an event object describing what happened. Its most useful members:
event.target– the element where the event started (the innermost element clicked).event.currentTarget– the element the listener is attached to.event.preventDefault()– cancel the browser's default action, such as following a link or submitting a form.event.key– for keyboard events, the key pressed ("Enter","Escape","a").
Events you will use most
| Event | Fires when |
|---|---|
click | An element is activated by mouse, touch, or Enter/Space on a button |
input | A field's value changes, on every keystroke |
change | A field's value is committed (on blur for text, immediately for checkboxes and selects) |
submit | A form is submitted, by button or by pressing Enter in a field |
keydown | A key is pressed |
focusin / focusout | An element gains or loses focus (these bubble; focus and blur do not) |
DOMContentLoaded | The HTML has been fully parsed |
Handling forms
Listen for submit on the form, not click on the button, so pressing Enter works too. FormData collects every named field:
const form = document.querySelector("#signup");
form.addEventListener("submit", (event) => {
event.preventDefault(); // stay on the page instead of navigating
const data = Object.fromEntries(new FormData(form));
console.log(data); // { email: "[email protected]", plan: "pro" }
form.reset();
});Built-in validation still runs first: the submit event only fires once required fields and patterns are satisfied.
Removing listeners
removeEventListener needs the same function that was added, so you cannot remove an anonymous arrow function. Keep a reference, or pass { once: true } for a listener that should run only once:
function onKey(event) {
if (event.key === "Escape") closeDialog();
}
document.addEventListener("keydown", onKey);
document.removeEventListener("keydown", onKey);
button.addEventListener("click", showWelcome, { once: true });Propagation and event delegation
When you click a <span> inside a <button> inside a <li>, which element "was clicked"? All of them. An event travels in three phases:
- Capture – from
windowdown through each ancestor to the target. - Target – at the element itself.
- Bubble – back up from the target through every ancestor to
window.
Listeners run in the bubble phase by default, so a listener on the <ul> hears clicks on every <li> inside it. event.stopPropagation() stops the journey, but use it rarely: it silently breaks any other code listening further up.
Here is the whole journey of one click on a remove button inside a list item. Solid arrows going down are the capture phase, the note on the button is the target phase, and the dashed arrows going back up are the bubble phase, where ordinary listeners run.
Event delegation uses bubbling to handle events for many children with one listener on their parent:
<ul id="basket">
<li data-id="1">Socks <button class="remove">Remove</button></li>
<li data-id="2">Cap <button class="remove">Remove</button></li>
</ul>const basket = document.querySelector("#basket");
basket.addEventListener("click", (event) => {
const button = event.target.closest(".remove");
if (!button) return; // the click was not on a remove button
const item = button.closest("li");
console.log("Removing", item.dataset.id);
item.remove();
});closest matters because event.target might be an element inside the button, such as an icon. Delegation has two big advantages: items added later work automatically, with no new listeners, and a list of 1,000 items needs one listener instead of 1,000.
Practice
Build a to-do list in one HTML file with a <script> at the end of the body (or a deferred script):
- A form with a text input and an "Add" button. Submitting adds a new item and clears the input. Ignore empty or whitespace-only entries.
- Each item shows its text, a checkbox to mark it done (strike through the text using a
.doneclass), and a "Delete" button. - A counter shows "N items left", updating after every change.
- Use one click/change listener on the list for all items (event delegation), and never use
innerHTMLwith the user's text.
Show solution
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>To-do</title>
<style>
.done span { text-decoration: line-through; opacity: 0.6; }
</style>
</head>
<body>
<h1>To-do</h1>
<form id="new-todo">
<label for="todo-text">New task</label>
<input id="todo-text" name="text" autocomplete="off">
<button type="submit">Add</button>
</form>
<ul id="todos"></ul>
<p id="count">0 items left</p>
<script>
const form = document.querySelector("#new-todo");
const input = document.querySelector("#todo-text");
const list = document.querySelector("#todos");
const count = document.querySelector("#count");
function updateCount() {
const left = list.querySelectorAll("li:not(.done)").length;
count.textContent = `${left} ${left === 1 ? "item" : "items"} left`;
}
function createTodo(text) {
const li = document.createElement("li");
const label = document.createElement("label");
const checkbox = document.createElement("input");
checkbox.type = "checkbox";
const span = document.createElement("span");
span.textContent = text; // safe: never parsed as HTML
label.append(checkbox, " ", span);
const del = document.createElement("button");
del.type = "button";
del.className = "delete";
del.textContent = "Delete";
li.append(label, " ", del);
return li;
}
form.addEventListener("submit", (event) => {
event.preventDefault();
const text = input.value.trim();
if (!text) return;
list.append(createTodo(text));
form.reset();
input.focus();
updateCount();
});
// Delegation: checkbox changes bubble up to the list
list.addEventListener("change", (event) => {
if (event.target.type !== "checkbox") return;
event.target.closest("li").classList.toggle("done", event.target.checked);
updateCount();
});
// Delegation: delete clicks bubble up to the list
list.addEventListener("click", (event) => {
const del = event.target.closest(".delete");
if (!del) return;
del.closest("li").remove();
updateCount();
});
</script>
</body>
</html>classList.toggle("done", checked) with a second argument adds the class when checked is true and removes it when false, keeping the class in sync with the checkbox. The change event is used for the checkbox because it reliably fires when the box is toggled by mouse or keyboard. The counter is recomputed from the DOM rather than tracked separately, so it can never drift out of sync.
Key takeaways
- The DOM is the browser's live tree of objects built from your HTML; JavaScript changes the DOM, not the file.
- Find elements with
querySelector,querySelectorAllandclosest; expectnullwhen nothing matches. - Use
textContentfor text,classListfor styling,datasetfor small data – and never put user input ininnerHTML. - Create elements with
createElement, then insert them withappend,before,afterorreplaceChildren. addEventListenergives you an event object; callpreventDefault()to stop default actions like form submission.- Events bubble up to ancestors, so one delegated listener with
closestcan handle every item in a list, including future ones.
Check yourself
Quiz
0/4 correct · 4 left
1.A user types <b>Hi</b> into a comment box. Which line displays it safely, exactly as typed?
2.A click listener is attached to a <ul>. The user clicks an <svg> icon inside a <button> inside an <li>. What is event.currentTarget?
3.Your form's submit handler runs, but the page reloads immediately and your changes vanish. What is missing?
4.Why does event delegation work for list items that are added after the listener was attached?
Concepts in this lesson
One-page notes on each idea, for review or a quick lookup later.
Finished reading?
Mark it complete to fill in your roadmap.