Course outline · 0% complete

0/30 lessons0%

Course overview →

Creating elements from data

lesson 8-4 · ~10 min · 27/30

Pages built from arrays

Real interfaces are rarely written by hand element by element. Search results, chat messages, and product lists arrive as arrays of data, and the page manufactures one element per entry. Every feed ever scrolled works this way.

The DOM gives three operations for it.

const li = document.createElement("li"); // 1. make a detached node
li.textContent = "Ada";                  // 2. fill it in
list.append(li);                         // 3. attach it into the tree
StepCallResult
1createElementa node not yet on the page
2textContentits text is set
3appendit becomes visible

A created element is invisible until append places it inside a node already on the page, and the reverse is just as short, since element.remove() detaches it again.

The full loop is a shape worth knowing cold.

const names = ["Ada", "Grace", "Linus"];
const list = document.querySelector("#list");

for (const name of names) {
  const li = document.createElement("li");
  li.textContent = name;
  list.append(li);
}

The security line between textContent and innerHTML

There is a tempting shortcut, since element.innerHTML = "<li>Ada</li>" parses a string as HTML and builds the nodes automatically.

The danger appears the moment any part of that string came from a user. A display name set to <img src=x onerror="..."> will be built as a real element, running the attacker's code in every visitor's browser. This attack is called XSS, for cross-site scripting, and it is one of the most common real-world web vulnerabilities.

AssignmentTreats the string asCan create elements
textContentplain textno
innerHTMLmarkupyes

textContent is immune, because a < arrives on screen as a literal < character. The browser performs the lesson 2-4 entity escaping automatically.

The working rule is enforced in real code reviews. Data, meaning anything typed by a user or fetched from a server, goes in via textContent, and innerHTML only ever receives markup written by hand.

name = "<img src=x onerror ...>"card.textContent = namecard.innerHTML = nametreated as charactersparsed as markupthe tag appears on screen as textan element is built and the code runssafethis is XSSData always takes the left path, and innerHTML only ever receives markup written by hand.
The same untrusted string down two paths: textContent renders it as characters, innerHTML parses it into a live element.

Building the escaper by hand

escapeHtml(text) replaces the three characters that give HTML its structure, which is what textContent does internally.

function escapeHtml(text) {
  let out = "";
  for (const ch of text) {
    if (ch === "&") out += "&amp;";
    else if (ch === "<") out += "&lt;";
    else if (ch === ">") out += "&gt;";
    else out += ch;
  }
  return out;
}

console.log(escapeHtml("Tom & Jerry"));
console.log(escapeHtml("<script>alert(1)</script>"));
console.log(escapeHtml("plain text"));

Output

Tom &amp; Jerry
&lt;script&gt;alert(1)&lt;/script&gt;
plain text
CharacterEntity
&&amp;
<&lt;
>&gt;

A for...of loop appends either the replacement or the original character to an output string, and the entities are the ones from lesson 2-4, semicolons included.

& is handled first for a reason. Escaping it last would rewrite the ampersands introduced by the other two replacements, turning &lt; into &amp;lt; and printing the entity instead of the character.

A list built from data

Four names in an array become four list items, with no markup written by hand.

JavaScript

const names = ["Ada", "Grace", "Linus", "Margaret"];

const roster = document.querySelector("#roster");

for (const name of names) {
  const li = document.createElement("li");
  li.textContent = name;
  roster.append(li);
}
StepCall
make the nodedocument.createElement("li")
fill itli.textContent = name
attach itroster.append(li)

The loop is for (const name of names), and each pass runs the same three steps.

Because the markup is generated from the array, the page follows the data, so adding a name adds a row with no HTML change. That is the property every framework later automates, and the underlying calls stay these three.

The safe assignment for untrusted data

For a display name arriving from a server and shown inside a card, the safe assignment is card.textContent = name.

It treats the string as text and never as markup, and because textContent cannot create elements, a malicious name full of tags renders as harmless literal text.

AssignmentOutcome for <img src=x onerror=...>
card.textContent = namethe tag is displayed as text
card.innerHTML = namethe element is built and the code runs

innerHTML parses the string as HTML and would execute an XSS payload, so data goes through textContent without exception. The rule is worth applying even to data that seems trustworthy, since the source of a string is easy to lose track of as code moves.