Finding elements
Every DOM change starts the same way, by finding the node and then changing it. The finding half reuses an existing skill, since the selector language from unit 4 doubles as JavaScript's query language.
document.querySelector(sel) returns the first element matching a CSS selector, using the exact selectors from lesson 4-1.
const title = document.querySelector("#site-title"); const firstCard = document.querySelector(".card"); const navLink = document.querySelector("nav a");
| Call | Returns |
|---|---|
querySelector(sel) | the first match, or null |
querySelectorAll(sel) | every match, loopable with for...of |
When nothing matches, querySelector returns null, and reading a property of null produces the single most common browser error, TypeError: Cannot read properties of null.
Seeing it means the selector missed, so the two things to check are the spelling and whether the script runs after parsing, using defer from lesson 8-1.
Changing elements
Once a node is in hand, a handful of properties change it.
title.textContent = "Sold out!"; // replace the text title.classList.add("highlight"); // add a class, CSS does the rest title.classList.remove("hidden"); title.classList.toggle("open"); // add if absent, remove if present title.style.color = "crimson"; // inline style, for one-offs img.setAttribute("src", "/new.jpg"); // any attribute
| Member | Changes |
|---|---|
textContent | the element's text |
classList.add and .remove | which classes it carries |
classList.toggle | flips one class on or off |
style.color | an inline style |
setAttribute | any attribute |
The pattern that keeps CSS in charge is to define each visual state as a class, such as .hidden { display: none; } from lesson 5-2, and have JavaScript only add and remove classes.
element.style.x writes an inline style, and lesson 4-3 showed that inline beats every stylesheet rule. That power is exactly the reason to use it sparingly, since a value written inline can no longer be overridden from the stylesheet.
Reading the null property error
A crash reading Cannot read properties of null (reading 'textContent') means no element matched the selector when the script ran.
querySelector returned null, which happens for one of two reasons.
| Cause | Fix |
|---|---|
| the selector has a typo | correct the selector |
| the script ran before the element was parsed | load the script with defer |
The error names the property being read, which is a useful clue, since it confirms the crash is on the line after the failed lookup rather than in the lookup itself. Logging the result of querySelector distinguishes the two causes immediately.
JavaScript reaching into an existing page
Two lookups, a text replacement, and a class addition that lets the stylesheet do the restyling.
JavaScript
const headline = document.querySelector("#headline"); headline.textContent = "Tickets sold out"; headline.classList.add("sold-out"); const status = document.querySelector(".status"); status.textContent = "Check back tomorrow";
| Line | Selector kind | Effect |
|---|---|---|
querySelector("#headline") | id | finds the heading |
textContent = ... | none | replaces its text |
classList.add("sold-out") | none | lets the CSS restyle it |
querySelector(".status") | class | finds the status line |
IDs need the # and classes need the dot, exactly as in a stylesheet. The sold-out class is already styled in the CSS block, so the script never touches a color itself.
The script runs inside the page, so document refers to this document and nothing needs to be imported or connected.
The core of selector matching
matches(node, selector) implements the decision querySelector makes for a toy node, where #x matches the id, .x matches one of the classes, and a bare name matches the tag.
const node = { tag: "p", id: "intro", classes: ["lead", "warning"] }; function matches(node, selector) { if (selector.startsWith("#")) { return node.id === selector.slice(1); } if (selector.startsWith(".")) { return node.classes.includes(selector.slice(1)); } return node.tag === selector; } console.log(matches(node, "p")); console.log(matches(node, "#intro")); console.log(matches(node, ".lead")); console.log(matches(node, ".card")); console.log(matches(node, "h1"));
Output
true true true false false
| Selector | Checks | Result |
|---|---|---|
p | the tag | true |
#intro | the id | true |
.lead | class membership | true |
.card | class membership | false |
h1 | the tag | false |
startsWith("#") and startsWith(".") split the three cases, and slice(1) drops the leading character to leave the bare name.
The class case uses includes rather than equality, because an element carries a list of classes and any one of them can match. That asymmetry with the id case is why real elements expose a classList rather than a single class string.