1. Selecting & Updating Elements
The DOM (Document Object Model) is the browser's live, in-memory representation of the page — every HTML tag becomes an object you can read from and write to with JavaScript:
// querySelector returns the FIRST match, using CSS-selector syntax
const heading = document.querySelector("h1");
const firstButton = document.querySelector(".btn");
// querySelectorAll returns ALL matches, as a NodeList (array-like, not a real array)
const allButtons = document.querySelectorAll(".btn");
console.log(allButtons.length);
const heading = document.querySelector("h1");
heading.textContent = "Updated title"; // change the text
heading.classList.add("highlight"); // add a CSS class
heading.classList.remove("hidden"); // remove a CSS class
heading.classList.toggle("active"); // add if missing, remove if present
heading.style.color = "blue"; // set an inline style directly
heading.setAttribute("data-id", "42"); // set any HTML attribute
querySelector/querySelectorAll accept the exact same
selector syntax as CSS — a tag name, a .class, an #id, or
any combination — which means anything you already know how to target in a
stylesheet, you already know how to select in JavaScript. classList is
almost always the right tool for changing appearance: toggling a CSS class is more
maintainable than setting individual style properties by hand.
textContent over innerHTML for plain text
innerHTML parses its string as actual HTML — setting it from untrusted input (like something a user typed) opens the door to injecting scripts onto your own page. textContent always inserts a plain string safely, with no HTML parsing at all.
2. Handling Events
addEventListener registers a callback — a familiar idea from every
earlier week — to run whenever a specific event happens on an element:
const button = document.querySelector("#save-btn");
button.addEventListener("click", (event) => {
console.log("Clicked!", event.target); // event.target is the element that was clicked
});
const input = document.querySelector("#search");
input.addEventListener("input", (event) => {
console.log("Current value:", event.target.value);
});
const form = document.querySelector("#signup-form");
form.addEventListener("submit", (event) => {
event.preventDefault(); // stop the browser's default full-page reload on submit
console.log("Form submitted without reloading the page");
});
The callback always receives an event object with details about
what happened — event.target is the actual element the event fired on,
and event.target.value is how you read the current text out of an input
as the user types. event.preventDefault() stops the browser's own
default behavior for that event — essential for forms, where the default is a full
page reload that would otherwise wipe out any JavaScript-driven behavior.
3. Event Delegation
Attaching a separate listener to every single item in a list works, but breaks the moment new items are added dynamically — and it's wasteful for a large list. Event delegation solves both problems by taking advantage of a behavior called event bubbling: an event fired on an element also fires on each of its ancestors, all the way up.
// HTML:
// <ul id="todo-list">
// <li>Buy milk</li>
// <li>Walk the dog</li>
// </ul>
const list = document.querySelector("#todo-list");
list.addEventListener("click", (event) => {
if (event.target.tagName === "LI") {
event.target.classList.toggle("done"); // works even for <li>s added AFTER this ran
}
});
The click actually fires on the specific <li>, then
bubbles up through its parents — including #todo-list,
where the one listener above catches it. Checking event.target tells you
exactly which child was actually clicked. Because there's only one listener on the
parent (which never gets removed or recreated), this pattern keeps working correctly
even for list items added long after the listener was first attached.
4. fetch() & a Real API
fetch makes an HTTP request and returns a Promise — everything from
Weeks 7 and 8 applies directly:
async function loadUsers() {
const list = document.querySelector("#user-list");
list.textContent = "Loading...";
try {
const response = await fetch("https://jsonplaceholder.typicode.com/users");
if (!response.ok) {
throw new Error(`Request failed: ${response.status}`);
}
const users = await response.json(); // parses the response body as JSON
list.innerHTML = ""; // clear "Loading..."
for (const user of users) {
const item = document.createElement("li");
item.textContent = user.name; // textContent -- safe, no HTML parsing
list.appendChild(item);
}
} catch (err) {
list.textContent = `Failed to load users: ${err.message}`;
}
}
loadUsers();
Two things about fetch catch people off guard: first,
await fetch(...) only rejects on a genuine network failure —
a 404 or 500 response is still a "successful" fetch as far as the Promise is
concerned, which is exactly why response.ok needs to be checked
explicitly and thrown on manually. Second, the response body has to be read
separately with await response.json() (itself another Promise) —
parsing the body is a distinct async step from getting the response headers back.
5. What Node's Runtime Adds
JavaScript the language is identical in a browser and in Node — closures, Promises, classes, all of it work exactly the same. What differs is the runtime environment surrounding the language: the set of built-in objects and APIs available to it.
// Browser-only: document, window, DOM, localStorage -- none of this exists in Node
document.querySelector("h1"); // ReferenceError in Node: document is not defined
// Node-only: direct filesystem and process access -- none of this exists in a browser
const fs = require("fs"); // (or: import fs from "node:fs")
fs.readFileSync("data.txt", "utf8");
process.env.API_KEY; // read an environment variable
A browser gives JavaScript a DOM to manipulate, but deliberately walls it off from the local filesystem and OS-level details, for obvious security reasons — a random website should never be able to read your files. Node removes the DOM entirely (there is no page to manipulate) but opens up direct filesystem, network-server, and process access instead, which is exactly why Node is used for servers and build tools rather than in-browser UI code.
6. Hands-on Exercise
Build a filterable, fetched todo list
Combine DOM updates, event delegation and a real fetch call in one small page.
Requirements:
- Create a minimal HTML page with an empty
<ul id="todo-list">and a text input#filter-input. - On page load,
fetch("https://jsonplaceholder.typicode.com/todos?_limit=15"), and render each todo as an<li>(usetextContent, and add a CSS class if the fetchedcompletedfield is true). - Use event delegation on
#todo-listso clicking any<li>toggles adoneclass on it. - Add an
inputlistener on#filter-inputthat hides any<li>whose text doesn't include the typed value (case-insensitive), usingclassListto toggle ahiddenclass rather than removing elements. - Wrap the fetch in
try/catchand show an error message in the list if the request fails.
For step 4, list.querySelectorAll("li") combined with Array.from(...) and Week 5's forEach is a clean way to loop over every item and check each one's text against the filter.
7. Knowledge Check
Four quick questions. Expand each to check your answer.
Q1
Why is textContent generally safer than innerHTML when inserting data from an API or user input?
Why is textContent generally safer than innerHTML when inserting data from an API or user input?
innerHTML parses whatever string it's given as real HTML, which means untrusted text containing a <script> tag or similar could actually execute on your page. textContent always inserts a plain string with no HTML parsing at all, so it can't be used to inject markup or scripts, regardless of what the string contains.
Q2
What makes event delegation work — why does a click on a child <li> get caught by a listener on its parent <ul>?
What makes event delegation work — why does a click on a child <li> get caught by a listener on its parent <ul>?
Event bubbling. A click event doesn't only fire on the exact element clicked — it also fires on every ancestor of that element, all the way up the DOM tree, unless something explicitly stops it. A listener on the <ul> catches the bubbled event, and event.target tells it exactly which <li> was the original source.
Q3
Does await fetch(url) throw an error if the server responds with a 404?
Does await fetch(url) throw an error if the server responds with a 404?
No — fetch's Promise only rejects on a genuine network-level failure (like no connection at all). A 404 or 500 response is still a "successful" HTTP exchange as far as fetch is concerned, so it resolves normally. Checking response.ok (or response.status) and throwing manually is the correct way to treat an error status code as a failure.
Q4
Why does document.querySelector("h1") throw a ReferenceError when run in Node instead of a browser?
Why does document.querySelector("h1") throw a ReferenceError when run in Node instead of a browser?
document is part of the DOM, which is a browser-provided runtime API, not part of the JavaScript language itself. Node's runtime has no page to represent and doesn't provide document or window at all — it offers a different set of built-ins instead, like direct filesystem access, that a browser doesn't provide for security reasons.