The DOM: changing things

Build and update the page safely and without making the browser work twice.

17of 32
≈ 12Min read
8Parts

Text

and the one that is dangerous
el.textContent = "Painted";     // safe: whatever the string is, it stays text
el.textContent;                  // reads all descendant text

el.innerHTML = "<b>Painted</b>"; // parses markup — only for markup YOU wrote
el.innerText = "…";              // as rendered; slower, forces layout
Never put user input in innerHTML

A name of <img src=x onerror=alert(1)> becomes a running script. textContent is immune, because it never parses.

Classes

Class is the boundary between JavaScript and CSS. Script decides the state; the stylesheet decides what that state looks like. A handler that sets el.style.background has taken a design decision into the wrong file.

classList, never className +=
el.classList.add("is-open");
el.classList.remove("is-open");
el.classList.toggle("is-open");
el.classList.toggle("is-open", shouldBeOpen);   // force a state
el.classList.contains("is-open");
el.classList.replace("old", "new");

// the old way loses every other class
el.className = "is-open";

Attributes, properties and data

three different things
el.setAttribute("aria-expanded", "true");
el.getAttribute("href");
el.removeAttribute("disabled");
el.toggleAttribute("hidden", shouldHide);

el.disabled = true;              // a property — the right way for booleans
el.value = "x";                  // the live value; the attribute is the default

el.dataset.panel = "specs";      // data-panel="specs"
Number(el.dataset.openDelay);    // always a string, so convert
setAttribute("disabled", false) disables it

Boolean attributes care only about presence. Use the property — el.disabled = false — or removeAttribute.

Styles

Set a custom property from script and let the stylesheet decide what it means. The script says --p: 0.62; the CSS says width: calc(var(--p) * 100%). Nothing about the look lives in the script.

the narrow case where inline style is right
el.style.transform = "translateX(20px)";     // a computed value
el.style.setProperty("--p", 0.62);           // a custom property the CSS uses

getComputedStyle(el).color;                  // the final value, as a string
el.style.color;                              // only what is in the style attribute

Creating elements

build, fill, insert
const li = document.createElement("li");
li.className = "row";
li.textContent = door.name;
li.dataset.id = door.id;

list.append(li);          // inside, at the end — takes several nodes and strings
list.prepend(li);         // inside, at the start
li.before(other);         // outside, before
li.after(other);          // outside, after
li.replaceWith(other);
li.remove();
list.replaceChildren();   // empty it — better than innerHTML = ""

Insert once

A DocumentFragment is a node with no parent: appending to it costs nothing, and appending the fragment inserts all its children at once and leaves it empty.

the difference is real on a long list
// 500 separate insertions
for (const d of doors) list.append(row(d));

// one insertion
const frag = document.createDocumentFragment();
for (const d of doors) frag.append(row(d));
list.append(frag);

// or build the whole list and swap it in
list.replaceChildren(...doors.map(row));

Templates

This keeps the shape of a row in the HTML and only the values in the script — which is easier to read than a long template literal and impossible to make an injection hole.

the markup stays in the HTML where it belongs
const tpl = document.getElementById("row-tpl");

function row(door) {
  const node = tpl.content.cloneNode(true);
  node.querySelector(".title").textContent = door.name;
  node.querySelector(".days").textContent = `${door.days} days`;
  node.querySelector("a").href = `/doors/${door.id}/`;
  return node;
}

Read then write

Reading a geometric property — offsetHeight, getBoundingClientRect, scrollTop, getComputedStyle — makes the browser lay out immediately so it can answer. Alternating reads and writes makes it do that once per item.

layout thrashing
// BAD: every read after a write forces a synchronous layout
for (const el of items) {
  el.style.height = el.offsetHeight + 10 + "px";
}

// GOOD: read everything, then write everything
const heights = items.map((el) => el.offsetHeight);
items.forEach((el, i) => { el.style.height = heights[i] + 10 + "px"; });

Practice