Durant cinc mòduls has construït una aplicació sencera que ningú no pot veure. Tasca, Tauler, crearBacklog(), els errors de validació, les promeses, els generadors: tot funciona, tot està provat, i tot escup el seu resultat a la consola de les eines de desenvolupament. La Marta, l'Iván i la Lucía no obriran la consola. Necessiten una pàgina: una llista de tasques a la pantalla, amb els seus colors, els seus botons i el seu formulari. El pont entre el model que ja tens i aquella pantalla es diu DOM, el Model d'Objectes del Document, i és el que converteix un fitxer HTML en una estructura d'objectes que el teu JavaScript pot llegir i modificar. En aquesta lliçó encara no modificaràs res: entendràs què és exactament aquella estructura, com la construeix el navegador, de quins tipus de peces està feta, com es recorre i en quin moment exacte està llesta perquè el teu codi la toqui. Sense això, tot el que ve després són receptes que es copien sense saber per què funcionen.
Contingut
- Del text HTML a l'arbre de nodes
- El DOM no forma part del llenguatge JavaScript
windowidocument: l'objecte global i la porta d'entrada- Els tipus de node, i per què els espais en blanc compten
- Navegar per l'arbre: pares, fills i germans
HTMLCollectioniNodeList: col·leccions que no són arrays- Inspeccionar el DOM a les DevTools
- El cicle de vida de la pàgina:
DOMContentLoadediload - Per què
<script type="module">ja es comporta comdefer - Nómada Tasques: l'
index.htmlinicial - Errors Habituals i Consells
- Exercicis
- Conclusió
- Del text HTML a l'arbre de nodes
Quan el navegador rep un fitxer HTML, el que arriba per la xarxa és text pla. Una tira de caràcters:
Aquell text tot sol no serveix de res. El navegador l'analitza (en anglès, parse): el llegeix de cap a cap, reconeix les etiquetes d'obertura i tancament, i va construint en memòria una estructura d'objectes que representa el document. Aquella estructura és un arbre: cada etiqueta es converteix en un objecte (un node), i les etiquetes imbricades es converteixen en fills del node que les conté.
flowchart TD
DOC["document"] --> HTML["html"]
HTML --> HEAD["head"]
HTML --> BODY["body"]
HEAD --> META["meta charset"]
HEAD --> TITLE["title"]
HEAD --> LINK["link · estils.css"]
BODY --> HEADER["header.capcalera"]
BODY --> MAIN["main"]
BODY --> FOOTER["footer"]
BODY --> SCRIPT["script type=module"]
HEADER --> H1["h1 · Nómada Tasques"]
MAIN --> SECTION["section.tauler"]
SECTION --> H2["h2 · Backlog"]
SECTION --> P["p#resum"]
SECTION --> UL["ul#llista-tasques"]
UL --> LI1["li.tasca · id=1"]
UL --> LI2["li.tasca · id=2"]
UL --> LI3["li.tasca · id=3"]
D'aquest dibuix en surten tres idees importants:
- Hi ha exactament una arrel, l'objecte
document, del qual penja tota la resta.documentno és l'etiqueta<html>: és el document sencer, i<html>n'és l'únic fill element. - La relació d'imbricació del text es converteix en la relació pare/fill de l'arbre. Si a l'HTML escrius un
<li>dins d'un<ul>, al DOM el nodeliés fill del nodeul. Ni més ni menys. - L'arbre és un objecte viu, no una còpia del fitxer. Si el teu JavaScript hi afegeix un
<li>, l'arbre canvia i la pantalla es repinta. El fitxer.htmldel disc no es toca mai. En recarregar, el navegador torna a construir l'arbre a partir del text original i tots els teus canvis desapareixen.
Aquesta última idea és la que costa més al principi. El DOM no és el teu HTML: és el que el navegador ha construït a partir del teu HTML, i a partir d'aquell moment totes dues coses viuen vides separades.
El DOM tampoc no és el que es veu
Convé separar tres conceptes que es confonen constantment:
| Concepte | Què és | Qui el canvia |
|---|---|---|
| El fitxer HTML | Text al disc o al servidor | Tu, amb l'editor |
| El DOM | Arbre d'objectes en memòria | El navegador en carregar, i el teu JavaScript després |
| El que es veu a la pantalla | Píxels pintats a partir del DOM + el CSS | El navegador, quan el DOM o el CSS canvien |
El navegador combina l'arbre del DOM amb les regles de CSS per calcular un segon arbre (l'arbre de renderitzat), decideix on va cada caixa i pinta. Aquell procés té un cost, i és la raó que al Mòdul 9 hi hagi una lliçó sencera sobre manipular el DOM de manera eficient. Per ara queda't amb la cadena: HTML → DOM → CSS → píxels, i amb el fet que el teu JavaScript actua sobre la segona baula.
- El DOM no forma part del llenguatge JavaScript
A 01-01 va aparèixer una distinció que ara es torna central: JavaScript, el llenguatge, és el que defineix l'especificació ECMAScript. Allà hi ha let, les funcions, els objectes, Array, Promise, Map, les classes, els mòduls, els generadors… tot el que has après en cinc mòduls. En aquella especificació no hi apareix la paraula document ni una sola vegada.
El DOM és una API de l'entorn: una biblioteca d'objectes que el navegador posa a disposició del codi que executa, especificada a part pel WHATWG. Per això:
// Això funciona a qualsevol lloc: és el llenguatge.
const obertes = tasques.filter((t) => t.oberta);
// Això només funciona dins d'un navegador: és l'entorn.
document.querySelector('#llista-tasques');Si executes la segona línia a Node —el mateix motor V8, el mateix llenguatge— obtens ReferenceError: document is not defined. No és que Node tingui un JavaScript pitjor: és que Node no és un navegador i no té document per mostrar. A canvi et dona fs, process i altres APIs que el navegador no té.
flowchart TD
ES["ECMAScript · el llenguatge<br/>let · funcions · classes · Promise · Map"]
NAV["Entorn navegador<br/>document · window · addEventListener · fetch"]
NODE["Entorn Node<br/>fs · process · path · http"]
ES --> NAV
ES --> NODE
Aquesta separació té una conseqüència pràctica molt concreta que t'acompanyarà la resta del curs: el model de Nómada Tasques no ha de saber que existeix el DOM. Tasca i Tauler són JavaScript pur; funcionen igual al navegador, a Node i a les proves del Mòdul 8. Tota la part que parla amb la pàgina viurà en una carpeta nova, js/vista/, i aquella separació és el que farà el projecte provable i mantenible.
window i document: l'objecte global i la porta d'entrada
window i document: l'objecte global i la porta d'entradaEn un navegador, l'objecte global es diu window. Representa la pestanya (o la finestra, o l'iframe) i conté absolutament tot el que l'entorn ofereix: les APIs, les variables globals i també el mateix document.
console.log(typeof window); // 'object'
console.log(window === globalThis); // true ← el global estàndard del llenguatge
console.log(window.document === document); // true
console.log(window.innerWidth); // 1280 ← amplada útil de la finestra, en píxelsQue window sigui l'objecte global explica un detall històric: en escriure document, alert o setTimeout a seques, en realitat estàs accedint a window.document, window.alert i window.setTimeout. El prefix és opcional i gairebé ningú no l'escriu.
La divisió de la feina entre tots dos és senzilla:
| Objecte | De què s'ocupa | Exemples |
|---|---|---|
window |
La finestra i l'entorn | innerWidth, location, history, setTimeout, alert |
document |
El contingut de la pàgina | title, body, head, querySelector, createElement |
console.log(document.title); // 'Nómada Tasques · Taller Nómada'
console.log(document.body.tagName); // 'BODY'
console.log(document.nodeType); // 9 ← veurem què significa a l'apartat següentDues dreceres que el navegador ofereix i que convé conèixer: document.documentElement és el node <html>, document.head és el <head> i document.body és el <body>. Compte amb aquest últim: si el teu <script> s'executa abans que l'analitzador hagi arribat al <body>, document.body val null. És el primer avís del problema de temporització que resoldrem a l'apartat 8.
- Els tipus de node, i per què els espais en blanc compten
A l'arbre no tot són etiquetes. El DOM té diversos tipus de node, i cadascun té un número identificador accessible a la propietat nodeType. Aquests són els que et trobaràs:
nodeType |
Constant | Què representa | Exemple a l'HTML |
|---|---|---|---|
1 |
Node.ELEMENT_NODE |
Un element, és a dir, una etiqueta | <li class="tasca"> |
3 |
Node.TEXT_NODE |
Text, inclosos salts de línia i sagnies | Redissenyar la sala |
8 |
Node.COMMENT_NODE |
Un comentari HTML | <!-- pendent --> |
9 |
Node.DOCUMENT_NODE |
El document sencer | L'objecte document |
11 |
Node.DOCUMENT_FRAGMENT_NODE |
Un contenidor lleuger fora de l'arbre | DocumentFragment (lliçó 06-05) |
El que sorprèn a tothom és el tipus 3. Fixa't en aquest HTML, escrit com l'escriuria qualsevol:
<ul id="llista-tasques">
<li class="tasca">Redissenyar la sala polivalent</li>
<li class="tasca">Cartelleria del taller de serigrafia</li>
</ul>Entre <ul> i el primer <li> hi ha un salt de línia i dos espais. Per al navegador allò és un node de text, igual de real que els <li>. El mateix entre els dos <li> i entre l'últim i </ul>. Resultat:
const llista = document.getElementById('llista-tasques');
console.log(llista.childNodes.length); // 5 ← text, li, text, li, text
console.log(llista.children.length); // 2 ← només els elements
for (const node of llista.childNodes) {
console.log(node.nodeType, JSON.stringify(node.nodeName), JSON.stringify(node.textContent));
}
// 3 "#text" "\n "
// 1 "LI" "Redissenyar la sala polivalent"
// 3 "#text" "\n "
// 1 "LI" "Cartelleria del taller de serigrafia"
// 3 "#text" "\n"Cinc nodes fills on a simple vista n'hi ha dos. Aquesta és la causa número u de bugs de principiant en recórrer el DOM: llista.firstChild no és el primer <li>, és un node de text amb un salt de línia i dos espais. D'aquí que existeixi una família paral·lela de propietats que ignoren tot el que no sigui un element, i que és la que faràs servir el 99 % de les vegades.
Tres propietats de node que convé distingir:
nodeType: el número de la taula.nodeName: el nom del node. Per a elements és l'etiqueta en majúscules ('LI'); per a text,'#text'; per al document,'#document'.tagName: només existeix en elements, i és l'etiqueta en majúscules. Si dubtes entrenodeNameitagName, fes servirtagNamequan sàpigues que tens un element.
- Navegar per l'arbre: pares, fills i germans
Cada node coneix els seus veïns. Aquestes són les propietats de navegació, agrupades pel que retornen:
| Propietat | Retorna | Inclou nodes de text? |
|---|---|---|
parentNode |
El node pare | Sí (pot ser el document) |
parentElement |
L'element pare | No (retorna null si el pare no és un element) |
childNodes |
Tots els fills, com a NodeList viva |
Sí |
children |
Els fills element, com a HTMLCollection viva |
No |
firstChild / lastChild |
Primer i últim fill | Sí |
firstElementChild / lastElementChild |
Primer i últim fill element | No |
previousSibling / nextSibling |
Germà anterior/següent | Sí |
previousElementSibling / nextElementSibling |
Germà element anterior/següent | No |
childElementCount |
Nombre de fills element | No |
La regla mnemotècnica és senzilla: si el nom porta Element, ignora el text i els comentaris. I aquestes són les que vols gairebé sempre.
Amb l'HTML de l'apartat anterior:
const llista = document.getElementById('llista-tasques');
const primera = llista.firstElementChild;
console.log(primera.textContent); // 'Redissenyar la sala polivalent'
const segona = primera.nextElementSibling;
console.log(segona.textContent); // 'Cartelleria del taller de serigrafia'
console.log(segona.nextElementSibling); // null ← no hi ha tercera
console.log(primera.parentElement === llista); // true
console.log(llista.parentElement.tagName); // 'SECTION'Pujar per l'arbre també funciona, i encadena tantes vegades com vulguis:
I aquí un consell important: encadenar parentElement és fràgil. Si demà embolcalles la llista en un <div> per motius de disseny, totes aquelles cadenes es trenquen en silenci. A la lliçó següent aprendràs closest(), que puja buscant el primer avantpassat que compleixi un selector, i que és la manera robusta de fer això mateix.
Un exemple real, comptant quants nodes hi ha a l'arbre complet amb la recursivitat de 03-07:
/** Compta nodes de l'arbre, separant elements, text i comentaris. */
function censarNodes(node, compte = { elements: 0, text: 0, comentaris: 0 }) {
if (node.nodeType === Node.ELEMENT_NODE) compte.elements += 1;
else if (node.nodeType === Node.TEXT_NODE) compte.text += 1;
else if (node.nodeType === Node.COMMENT_NODE) compte.comentaris += 1;
for (const fill of node.childNodes) censarNodes(fill, compte);
return compte;
}
console.log(censarNodes(document.body));
// { elements: 24, text: 31, comentaris: 1 } ← els números depenen del teu HTMLQue hi hagi més nodes de text que elements en un HTML ben sagnat no és un error: és exactament el que explicava l'apartat 4.
HTMLCollection i NodeList: col·leccions que no són arrays
HTMLCollection i NodeList: col·leccions que no són arraysllista.children sembla un array: té length, s'hi accedeix amb claudàtors i conté elements. No és un array. És una HTMLCollection, i llista.childNodes és una NodeList. Cap de les dues no hereta d'Array.prototype, així que no tenen map, filter, reduce ni find.
const items = llista.children; // HTMLCollection
console.log(items.length); // 2 ✓
console.log(items[0].tagName); // 'LI' ✓
console.log(Array.isArray(items)); // false ← compte
console.log(items.map); // undefined
// items.map((li) => li.textContent);
// ✗ TypeError: items.map is not a functionLa diferència crítica entre totes dues no és només què contenen, sinó si estan vives: una col·lecció viva s'actualitza sola quan el DOM canvia; una d'estàtica és una foto fixa del moment en què es va crear.
| Col·lecció | La retornen | Conté | Viva? | forEach |
|---|---|---|---|---|
HTMLCollection |
children, getElementsByClassName, getElementsByTagName |
Només elements | Sí | No |
NodeList viva |
childNodes |
Qualsevol node | Sí | Sí |
NodeList estàtica |
querySelectorAll (lliçó 06-02) |
Qualsevol node | No | Sí |
Que una col·lecció estigui viva sona còmode i en realitat és una font clàssica de bucles infinits:
const items = llista.children; // viva
for (let i = 0; i < items.length; i++) {
llista.append(document.createElement('li')); // ← cada volta hi afegeix un fill…
}
// items.length creix a cada iteració: el bucle no acaba mai.La solució, i la pràctica recomanada en general, és convertir a array tan bon punt l'obtens, fent servir Array.from o el spread que vas aprendre a 04-03 i 04-07:
const items = Array.from(llista.children); // array de debò, estàtic
// o bé
const items2 = [...llista.children];
const titols = items
.filter((li) => !li.classList.contains('tasca--feta'))
.map((li) => li.textContent.trim());
console.log(titols);
// ['Redissenyar la sala polivalent', 'Cartelleria del taller de serigrafia']Totes dues conversions funcionen perquè tant HTMLCollection com NodeList són iterables (implementen Symbol.iterator, exactament el protocol de 05-08). Per això també les pots recórrer amb for...of sense convertir res:
Array.from té a més un segon paràmetre que fa de map a la mateixa passada, i que estalvia un array intermedi:
- Inspeccionar el DOM a les DevTools
Tot això es veu molt millor tocant-ho. Obre index.html amb Live Server, prem F12 i ves a la pestanya Elements (Elements).
El que hi veus no és el teu fitxer HTML: és una representació del DOM actual. Si el teu JavaScript hi afegeix un <li>, apareixerà en aquell plafó encara que no sigui al fitxer. Coses que pots fer i que convé practicar avui mateix:
- Desplegar l'arbre amb els triangles, i comprovar que la jerarquia coincideix amb el diagrama de l'apartat 1.
- Seleccionar un element fent-hi clic. En veuràs els estils aplicats al plafó de la dreta i, molt útil, la ruta de molla de pa a la part inferior:
body > main > section.tauler > ul#llista-tasques > li.tasca. - Editar en calent: doble clic sobre un text o un atribut per canviar-lo. Els canvis es veuen a l'instant i es perden en recarregar. És el laboratori perfecte per provar una classe CSS abans d'escriure el JavaScript que l'aplica.
- Fer servir
$0a la consola: després de seleccionar un element al plafó Elements, la variable$0a la consola apunta a aquell element.$0.children,$0.tagName,$0.classList… és la manera més ràpida d'explorar. console.dir(element)davant deconsole.log(element): el primer et mostra l'element com a objecte, amb totes les seves propietats desplegables; el segon te'l mostra com a HTML. Per aprendre quines propietats té un node,console.dirés infinitament millor.
const llista = document.getElementById('llista-tasques');
console.log(llista); // <ul id="llista-tasques">…</ul> ← vista HTML
console.dir(llista); // ul#llista-tasques { children: …, id: …, … } ← vista objecte
- El cicle de vida de la pàgina:
DOMContentLoaded i load
DOMContentLoaded i loadAquí hi ha el problema de temporització més clàssic de tots. El navegador analitza l'HTML de dalt a baix, i executa un <script> clàssic en el moment exacte en què el troba. Si l'script és al <head>, quan s'executa el <body> encara no existeix:
<head>
<script>
// L'analitzador encara no ha arribat al body.
console.log(document.getElementById('llista-tasques')); // null ✗
</script>
</head>No dona error: dona null, que és pitjor, perquè la fallada apareix dues línies més avall amb un TypeError: Cannot read properties of null.
Hi ha dos moments amb nom en la càrrega d'una pàgina:
| Esdeveniment | Quan es dispara | Què garanteix |
|---|---|---|
DOMContentLoaded |
Quan l'HTML s'ha analitzat del tot i l'arbre del DOM està construït | Tots els elements existeixen |
load (a window) |
Quan a més han acabat de descarregar-se imatges, fulls d'estil, tipografies i iframes | Tot està descarregat i amb la seva mida real |
document.addEventListener('DOMContentLoaded', () => {
console.log('DOM llest:', document.querySelectorAll('li.tasca').length, 'tasques');
});
window.addEventListener('load', () => {
console.log('Tot descarregat, imatges incloses');
});La regla és clara: per treballar amb el DOM en tens prou amb DOMContentLoaded; esperar load endarrereix l'arrencada de l'aplicació fins que es descarregui l'última imatge, i gairebé mai no cal. Només necessites load si depens de les dimensions reals d'una imatge o d'un recurs extern.
flowchart LR
A["Arriba l'HTML"] --> B["El navegador analitza<br/>i construeix el DOM"]
B --> C["DOMContentLoaded<br/>l'arbre és complet"]
C --> D["Descàrrega d'imatges,<br/>tipografies, iframes"]
D --> E["load<br/>tot està llest"]
- Per què
<script type="module"> ja es comporta com defer
<script type="module"> ja es comporta com deferLa solució històrica al problema anterior era col·locar el <script> just abans de </body>, o afegir-li l'atribut defer, que diu al navegador: «descarrega aquest fitxer en paral·lel, però no l'executis fins que el DOM estigui construït».
I aquí arriba la bona notícia, que enllaça directament amb 05-04: un <script type="module"> és defer per defecte. No cal escriure res. Un mòdul:
- Es descarrega en paral·lel sense bloquejar l'analitzador d'HTML.
- S'executa després que el document estigui completament analitzat.
- S'executa abans de l'esdeveniment
DOMContentLoaded.
| Manera d'incloure l'script | Bloqueja l'anàlisi? | Quan s'executa? | El DOM està llest? |
|---|---|---|---|
<script> al <head> |
Sí | En trobar-lo | No |
<script> abans de </body> |
Sí, però al final | En trobar-lo | Sí |
<script defer> |
No | Després d'analitzar el document | Sí |
<script type="module"> |
No | Després d'analitzar el document | Sí |
<script async> |
No | Tan bon punt es descarregui, en qualsevol ordre | No garantit |
Com que Nómada Tasques ja fa servir <script type="module" src="js/app.js"></script>, el teu app.js pot seleccionar elements des de la primera línia sense embolcallar res en un gestor de DOMContentLoaded. Això és una d'aquelles coses que es veuen en mil tutorials antics i que avui sobra:
// js/app.js — mòdul: això ja és segur, sense embolcalls
const llista = document.getElementById('llista-tasques');
console.log(llista.children.length); // 3 ✓Continua havent-hi un cas en què necessites l'esdeveniment: si el teu mòdul fa await d'alguna cosa lenta abans de tocar el DOM, o si escrius codi que es pot executar tant com a mòdul com en un <script> clàssic. Per a aquests casos, el patró defensiu és comprovar document.readyState:
function enEstarLlest(fn) {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', fn, { once: true });
} else {
fn(); // el DOM ja estava llest: executa ja
}
}document.readyState passa per tres valors: 'loading' mentre s'analitza, 'interactive' quan el DOM està llest (just abans de DOMContentLoaded) i 'complete' després de l'esdeveniment load.
- Nómada Tasques: l'
index.html inicial
index.html inicialÉs el moment d'escriure la pàgina. Aquest HTML és la base sobre la qual creixerà tot el mòdul: a 06-02 pintaràs aquests elements, a 06-03 els posaràs esdeveniments, a 06-05 els crearàs des de JavaScript i a 06-07 completaràs el formulari. De moment les tres tasques estan escrites a mà per tenir alguna cosa a inspeccionar.
<!DOCTYPE html>
<html lang="ca">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Nómada Tasques · Taller Nómada</title>
<link rel="stylesheet" href="css/estils.css">
</head>
<body>
<header class="capcalera">
<h1>Nómada Tasques</h1>
<p class="capcalera__sub">
Taller Nómada · <time datetime="2026-09-20">20 de setembre del 2026</time>
</p>
</header>
<main>
<section class="tauler" aria-labelledby="titol-tauler">
<h2 id="titol-tauler">Backlog</h2>
<p id="resum" class="resum" role="status">Carregant el tauler…</p>
<ul id="llista-tasques" class="llista-tasques">
<li class="tasca" data-id="1">
<span class="tasca__titol">Redissenyar la sala polivalent</span>
<span class="tasca__meta">Iván · 12 h · en-curs</span>
</li>
<li class="tasca" data-id="2">
<span class="tasca__titol">Cartelleria del taller de serigrafia</span>
<span class="tasca__meta">Marta · 6 h · pendent</span>
</li>
<li class="tasca" data-id="6">
<span class="tasca__titol">Pressupost de la fusteria</span>
<span class="tasca__meta">Iván · 5 h · pendent</span>
</li>
</ul>
</section>
<section aria-labelledby="titol-nova">
<h2 id="titol-nova">Nova tasca</h2>
<form id="nova-tasca" class="formulari" novalidate>
<p>El formulari es completa a la lliçó 06-07.</p>
</form>
</section>
</main>
<footer class="peu">
<p>Taller Nómada · coworking, serigrafia, enquadernació i fusteria</p>
</footer>
<script type="module" src="js/app.js"></script>
</body>
</html>Val la pena justificar cada decisió, perquè no són decoratives:
<html lang="ca">: els lectors de pantalla trien la veu i la pronunciació a partir d'aquest atribut. Sense ell, un lector configurat en anglès llegirà «Pressupost de la fusteria» amb fonètica anglesa.- HTML semàntic:
<header>,<main>,<section>,<footer>en lloc de<div>per a tot. Les tecnologies d'assistència exposen aquests elements com a punts de referència navegables; un usuari de lector de pantalla pot saltar directament al contingut principal. aria-labelledbya cada<section>: dona a la secció un nom accessible pres del<h2>. Una secció sense nom no apareix a la llista de punts de referència.<ul>amb<li>: una llista de tasques és una llista. El lector de pantalla anunciarà «llista de 3 elements», informació que un munt de<div>no donaria.role="status"al paràgraf de resum: quan el seu text canviï des de JavaScript, el lector de pantalla l'anunciarà sense robar el focus. És la manera correcta de comunicar «45 h obertes» a qui no veu la pantalla.data-ida cada<li>: és la peça clau de la resta del mòdul. Connecta cada element de la pàgina amb l'idde la sevaTascaal model. A 06-02 el llegiràs ambdataset, i a 06-04 serà la base de la delegació d'esdeveniments.<time datetime="2026-09-20">: la data llegible per a les persones i la data canònica per a les màquines, al mateix element.
I el CSS mínim. La consigna del mòdul és «només el just»: classes amb noms clars, sense floritures.
/* css/estils.css */
:root {
--color-alta: #c0392b;
--color-mitjana: #b7791f;
--color-baixa: #2b7a78;
--gris: #6b7280;
--vora: #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;
}
.capcalera__sub { color: var(--gris); margin-top: -0.5rem; }
.resum { font-weight: 600; }
.tauler { border: 1px solid var(--vora); border-radius: 0.5rem; padding: 1rem; }
.llista-tasques { list-style: none; padding: 0; margin: 0; }
.tasca {
display: flex;
justify-content: space-between;
gap: 1rem;
padding: 0.6rem 0.8rem;
border-left: 4px solid var(--gris);
border-bottom: 1px solid var(--vora);
}
.tasca--alta { border-left-color: var(--color-alta); }
.tasca--mitjana { border-left-color: var(--color-mitjana); }
.tasca--baixa { border-left-color: var(--color-baixa); }
.tasca--feta .tasca__titol { text-decoration: line-through; color: var(--gris); }
.tasca--vencuda .tasca__meta { color: var(--color-alta); font-weight: 600; }
.tasca__meta { color: var(--gris); font-size: 0.9rem; white-space: nowrap; }
/* El focus visible és obligatori: qui navega amb teclat necessita veure'l. */
:focus-visible { outline: 3px solid #2563eb; outline-offset: 2px; }Fixa't en la convenció de noms: .tasca és el bloc, .tasca__titol una part seva i .tasca--alta una variant. Aquella disciplina farà que a 06-02, quan manipulis classes amb classList, sàpigues exactament què afegir i què treure.
I l'app.js d'aquesta lliçó, que encara no modifica res: només comprova que l'arbre és el que esperem.
// js/app.js
import { Tauler } from './model/tauler.js';
import { crearBacklog } from './dades/backlog.js';
import { AVUI } from './util/dates.js';
const tauler = new Tauler('Taller Nómada', crearBacklog());
const resum = tauler.resum(AVUI);
console.log(resum);
// { total: 6, obertes: 5, horesTotals: 48, horesObertes: 45, vencudes: 1, esforc: 124 }
// El DOM ja està construït: és un mòdul, i els mòduls són 'defer'.
const llista = document.getElementById('llista-tasques');
console.log('Elements <li> a la pàgina:', llista.children.length); // 3
console.log('Nodes fills totals:', llista.childNodes.length); // 7 (els texts compten)
for (const li of llista.children) {
console.log(li.dataset.id, '→', li.firstElementChild.textContent);
}
// 1 → Redissenyar la sala polivalent
// 2 → Cartelleria del taller de serigrafia
// 6 → Pressupost de la fusteriaEls números canònics del model (48 h totals, 45 obertes, 1 vençuda, esforç 124) segueixen intactes, i ara conviuen a la mateixa pàgina amb tres <li> escrits a mà. Tancar aquella escletxa —que la llista de la pàgina surti del Tauler i no de l'HTML— és la feina de les sis lliçons següents.
Errors Habituals i Consells
- Creure que
documentés part de JavaScript. No ho és, i per això el mateix codi falla a Node. Mantén el model (model/,dades/,util/) lliure de qualsevol referència al DOM: t'ho agrairàs al Mòdul 8, quan escriguis proves que s'executen sense navegador. - Confondre
childNodesambchildren. És el parany clàssic:childNodesinclou els nodes de text que genera la teva pròpia sagnia. Fes servir sempre la família ambElemental nom (children,firstElementChild,nextElementSibling) tret que de debò necessitis el text. - Tractar una
HTMLCollectioncom un array. No témap,filternireduce. Converteix ambArray.from(colleccio)o[...colleccio]tan bon punt l'obtinguis; a més així deixes de tenir una col·lecció viva que canvia sota els teus peus. - Modificar el DOM mentre recorres una col·lecció viva. El bucle pot no acabar mai, o saltar-se elements. Convertir a array abans del bucle elimina el problema d'arrel.
- Posar un
<script>clàssic al<head>sensedefer. Tots elsgetElementByIdretornarannull. Ambtype="module"el problema desapareix; si tot i així el teu codi es pot executar en contextos diferents, fes servir el patróenEstarLlestde l'apartat 9. - Esperar
loadquan n'hi ha prou ambDOMContentLoaded. Endarrereixes l'arrencada fins que es descarregui l'últim recurs extern. Esperaloadnomés si necessites dimensions reals d'imatges. - Consell: fes servir
console.dir(element)per explorar.console.logt'ensenya l'HTML;console.dirt'ensenya l'objecte amb totes les seves propietats. I combina el plafó Elements amb$0a la consola: és la manera més ràpida de descobrir l'API sense buscar a la documentació. - Consell: escriu HTML semàntic des del primer minut. Afegir accessibilitat al final és caríssim; escriure-la bé des del principi no costa res. Un
<ul>de<li>ambaria-labelledbya la secció ja et dona mitja accessibilitat feta.
Exercicis
Exercici 1 · El cens de l'arbre
Escriu a js/app.js una funció descriureArbre(arrel, nivell = 0) que imprimeixi per consola l'arbre d'elements que penja d'arrel, amb una sagnia de dos espais per nivell, mostrant l'etiqueta en minúscules, l'id si en té i les classes si en té. Ha d'ignorar els nodes de text i de comentari. Aplica-la a document.body.
Sortida esperada (retallada):
Exercici 2 · Tres maneres d'arribar al mateix lloc
Partint del <li> amb data-id="2", obtén l'element <section class="tauler"> de tres maneres diferents: (a) encadenant parentElement; (b) amb un bucle while que pugi fins a trobar un element la llista de classes del qual contingui tauler; (c) explicant, sense escriure el codi, quin mètode de la lliçó següent resoldria això en una línia. Comenta quina de les dues primeres et sembla més robusta i per què.
Exercici 3 · Titulars de les tasques obertes
Sense fer servir querySelector (encara no l'hem vist), obtén un array amb els títols dels <li> de la llista el text de meta dels quals no contingui 'feta'. Fes servir Array.from, filter i map. Després imprimeix quants n'hi ha i comprova que el resultat continua sent un array de debò amb Array.isArray.
Solucions
Exercici 1
function descriureArbre(arrel, nivell = 0) {
const sagnia = ' '.repeat(nivell);
const id = arrel.id ? ` #${arrel.id}` : '';
const classes = arrel.classList.length ? ' .' + [...arrel.classList].join(' .') : '';
console.log(`${sagnia}${arrel.tagName.toLowerCase()}${id}${classes}`);
for (const fill of arrel.children) { // 'children' ja filtra text i comentaris
descriureArbre(fill, nivell + 1);
}
}
descriureArbre(document.body);Punts clau: es recorre children (no childNodes), així que no cal comprovar nodeType; la recursivitat és la de 03-07, amb el nivell com a acumulador; i [...arrel.classList] funciona perquè classList també és iterable. Si un element no té classes, classList.length és 0 i la cadena queda buida en lloc de mostrar un punt solt.
Exercici 2
const li = document.getElementById('llista-tasques').children[1];
// (a) Encadenant parentElement
const seccioA = li.parentElement.parentElement;
console.log(seccioA.tagName, seccioA.className); // SECTION tauler
// (b) Pujant amb un bucle fins a trobar la classe
function pujarFinsA(node, classe) {
let actual = node;
while (actual !== null && !actual.classList?.contains(classe)) {
actual = actual.parentElement;
}
return actual;
}
const seccioB = pujarFinsA(li, 'tauler');
console.log(seccioA === seccioB); // true(c) li.closest('.tauler') fa exactament el mateix que la versió (b) en una sola crida, i el veuràs a 06-02.
La versió (b) és clarament més robusta. La (a) codifica una distància exacta a l'arbre: si demà embolcalles el <ul> en un <div class="scroll">, seccioA passarà a ser el <div> i ningú no se n'assabentarà fins que alguna cosa es trenqui dues pantalles més enllà. La (b) codifica una intenció («puja fins al tauler»), i sobreviu als canvis de maquetació. El ?. abans de contains és la protecció de 01-06: quan el bucle arriba a document, classList és undefined i sense l'encadenament opcional hi hauria un TypeError.
Exercici 3
const llista = document.getElementById('llista-tasques');
const obertes = Array.from(llista.children)
.filter((li) => !li.lastElementChild.textContent.includes('feta'))
.map((li) => li.firstElementChild.textContent.trim());
console.log(obertes);
// ['Redissenyar la sala polivalent', 'Cartelleria del taller de serigrafia', 'Pressupost de la fusteria']
console.log(obertes.length); // 3
console.log(Array.isArray(obertes)); // trueL'essencial: sense l'Array.from inicial, la línia del filter llançaria TypeError: llista.children.filter is not a function, perquè children és una HTMLCollection. El .trim() neteja els salts de línia i la sagnia que l'HTML introdueix dins del <span>, exactament els nodes de text de l'apartat 4. I fixa't en com de fràgil resulta llegir l'estat d'una tasca buscant la subcadena 'feta' dins d'un text: és just el que deixaràs de fer a la lliçó següent, quan desis aquella informació en atributs data-* i en classes en lloc de en prosa.
Conclusió
Ja saps què és el DOM i, gairebé més important, què no és. No és el teu fitxer HTML, sinó l'arbre d'objectes que el navegador construeix a partir d'ell i que a partir d'aquell moment viu pel seu compte en memòria. No és part del llenguatge JavaScript, sinó una API que aporta l'entorn del navegador: per això document no existeix a Node i per això el model de Nómada Tasques —Tasca, Tauler, crearBacklog— ha de continuar sense esmentar-lo mai. I no és el que es veu: el que es veu són píxels que el navegador pinta combinant el DOM amb el CSS, en una cadena HTML → DOM → CSS → píxels sobre la qual tu actues a la segona baula.
Coneixes l'anatomia de l'arbre: window com a objecte global i document com a porta d'entrada al contingut; els tipus de node amb els seus nodeType (1 element, 3 text, 8 comentari, 9 document) i la sorpresa que la teva pròpia sagnia genera nodes de text reals, que és la raó que childNodes retorni cinc fills on children en retorna dos. Saps moure't entre nodes amb parentElement, children, firstElementChild i nextElementSibling, amb la regla d'or de preferir sempre les propietats que porten Element al nom. I saps que les col·leccions que retorna el DOM —HTMLCollection i NodeList— no són arrays: són iterables, així que for...of funciona, però per fer servir map i filter cal passar per Array.from o el spread, cosa que a més t'estalvia els ensurts de les col·leccions vives.
També tens resolt el problema de temporització que arruïna el primer dia de tanta gent: l'analitzador construeix l'arbre de dalt a baix, DOMContentLoaded avisa que ja és complet, load espera a més les imatges, i un <script type="module"> ja es comporta com defer, de manera que el teu app.js pot seleccionar elements des de la primera línia. I tens escrit l'index.html de Nómada Tasques amb estructura semàntica de debò —header, main, section.tauler, ul#llista-tasques, form#nova-tasca, role="status", aria-labelledby— juntament amb un CSS mínim de classes clares (.tasca, .tasca--alta, .tasca--feta, .tasca--vencuda) i, sobretot, un data-id a cada <li> que serà la frontissa entre la pantalla i el model durant tot el mòdul.
El que encara no saps fer és trobar un element concret sense anar saltant de pare a fill, i canviar-lo. Recórrer l'arbre a mà amb firstElementChild.nextElementSibling.parentElement funciona, però és exactament el tipus de codi fràgil que es trenca en moure un <div>. Hi ha una manera molt millor: descriure el que busques amb un selector CSS —'.tasca--alta', '#llista-tasques > li', '[data-id="6"]'— i deixar que el navegador el trobi. Això, juntament amb la manera correcta de canviar textos, atributs, classes i estils (i per què innerHTML és un forat de seguretat esperant a passar), és Selecció i Manipulació d'Elements del DOM, on per fi pintaràs a la pantalla l'estat i la prioritat reals de les tasques de Taller Nómada.
Curs de JavaScript: De Principiant a Avançat
Mòdul 1: Introducció a JavaScript
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
