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

  1. Del text HTML a l'arbre de nodes
  2. El DOM no forma part del llenguatge JavaScript
  3. window i document: l'objecte global i la porta d'entrada
  4. Els tipus de node, i per què els espais en blanc compten
  5. Navegar per l'arbre: pares, fills i germans
  6. HTMLCollection i NodeList: col·leccions que no són arrays
  7. Inspeccionar el DOM a les DevTools
  8. El cicle de vida de la pàgina: DOMContentLoaded i load
  9. Per què <script type="module"> ja es comporta com defer
  10. Nómada Tasques: l'index.html inicial
  11. Errors Habituals i Consells
  12. Exercicis
  13. Conclusió

  1. 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:

<ul id="llista-tasques"><li class="tasca">Redissenyar la sala polivalent</li></ul>

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. document no é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 node li és fill del node ul. 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 .html del 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.

  1. 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.

  1. window i document: l'objecte global i la porta d'entrada

En 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íxels

Que 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üent

Dues 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.

  1. 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 entre nodeName i tagName, fes servir tagName quan sàpigues que tens un element.

  1. 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
children Els fills element, com a HTMLCollection viva No
firstChild / lastChild Primer i últim fill
firstElementChild / lastElementChild Primer i últim fill element No
previousSibling / nextSibling Germà anterior/següent
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:

console.log(primera.parentElement.parentElement.tagName); // 'SECTION' → 'MAIN'? compte

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 HTML

Que 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.

  1. HTMLCollection i NodeList: col·leccions que no són arrays

llista.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 function

La 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 No
NodeList viva childNodes Qualsevol node
NodeList estàtica querySelectorAll (lliçó 06-02) Qualsevol node No

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:

for (const li of llista.children) {
  console.log(li.textContent.trim());
}

Array.from té a més un segon paràmetre que fa de map a la mateixa passada, i que estalvia un array intermedi:

const titols = Array.from(llista.children, (li) => li.textContent.trim());

  1. 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 $0 a la consola: després de seleccionar un element al plafó Elements, la variable $0 a 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 de console.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

  1. El cicle de vida de la pàgina: DOMContentLoaded i load

Aquí 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"]

  1. Per què <script type="module"> ja es comporta com defer

La 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:

  1. Es descarrega en paral·lel sense bloquejar l'analitzador d'HTML.
  2. S'executa després que el document estigui completament analitzat.
  3. 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> En trobar-lo No
<script> abans de </body> Sí, però al final En trobar-lo
<script defer> No Després d'analitzar el document
<script type="module"> No Després d'analitzar el document
<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.

  1. Nómada Tasques: l'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-labelledby a 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-id a cada <li>: és la peça clau de la resta del mòdul. Connecta cada element de la pàgina amb l'id de la seva Tasca al model. A 06-02 el llegiràs amb dataset, 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 fusteria

Els 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 childNodes amb children. És el parany clàssic: childNodes inclou els nodes de text que genera la teva pròpia sagnia. Fes servir sempre la família amb Element al nom (children, firstElementChild, nextElementSibling) tret que de debò necessitis el text.
  • Tractar una HTMLCollection com un array. No té map, filter ni reduce. Converteix amb Array.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> sense defer. Tots els getElementById retornaran null. Amb type="module" el problema desapareix; si tot i així el teu codi es pot executar en contextos diferents, fes servir el patró enEstarLlest de l'apartat 9.
  • Esperar load quan n'hi ha prou amb DOMContentLoaded. Endarrereixes l'arrencada fins que es descarregui l'últim recurs extern. Espera load només si necessites dimensions reals d'imatges.
  • Consell: fes servir console.dir(element) per explorar. console.log t'ensenya l'HTML; console.dir t'ensenya l'objecte amb totes les seves propietats. I combina el plafó Elements amb $0 a 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> amb aria-labelledby a 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):

body
  header .capcalera
    h1
    p .capcalera__sub
      time
  main
    section .tauler

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)); // true

L'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 NodeListno 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

Mòdul 2: Estructures de Control

Mòdul 3: Funcions

Mòdul 4: Objectes i Arrays

Mòdul 5: Objectes i Funcions Avançades

Mòdul 6: El Model d'Objectes del Document (DOM)

Mòdul 7: APIs del Navegador i Temes Avançats

Mòdul 8: Proves i Depuració

Mòdul 9: Rendiment i Optimització

Mòdul 10: Frameworks i Llibreries de JavaScript

Mòdul 11: Projecte Final

© Copyright 2026. Tots els drets reservats