From lesson 1-2, parsing HTML builds the DOM, a tree of element objects.
The Document Object Model is one node object per element, nested exactly like the HTML.
| HTML | DOM |
|---|---|
| an element | a node object |
| nested elements | children |
| the wrapper | the parent |
This unit is about reading and changing that tree from JavaScript.
Getting JavaScript into the page
A page without JavaScript can only ever be a document. The DOM API is what turns it into an application, and every framework that comes later, including React and Vue, ultimately generates the same tree through the same calls taught in this unit.
There is a second reason this lesson matters. When code appears to do nothing, the cause is usually here, because the script ran at the wrong time.
<head> <script src="app.js" defer></script> </head>
| Attribute | Effect |
|---|---|
src | loads and runs a JavaScript file |
defer | download now, run after the HTML is parsed |
Without defer, a script in the head runs before the body exists, so code that touches elements finds nothing there.
A script tag can also sit at the end of body, and code can be written inline between <script> tags for quick tests. Using src plus defer in the head is the habit worth building.
Once running, code sees the page through one global object, document, which is the root of the DOM.
The DOM is a tree of objects
The browser turns HTML nesting, the boxes in boxes from lesson 2-2, into a tree. Every element becomes a node object, nested elements become its children, and the wrapper is the parent, with document at the root.
A tree of plain objects is already familiar territory.
const dom = { tag: "body", children: [ { tag: "h1", children: [] }, { tag: "ul", children: [ { tag: "li", children: [] }, { tag: "li", children: [] }, ] }, ], };
| Term | In this object |
|---|---|
| root | the body node |
| children | the children array |
| leaf | any node with an empty children |
Every DOM operation, whether finding a node, changing it, or adding children, is a walk through this structure. The real one simply has richer nodes, carrying text content, classes, and styles.
Walking the toy DOM
A depth-first visit of every node, indenting by depth, which reconstructs the shape of the original HTML.
const dom = { tag: "body", children: [ { tag: "h1", children: [] }, { tag: "ul", children: [ { tag: "li", children: [] }, { tag: "li", children: [] }, ] }, ], }; function walk(node, depth) { console.log(" ".repeat(depth) + node.tag); for (const child of node.children) { walk(child, depth + 1); } } walk(dom, 0);
Output
body
h1
ul
li
li| Node | Depth | Indent |
|---|---|---|
body | 0 | none |
h1 and ul | 1 | two spaces |
the two li | 2 | four spaces |
The node is printed before recursing, which is what makes a parent appear above its children. Passing depth + 1 down is the entire indentation mechanism, and it needs no bookkeeping on the way back up.
Counting nodes by tag
countTag(node, tag) counts how many nodes in the tree, including the starting node, carry the given tag. It shares its recursive shape with walk.
const dom = { tag: "body", children: [ { tag: "h1", children: [] }, { tag: "ul", children: [ { tag: "li", children: [] }, { tag: "li", children: [] }, ] }, ], }; function countTag(node, tag) { let count = node.tag === tag ? 1 : 0; for (const child of node.children) { count += countTag(child, tag); } return count; } console.log(countTag(dom, "li")); console.log(countTag(dom, "body")); console.log(countTag(dom, "a"));
Output
2 1 0
| Tag | Count |
|---|---|
li | 2 |
body | 1 |
a | 0 |
The count starts at 1 or 0 depending on whether this node matches, then adds countTag(child, tag) for every child exactly as walk recursed.
No base case is written out, because a node with no children means the loop never runs and the recursion stops by itself. That is a property of iterating the children rather than an accident.