Course outline · 0% complete

0/30 lessons0%

Course overview →

Selecting and changing elements

lesson 8-2 · ~9 min · 25/30

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");
CallReturns
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
MemberChanges
textContentthe element's text
classList.add and .removewhich classes it carries
classList.toggleflips one class on or off
style.coloran inline style
setAttributeany 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.

CauseFix
the selector has a typocorrect the selector
the script ran before the element was parsedload 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";
LineSelector kindEffect
querySelector("#headline")idfinds the heading
textContent = ...nonereplaces its text
classList.add("sold-out")nonelets the CSS restyle it
querySelector(".status")classfinds 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
SelectorChecksResult
pthe tagtrue
#introthe idtrue
.leadclass membershiptrue
.cardclass membershipfalse
h1the tagfalse

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.