For five modules you have built an entire application that nobody can see. Task, Board, createBacklog(), the validation errors, the promises, the generators: everything works, everything is tested, and everything spits its result into the developer tools console. Marta, Iván and Lucía are not going to open the console. They need a page: a list of tasks on the screen, with its colors, its buttons and its form. The bridge between the model you already have and that screen is called the DOM, the Document Object Model, and it is what turns an HTML file into a structure of objects that your JavaScript can read and modify. In this lesson you are not going to modify anything yet: you are going to understand what that structure actually is, how the browser builds it, what kinds of pieces it is made of, how it is walked and at exactly which moment it is ready for your code to touch it. Without this, everything that comes afterwards is recipes copied without knowing why they work.
Contents
- From HTML text to a tree of nodes
- The DOM is not part of the JavaScript language
windowanddocument: the global object and the front door- Node types, and why whitespace counts
- Walking the tree: parents, children and siblings
HTMLCollectionandNodeList: collections that are not arrays- Inspecting the DOM in the DevTools
- The page lifecycle:
DOMContentLoadedandload - Why
<script type="module">already behaves likedefer - Nómada Tasks: the initial
index.html - Common Mistakes and Tips
- Exercises
- Conclusion
- From HTML text to a tree of nodes
When the browser receives an HTML file, what arrives over the network is plain text. A string of characters:
That text is useless on its own. The browser parses it: it reads it from beginning to end, recognizes the opening and closing tags, and builds in memory a structure of objects that represents the document. That structure is a tree: each tag becomes an object (a node), and nested tags become children of the node that contains them.
flowchart TD
DOC["document"] --> HTML["html"]
HTML --> HEAD["head"]
HTML --> BODY["body"]
HEAD --> META["meta charset"]
HEAD --> TITLE["title"]
HEAD --> LINK["link · styles.css"]
BODY --> HEADER["header.header"]
BODY --> MAIN["main"]
BODY --> FOOTER["footer"]
BODY --> SCRIPT["script type=module"]
HEADER --> H1["h1 · Nómada Tasks"]
MAIN --> SECTION["section.board"]
SECTION --> H2["h2 · Backlog"]
SECTION --> P["p#summary"]
SECTION --> UL["ul#task-list"]
UL --> LI1["li.task · id=1"]
UL --> LI2["li.task · id=2"]
UL --> LI3["li.task · id=3"]
Three important ideas come out of this drawing:
- There is exactly one root, the
documentobject, from which everything else hangs.documentis not the<html>tag: it is the whole document, and<html>is its only element child. - The nesting relationship in the text becomes the parent/child relationship of the tree. If in the HTML you write an
<li>inside a<ul>, in the DOM thelinode is a child of theulnode. No more, no less. - The tree is a living object, not a copy of the file. If your JavaScript adds an
<li>, the tree changes and the screen repaints. The.htmlfile on disk is never touched. On reload, the browser rebuilds the tree from the original text and all your changes vanish.
That last idea is the hardest one at first. The DOM is not your HTML: it is what the browser has built from your HTML, and from that moment on the two things live separate lives.
The DOM is not what you see either
It is worth separating three concepts that get confused all the time:
| Concept | What it is | Who changes it |
|---|---|---|
| The HTML file | Text on disk or on the server | You, with the editor |
| The DOM | A tree of objects in memory | The browser on load, and your JavaScript afterwards |
| What you see on screen | Pixels painted from the DOM + the CSS | The browser, when the DOM or the CSS change |
The browser combines the DOM tree with the CSS rules to compute a second tree (the render tree), decides where each box goes and paints. That process has a cost, and it is the reason there is a whole lesson in Module 9 about manipulating the DOM efficiently. For now, hold on to the chain: HTML → DOM → CSS → pixels, and to the fact that your JavaScript acts on the second link.
- The DOM is not part of the JavaScript language
In 01-01 a distinction appeared that now becomes central: JavaScript, the language, is what the ECMAScript specification defines. That is where let, functions, objects, Array, Promise, Map, classes, modules and generators live — everything you have learned in five modules. In that specification the word document does not appear a single time.
The DOM is an environment API: a library of objects that the browser makes available to the code it runs, specified separately by the WHATWG. That is why:
// This works anywhere: it is the language.
const open = tasks.filter((t) => t.isOpen);
// This only works inside a browser: it is the environment.
document.querySelector('#task-list');If you run the second line in Node —the same V8 engine, the same language— you get ReferenceError: document is not defined. It is not that Node has a worse JavaScript: it is that Node is not a browser and has no document to display. In exchange it gives you fs, process and other APIs the browser does not have.
flowchart TD
ES["ECMAScript · the language<br/>let · functions · classes · Promise · Map"]
NAV["Browser environment<br/>document · window · addEventListener · fetch"]
NODE["Node environment<br/>fs · process · path · http"]
ES --> NAV
ES --> NODE
This separation has a very concrete practical consequence that will stay with you for the rest of the course: the Nómada Tasks model must not know the DOM exists. Task and Board are pure JavaScript; they work the same in the browser, in Node and in the Module 8 tests. The whole part that talks to the page will live in a new folder, js/view/, and that separation is what will make the project testable and maintainable.
window and document: the global object and the front door
window and document: the global object and the front doorIn a browser, the global object is called window. It represents the tab (or the window, or the iframe) and contains absolutely everything the environment offers: the APIs, the global variables and the document itself as well.
console.log(typeof window); // 'object'
console.log(window === globalThis); // true ← the language's standard global
console.log(window.document === document); // true
console.log(window.innerWidth); // 1280 ← usable width of the window, in pixelsThe fact that window is the global object explains a historical detail: when you write plain document, alert or setTimeout, you are really accessing window.document, window.alert and window.setTimeout. The prefix is optional and almost nobody writes it.
The division of labor between the two is simple:
| Object | What it deals with | Examples |
|---|---|---|
window |
The window and the environment | innerWidth, location, history, setTimeout, alert |
document |
The content of the page | title, body, head, querySelector, createElement |
console.log(document.title); // 'Nómada Tasks · Taller Nómada'
console.log(document.body.tagName); // 'BODY'
console.log(document.nodeType); // 9 ← we will see what that means in the next sectionTwo shortcuts the browser offers and that are worth knowing: document.documentElement is the <html> node, document.head is the <head> and document.body is the <body>. Careful with that last one: if your <script> runs before the parser has reached the <body>, document.body is null. It is the first warning about the timing problem we will solve in section 8.
- Node types, and why whitespace counts
Not everything in the tree is tags. The DOM has several node types, and each one has an identifying number available in the nodeType property. These are the ones you are going to run into:
nodeType |
Constant | What it represents | Example in the HTML |
|---|---|---|---|
1 |
Node.ELEMENT_NODE |
An element, that is, a tag | <li class="task"> |
3 |
Node.TEXT_NODE |
Text, including line breaks and indentation | Redesign the multipurpose |
8 |
Node.COMMENT_NODE |
An HTML comment | <!-- pending --> |
9 |
Node.DOCUMENT_NODE |
The whole document | The document object |
11 |
Node.DOCUMENT_FRAGMENT_NODE |
A lightweight container outside the tree | DocumentFragment (lesson 06-05) |
The one that surprises everybody is type 3. Look at this HTML, written the way anyone would write it:
<ul id="task-list">
<li class="task">Redesign the multipurpose room</li>
<li class="task">Signage for the screen-printing workshop</li>
</ul>Between <ul> and the first <li> there is a line break and two spaces. For the browser that is a text node, just as real as the <li>s. The same between the two <li>s and between the last one and </ul>. Result:
const list = document.getElementById('task-list');
console.log(list.childNodes.length); // 5 ← text, li, text, li, text
console.log(list.children.length); // 2 ← only the elements
for (const node of list.childNodes) {
console.log(node.nodeType, JSON.stringify(node.nodeName), JSON.stringify(node.textContent));
}
// 3 "#text" "\n "
// 1 "LI" "Redesign the multipurpose room"
// 3 "#text" "\n "
// 1 "LI" "Signage for the screen-printing workshop"
// 3 "#text" "\n"Five child nodes where at first glance there are two. This is the number one cause of beginner bugs when walking the DOM: list.firstChild is not the first <li>, it is a text node with a line break and two spaces. Hence the existence of a parallel family of properties that ignore everything that is not an element, and which is the one you will use 99 % of the time.
Three node properties worth telling apart:
nodeType: the number from the table.nodeName: the name of the node. For elements it is the tag in uppercase ('LI'); for text,'#text'; for the document,'#document'.tagName: only exists on elements, and is the tag in uppercase. If you hesitate betweennodeNameandtagName, usetagNamewhen you know you have an element.
- Walking the tree: parents, children and siblings
Every node knows its neighbors. These are the navigation properties, grouped by what they return:
| Property | Returns | Does it include text nodes? |
|---|---|---|
parentNode |
The parent node | Yes (it can be the document) |
parentElement |
The parent element | No (returns null if the parent is not an element) |
childNodes |
All the children, as a live NodeList |
Yes |
children |
The element children, as a live HTMLCollection |
No |
firstChild / lastChild |
First and last child | Yes |
firstElementChild / lastElementChild |
First and last element child | No |
previousSibling / nextSibling |
Previous/next sibling | Yes |
previousElementSibling / nextElementSibling |
Previous/next element sibling | No |
childElementCount |
Number of element children | No |
The mnemonic is simple: if the name carries Element, it ignores text and comments. And those are the ones you want almost always.
With the HTML from the previous section:
const list = document.getElementById('task-list');
const first = list.firstElementChild;
console.log(first.textContent); // 'Redesign the multipurpose room'
const second = first.nextElementSibling;
console.log(second.textContent); // 'Signage for the screen-printing workshop'
console.log(second.nextElementSibling); // null ← there is no third one
console.log(first.parentElement === list); // true
console.log(list.parentElement.tagName); // 'SECTION'Going up the tree works too, and chains as many times as you want:
And here comes an important piece of advice: chaining parentElement is fragile. If tomorrow you wrap the list in a <div> for design reasons, all those chains break silently. In the next lesson you will learn closest(), which goes up looking for the first ancestor that matches a selector, and which is the robust way of doing exactly this.
A real example, counting how many nodes there are in the complete tree with the recursion from 03-07:
/** Counts nodes in the tree, separating elements, text and comments. */
function countNodes(node, counts = { elements: 0, text: 0, comments: 0 }) {
if (node.nodeType === Node.ELEMENT_NODE) counts.elements += 1;
else if (node.nodeType === Node.TEXT_NODE) counts.text += 1;
else if (node.nodeType === Node.COMMENT_NODE) counts.comments += 1;
for (const child of node.childNodes) countNodes(child, counts);
return counts;
}
console.log(countNodes(document.body));
// { elements: 24, text: 31, comments: 1 } ← the numbers depend on your HTMLThat there are more text nodes than elements in well-indented HTML is not a mistake: it is exactly what section 4 explained.
HTMLCollection and NodeList: collections that are not arrays
HTMLCollection and NodeList: collections that are not arrayslist.children looks like an array: it has length, you index it with brackets and it contains elements. It is not an array. It is an HTMLCollection, and list.childNodes is a NodeList. Neither of them inherits from Array.prototype, so they have no map, filter, reduce or find.
const items = list.children; // HTMLCollection
console.log(items.length); // 2 ✓
console.log(items[0].tagName); // 'LI' ✓
console.log(Array.isArray(items)); // false ← careful
console.log(items.map); // undefined
// items.map((li) => li.textContent);
// ✗ TypeError: items.map is not a functionThe critical difference between the two is not only what they contain, but whether they are live: a live collection updates itself when the DOM changes; a static one is a fixed snapshot of the moment it was created.
| Collection | Returned by | Contains | Live? | forEach |
|---|---|---|---|---|
HTMLCollection |
children, getElementsByClassName, getElementsByTagName |
Elements only | Yes | No |
Live NodeList |
childNodes |
Any node | Yes | Yes |
Static NodeList |
querySelectorAll (lesson 06-02) |
Any node | No | Yes |
A live collection sounds convenient and is in fact a classic source of infinite loops:
const items = list.children; // live
for (let i = 0; i < items.length; i++) {
list.append(document.createElement('li')); // ← every pass adds a child…
}
// items.length grows on every iteration: the loop never ends.The solution, and the recommended practice in general, is to convert to an array as soon as you get it, using Array.from or the spread you learned in 04-03 and 04-07:
const items = Array.from(list.children); // a real array, static
// or else
const items2 = [...list.children];
const titles = items
.filter((li) => !li.classList.contains('task--done'))
.map((li) => li.textContent.trim());
console.log(titles);
// ['Redesign the multipurpose room', 'Signage for the screen-printing workshop']Both conversions work because HTMLCollection and NodeList are both iterable (they implement Symbol.iterator, exactly the protocol from 05-08). That is also why you can walk them with for...of without converting anything:
Array.from also has a second parameter that acts as a map in the same pass, and saves an intermediate array:
- Inspecting the DOM in the DevTools
All of this is much clearer when you poke at it. Open index.html with Live Server, press F12 and go to the Elements tab.
What you see there is not your HTML file: it is a representation of the current DOM. If your JavaScript adds an <li>, it will show up in that panel even though it is not in the file. Things you can do and that are worth practicing today:
- Expand the tree with the triangles, and check that the hierarchy matches the diagram in section 1.
- Select an element by clicking on it. You will see its applied styles in the panel on the right and, very usefully, the breadcrumb trail at the bottom:
body > main > section.board > ul#task-list > li.task. - Edit live: double-click on a piece of text or an attribute to change it. The changes show up instantly and are lost on reload. It is the perfect laboratory for trying out a CSS class before writing the JavaScript that applies it.
- Use
$0in the console: after selecting an element in the Elements panel, the$0variable in the console points to that element.$0.children,$0.tagName,$0.classList… it is the fastest way to explore. console.dir(element)versusconsole.log(element): the first shows you the element as an object, with all its properties expandable; the second shows it to you as HTML. To learn what properties a node has,console.diris infinitely better.
const list = document.getElementById('task-list');
console.log(list); // <ul id="task-list">…</ul> ← HTML view
console.dir(list); // ul#task-list { children: …, id: …, … } ← object view
- The page lifecycle:
DOMContentLoaded and load
DOMContentLoaded and loadHere is the most classic timing problem of them all. The browser parses the HTML from top to bottom, and runs a classic <script> at the exact moment it finds it. If the script is in the <head>, when it runs the <body> does not exist yet:
<head>
<script>
// The parser has not reached the body yet.
console.log(document.getElementById('task-list')); // null ✗
</script>
</head>It does not throw: it gives null, which is worse, because the failure shows up two lines further down with a TypeError: Cannot read properties of null.
There are two named moments in the loading of a page:
| Event | When it fires | What it guarantees |
|---|---|---|
DOMContentLoaded |
When the HTML has been fully parsed and the DOM tree is built | Every element exists |
load (on window) |
When images, stylesheets, fonts and iframes have finished downloading as well | Everything is downloaded and at its real size |
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM ready:', document.querySelectorAll('li.task').length, 'tasks');
});
window.addEventListener('load', () => {
console.log('Everything downloaded, images included');
});The rule is clear: to work with the DOM, DOMContentLoaded is enough; waiting for load delays the start of the application until the last image is downloaded, and is almost never necessary. You only need load if you depend on the real dimensions of an image or of an external resource.
flowchart LR
A["The HTML arrives"] --> B["The browser parses it<br/>and builds the DOM"]
B --> C["DOMContentLoaded<br/>the tree is complete"]
C --> D["Download of images,<br/>fonts, iframes"]
D --> E["load<br/>everything is ready"]
- Why
<script type="module"> already behaves like defer
<script type="module"> already behaves like deferThe historical solution to the previous problem was to put the <script> right before </body>, or to add the defer attribute to it, which tells the browser: "download this file in parallel, but do not run it until the DOM is built".
And here comes the good news, which links straight back to 05-04: a <script type="module"> is defer by default. There is nothing to write. A module:
- Downloads in parallel without blocking the HTML parser.
- Runs after the document has been completely parsed.
- Runs before the
DOMContentLoadedevent.
| Way of including the script | Does it block parsing? | When does it run? | Is the DOM ready? |
|---|---|---|---|
<script> in the <head> |
Yes | As soon as it is found | No |
<script> before </body> |
Yes, but at the end | As soon as it is found | Yes |
<script defer> |
No | After parsing the document | Yes |
<script type="module"> |
No | After parsing the document | Yes |
<script async> |
No | As soon as it downloads, in any order | Not guaranteed |
Since Nómada Tasks already uses <script type="module" src="js/app.js"></script>, your app.js can select elements from its very first line without wrapping anything in a DOMContentLoaded handler. This is one of those things you see in a thousand old tutorials and that today is redundant:
// js/app.js — a module: this is already safe, no wrappers
const list = document.getElementById('task-list');
console.log(list.children.length); // 3 ✓There is still a case where you need the event: if your module awaits something slow before touching the DOM, or if you write code that can run both as a module and in a classic <script>. For those cases, the defensive pattern is to check document.readyState:
function onReady(fn) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fn, { once: true });
} else {
fn(); // the DOM was already ready: run now
}
}document.readyState goes through three values: 'loading' while parsing, 'interactive' when the DOM is ready (right before DOMContentLoaded) and 'complete' after the load event.
- Nómada Tasks: the initial
index.html
index.htmlIt is time to write the page. This HTML is the base on which the whole module will grow: in 06-02 you will paint these elements, in 06-03 you will attach events to them, in 06-05 you will create them from JavaScript and in 06-07 you will complete the form. For now the three tasks are written by hand so there is something to inspect.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nómada Tasks · Taller Nómada</title>
<link rel="stylesheet" href="css/styles.css">
</head>
<body>
<header class="header">
<h1>Nómada Tasks</h1>
<p class="header__sub">
Taller Nómada · <time datetime="2026-09-20">20 September 2026</time>
</p>
</header>
<main>
<section class="board" aria-labelledby="board-title">
<h2 id="board-title">Backlog</h2>
<p id="summary" class="summary" role="status">Loading the board…</p>
<ul id="task-list" class="task-list">
<li class="task" data-id="1">
<span class="task__title">Redesign the multipurpose room</span>
<span class="task__meta">Iván · 12 h · in-progress</span>
</li>
<li class="task" data-id="2">
<span class="task__title">Signage for the screen-printing workshop</span>
<span class="task__meta">Marta · 6 h · pending</span>
</li>
<li class="task" data-id="6">
<span class="task__title">Carpentry workshop quote</span>
<span class="task__meta">Iván · 5 h · pending</span>
</li>
</ul>
</section>
<section aria-labelledby="new-task-title">
<h2 id="new-task-title">New task</h2>
<form id="new-task" class="form" novalidate>
<p>The form is completed in lesson 06-07.</p>
</form>
</section>
</main>
<footer class="footer">
<p>Taller Nómada · coworking, screen printing, bookbinding and carpentry</p>
</footer>
<script type="module" src="js/app.js"></script>
</body>
</html>Every decision is worth justifying, because none of them is decorative:
<html lang="en">: screen readers choose the voice and the pronunciation from this attribute. Without it, a reader configured in Spanish would read "Carpentry workshop quote" with Spanish phonetics.- Semantic HTML:
<header>,<main>,<section>,<footer>instead of<div>for everything. Assistive technologies expose those elements as navigable landmarks; a screen reader user can jump straight to the main content. aria-labelledbyon each<section>: it gives the section an accessible name taken from the<h2>. A section without a name does not appear in the list of landmarks.<ul>with<li>: a task list is a list. The screen reader will announce "list with 3 items", information that a pile of<div>s would not give.role="status"on the summary paragraph: when its text changes from JavaScript, the screen reader will announce it without stealing focus. It is the correct way to communicate "45 h open" to someone who cannot see the screen.data-idon each<li>: it is the key piece for the rest of the module. It connects each element on the page with theidof itsTaskin the model. In 06-02 you will read it withdataset, and in 06-04 it will be the basis of event delegation.<time datetime="2026-09-20">: the human-readable date and the canonical machine date, in the same element.
And the minimal CSS. The module's motto is "only what is needed": classes with clear names, no frills.
/* css/styles.css */
:root {
--color-high: #c0392b;
--color-medium: #b7791f;
--color-low: #2b7a78;
--gray: #6b7280;
--border: #e5e7eb;
}
body {
font-family: system-ui, -apple-system, "Segoe UI", sans-serif;
max-width: 52rem;
margin: 0 auto;
padding: 1.5rem;
color: #1f2933;
line-height: 1.5;
}
.header__sub { color: var(--gray); margin-top: -0.5rem; }
.summary { font-weight: 600; }
.board { border: 1px solid var(--border); border-radius: 0.5rem; padding: 1rem; }
.task-list { list-style: none; padding: 0; margin: 0; }
.task {
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 0.6rem 0.8rem;
border-left: 4px solid var(--gray);
border-bottom: 1px solid var(--border);
}
.task--high { border-left-color: var(--color-high); }
.task--medium { border-left-color: var(--color-medium); }
.task--low { border-left-color: var(--color-low); }
.task--done .task__title { text-decoration: line-through; color: var(--gray); }
.task--overdue .task__meta { color: var(--color-high); font-weight: 600; }
.task__meta { color: var(--gray); font-size: 0.9rem; white-space: nowrap; }
/* A visible focus ring is mandatory: whoever navigates with the keyboard needs to see it. */
:focus-visible { outline: 3px solid #2563eb; outline-offset: 2px; }Note the naming convention: .task is the block, .task__title a part of it and .task--high a variant. That discipline will mean that in 06-02, when you manipulate classes with classList, you know exactly what to add and what to remove.
And the app.js for this lesson, which still does not modify anything: it only checks that the tree is the one we expect.
// js/app.js
import { Board } from './model/board.js';
import { createBacklog } from './data/backlog.js';
import { TODAY } from './util/dates.js';
const board = new Board('Taller Nómada', createBacklog());
const summary = board.summary(TODAY);
console.log(summary);
// { total: 6, open: 5, totalHours: 48, openHours: 45, overdue: 1, effort: 124 }
// The DOM is already built: this is a module, and modules are 'defer'.
const list = document.getElementById('task-list');
console.log('<li> elements on the page:', list.children.length); // 3
console.log('Total child nodes:', list.childNodes.length); // 7 (the text nodes count)
for (const li of list.children) {
console.log(li.dataset.id, '→', li.firstElementChild.textContent);
}
// 1 → Redesign the multipurpose room
// 2 → Signage for the screen-printing workshop
// 6 → Carpentry workshop quoteThe model's canonical numbers (48 h in total, 45 open, 1 overdue, effort 124) are still intact, and now they share the same page with three hand-written <li>s. Closing that gap —making the list on the page come from the Board and not from the HTML— is the work of the next six lessons.
Common Mistakes and Tips
- Believing
documentis part of JavaScript. It is not, and that is why the same code fails in Node. Keep the model (model/,data/,util/) free of any reference to the DOM: you will thank yourself in Module 8, when you write tests that run without a browser. - Confusing
childNodeswithchildren. It is the classic trap:childNodesincludes the text nodes generated by your own indentation. Always use the family withElementin the name (children,firstElementChild,nextElementSibling) unless you really need the text. - Treating an
HTMLCollectionas an array. It has nomap,filterorreduce. Convert withArray.from(collection)or[...collection]as soon as you get it; that way you also stop having a live collection that changes under your feet. - Modifying the DOM while walking a live collection. The loop may never end, or may skip elements. Converting to an array before the loop eliminates the problem at the root.
- Putting a classic
<script>in the<head>withoutdefer. EverygetElementByIdwill returnnull. Withtype="module"the problem disappears; if even so your code can run in different contexts, use theonReadypattern from section 9. - Waiting for
loadwhenDOMContentLoadedis enough. You delay startup until the last external resource downloads. Only wait forloadif you need the real dimensions of images. - Tip: use
console.dir(element)to explore.console.logshows you the HTML;console.dirshows you the object with all its properties. And combine the Elements panel with$0in the console: it is the fastest way to discover the API without searching the documentation. - Tip: write semantic HTML from the very first minute. Adding accessibility at the end is extremely expensive; writing it well from the start costs nothing. A
<ul>of<li>s witharia-labelledbyon the section already gets you half of the accessibility done.
Exercises
Exercise 1 · The tree census
Write in js/app.js a function describeTree(root, level = 0) that prints to the console the element tree hanging from root, with an indent of two spaces per level, showing the tag in lowercase, the id if it has one and the classes if it has any. It must ignore text and comment nodes. Apply it to document.body.
Expected output (trimmed):
Exercise 2 · Three ways to reach the same place
Starting from the <li> with data-id="2", get the <section class="board"> element in three different ways: (a) chaining parentElement; (b) with a while loop that goes up until it finds an element whose class list contains board; (c) explaining, without writing the code, which method from the next lesson would solve this in one line. Comment on which of the first two you find more robust and why.
Exercise 3 · Headlines of the open tasks
Without using querySelector (we have not seen it yet), get an array with the titles of the <li>s in the list whose meta text does not contain 'done'. Use Array.from, filter and map. Then print how many there are and check that the result is still a real array with Array.isArray.
Solutions
Exercise 1
function describeTree(root, level = 0) {
const indent = ' '.repeat(level);
const id = root.id ? ` #${root.id}` : '';
const classes = root.classList.length ? ' .' + [...root.classList].join(' .') : '';
console.log(`${indent}${root.tagName.toLowerCase()}${id}${classes}`);
for (const child of root.children) { // 'children' already filters out text and comments
describeTree(child, level + 1);
}
}
describeTree(document.body);Key points: we walk children (not childNodes), so there is no need to check nodeType; the recursion is the one from 03-07, with the level as an accumulator; and [...root.classList] works because classList is iterable too. If an element has no classes, classList.length is 0 and the string ends up empty instead of showing a stray dot.
Exercise 2
const li = document.getElementById('task-list').children[1];
// (a) Chaining parentElement
const sectionA = li.parentElement.parentElement;
console.log(sectionA.tagName, sectionA.className); // SECTION board
// (b) Going up with a loop until the class is found
function climbTo(node, className) {
let current = node;
while (current !== null && !current.classList?.contains(className)) {
current = current.parentElement;
}
return current;
}
const sectionB = climbTo(li, 'board');
console.log(sectionA === sectionB); // true(c) li.closest('.board') does exactly the same as version (b) in a single call, and you will see it in 06-02.
Version (b) is clearly more robust. Version (a) encodes an exact distance in the tree: if tomorrow you wrap the <ul> in a <div class="scroll">, sectionA will become the <div> and nobody will notice until something breaks two screens later. Version (b) encodes an intention ("go up to the board"), and survives layout changes. The ?. before contains is the protection from 01-06: when the loop reaches document, classList is undefined and without optional chaining there would be a TypeError.
Exercise 3
const list = document.getElementById('task-list');
const openTasks = Array.from(list.children)
.filter((li) => !li.lastElementChild.textContent.includes('done'))
.map((li) => li.firstElementChild.textContent.trim());
console.log(openTasks);
// ['Redesign the multipurpose room', 'Signage for the screen-printing workshop', 'Carpentry workshop quote']
console.log(openTasks.length); // 3
console.log(Array.isArray(openTasks)); // trueThe essential point: without the initial Array.from, the filter line would throw TypeError: list.children.filter is not a function, because children is an HTMLCollection. The .trim() cleans up the line breaks and indentation that the HTML introduces inside the <span> — exactly the text nodes from section 4. And notice how fragile it is to read the status of a task by looking for the substring 'done' inside a piece of text: it is precisely what you will stop doing in the next lesson, when you store that information in data-* attributes and in classes instead of in prose.
Conclusion
You now know what the DOM is and, almost more importantly, what it is not. It is not your HTML file, but the tree of objects the browser builds from it and which from that moment on lives on its own in memory. It is not part of the JavaScript language, but an API contributed by the browser environment: that is why document does not exist in Node and why the Nómada Tasks model —Task, Board, createBacklog— must go on never mentioning it. And it is not what you see: what you see are pixels the browser paints by combining the DOM with the CSS, in a chain HTML → DOM → CSS → pixels on which you act at the second link.
You know the anatomy of the tree: window as the global object and document as the front door to the content; the node types with their nodeType (1 element, 3 text, 8 comment, 9 document) and the surprise that your own indentation generates real text nodes, which is the reason childNodes returns five children where children returns two. You know how to move between nodes with parentElement, children, firstElementChild and nextElementSibling, with the golden rule of always preferring the properties that carry Element in the name. And you know that the collections the DOM returns —HTMLCollection and NodeList— are not arrays: they are iterable, so for...of works, but to use map and filter you have to go through Array.from or the spread, something that also spares you the scares of live collections.
You have also solved the timing problem that ruins so many people's first day: the parser builds the tree from top to bottom, DOMContentLoaded announces that it is complete, load waits for the images as well, and a <script type="module"> already behaves like defer, so your app.js can select elements from its very first line. And you have written the Nómada Tasks index.html with genuinely semantic structure —header, main, section.board, ul#task-list, form#new-task, role="status", aria-labelledby— together with a minimal CSS of clear classes (.task, .task--high, .task--done, .task--overdue) and, above all, a data-id on each <li> that will be the hinge between the screen and the model throughout the module.
What you still cannot do is find a specific element without hopping from parent to child, and change it. Walking the tree by hand with firstElementChild.nextElementSibling.parentElement works, but it is exactly the kind of fragile code that breaks when you move a <div>. There is a much better way: describe what you are looking for with a CSS selector —'.task--high', '#task-list > li', '[data-id="6"]'— and let the browser find it. That, together with the correct way to change text, attributes, classes and styles (and why innerHTML is a security hole waiting to happen), is Selecting and Manipulating DOM Elements, where you will finally paint the real status and priority of the Taller Nómada tasks on the screen.
JavaScript Course: From Beginner to Advanced
Module 1: Introduction to JavaScript
- What Is JavaScript?
- Setting Up Your Development Environment
- Your First JavaScript Program
- JavaScript Syntax and Basic Concepts
- Variables and Data Types
- Basic Operators
- Type Conversion and Comparisons
- The Course Project: Nómada Tasks
Module 2: Control Structures
- Conditional Statements
- Loops: for, while, do-while
- Switch Statements
- Flow Control: break, continue and Nested Loops
- Error Handling with try-catch
Module 3: Functions
- Defining and Calling Functions
- Function Expressions and Arrow Functions
- Parameters and Return Values
- Scope and Closures
- Hoisting and the Execution Context
- Higher-Order Functions
- Recursion
Module 4: Objects and Arrays
- Introduction to Objects
- Object Methods and the
thisKeyword - Arrays: Basics and Methods
- Iterating over Arrays
- Searching, Sorting and Aggregating Data: find, sort and reduce
- Array Destructuring
- Object Destructuring, Spread and Rest
- JSON and Copying Objects
Module 5: Advanced Objects and Functions
- Prototypes and Inheritance
- Classes and Object-Oriented Programming
- Encapsulation: Getters, Setters and Private Fields
- Modules: Import and Export
- Asynchronous JavaScript: Callbacks
- Promises and Async/Await
- The Event Loop and the Microtask Queue
- Iterators and Generators
Module 6: The Document Object Model (DOM)
- Introduction to the DOM
- Selecting and Manipulating DOM Elements
- Handling Events
- Propagation, Delegation and Custom Events
- Creating and Removing DOM Elements
- Rendering Lists and HTML Templates
- Handling and Validating Forms
Module 7: Browser APIs and Advanced Topics
- Local and Session Storage
- The Fetch API and AJAX
- Robust Requests: Errors, Timeouts and AbortController
- WebSockets
- Service Workers and Progressive Web Apps (PWAs)
- Essential Browser APIs
- Introduction to WebAssembly
Module 8: Testing and Debugging
- Debugging JavaScript
- Code Quality: ESLint, Prettier and Conventions
- Unit Testing with Jest
- Test Doubles: Mocks, Stubs and Spies
- Integration Testing
- End-to-End Testing with Cypress
Module 9: Performance and Optimization
- Measure Before You Optimize: DevTools and Web Vitals
- Optimizing JavaScript Performance
- Memory Management
- Efficient DOM Manipulation
- Lazy Loading and Code Splitting
Module 10: JavaScript Frameworks and Libraries
- Why Frameworks Exist
- Introduction to React
- State Management with Redux
- Vue.js Basics
- Angular Basics
- Choosing the Right Framework
