Durante cinco módulos has construido una aplicación entera que nadie puede ver. Tarea, Tablero, crearBacklog(), los errores de validación, las promesas, los generadores: todo funciona, todo está probado, y todo escupe su resultado en la consola de las herramientas de desarrollo. Marta, Iván y Lucía no van a abrir la consola. Necesitan una página: una lista de tareas en la pantalla, con sus colores, sus botones y su formulario. El puente entre el modelo que ya tienes y esa pantalla se llama DOM, el Modelo de Objetos del Documento, y es lo que convierte un fichero HTML en una estructura de objetos que tu JavaScript puede leer y modificar. En esta lección no vas a modificar nada todavía: vas a entender qué es exactamente esa estructura, cómo la construye el navegador, de qué tipos de piezas está hecha, cómo se recorre y en qué momento exacto está lista para que tu código la toque. Sin esto, todo lo que viene después son recetas que se copian sin saber por qué funcionan.

Contenido

  1. Del texto HTML al árbol de nodos
  2. El DOM no forma parte del lenguaje JavaScript
  3. window y document: el objeto global y la puerta de entrada
  4. Los tipos de nodo, y por qué los espacios en blanco cuentan
  5. Navegar por el árbol: padres, hijos y hermanos
  6. HTMLCollection y NodeList: colecciones que no son arrays
  7. Inspeccionar el DOM en las DevTools
  8. El ciclo de vida de la página: DOMContentLoaded y load
  9. Por qué <script type="module"> ya se comporta como defer
  10. Nómada Tareas: el index.html inicial
  11. Errores Comunes y Consejos
  12. Ejercicios
  13. Conclusión

  1. Del texto HTML al árbol de nodos

Cuando el navegador recibe un fichero HTML, lo que llega por la red es texto plano. Una tira de caracteres:

<ul id="lista-tareas"><li class="tarea">Rediseñar la sala polivalente</li></ul>

Ese texto no sirve para nada por sí solo. El navegador lo analiza (en inglés, parse): lo lee de principio a fin, reconoce las etiquetas de apertura y cierre, y va construyendo en memoria una estructura de objetos que representa el documento. Esa estructura es un árbol: cada etiqueta se convierte en un objeto (un nodo), y las etiquetas anidadas se convierten en hijos del nodo que las contiene.

flowchart TD
    DOC["document"] --> HTML["html"]
    HTML --> HEAD["head"]
    HTML --> BODY["body"]
    HEAD --> META["meta charset"]
    HEAD --> TITLE["title"]
    HEAD --> LINK["link · estilos.css"]
    BODY --> HEADER["header.cabecera"]
    BODY --> MAIN["main"]
    BODY --> FOOTER["footer"]
    BODY --> SCRIPT["script type=module"]
    HEADER --> H1["h1 · Nómada Tareas"]
    MAIN --> SECTION["section.tablero"]
    SECTION --> H2["h2 · Backlog"]
    SECTION --> P["p#resumen"]
    SECTION --> UL["ul#lista-tareas"]
    UL --> LI1["li.tarea · id=1"]
    UL --> LI2["li.tarea · id=2"]
    UL --> LI3["li.tarea · id=3"]

Tres ideas importantes salen de este dibujo:

  • Hay exactamente una raíz, el objeto document, del que cuelga todo lo demás. document no es la etiqueta <html>: es el documento entero, y <html> es su único hijo elemento.
  • La relación de anidamiento del texto se convierte en la relación padre/hijo del árbol. Si en el HTML escribes un <li> dentro de un <ul>, en el DOM el nodo li es hijo del nodo ul. Ni más ni menos.
  • El árbol es un objeto vivo, no una copia del fichero. Si tu JavaScript añade un <li>, el árbol cambia y la pantalla se repinta. El fichero .html del disco no se toca nunca. Al recargar, el navegador vuelve a construir el árbol desde el texto original y todos tus cambios desaparecen.

Esa última idea es la que más cuesta al principio. El DOM no es tu HTML: es lo que el navegador ha construido a partir de tu HTML, y a partir de ese momento las dos cosas viven vidas separadas.

El DOM tampoco es lo que se ve

Conviene separar tres conceptos que se confunden constantemente:

Concepto Qué es Quién lo cambia
El fichero HTML Texto en el disco o en el servidor Tú, con el editor
El DOM Árbol de objetos en memoria El navegador al cargar, y tu JavaScript después
Lo que se ve en pantalla Píxeles pintados a partir del DOM + el CSS El navegador, cuando el DOM o el CSS cambian

El navegador combina el árbol del DOM con las reglas de CSS para calcular un segundo árbol (el árbol de renderizado), decide dónde va cada caja y pinta. Ese proceso tiene un coste, y es la razón de que en el Módulo 9 haya una lección entera sobre manipular el DOM de forma eficiente. Por ahora quédate con la cadena: HTML → DOM → CSS → píxeles, y con que tu JavaScript actúa sobre el segundo eslabón.

  1. El DOM no forma parte del lenguaje JavaScript

En 01-01 apareció una distinción que ahora se vuelve central: JavaScript, el lenguaje, es lo que define la especificación ECMAScript. Ahí están let, las funciones, los objetos, Array, Promise, Map, las clases, los módulos, los generadores… todo lo que has aprendido en cinco módulos. En esa especificación no aparece la palabra document ni una sola vez.

El DOM es una API del entorno: una biblioteca de objetos que el navegador pone a disposición del código que ejecuta, especificada aparte por el WHATWG. Por eso:

// Esto funciona en cualquier sitio: es el lenguaje.
const abiertas = tareas.filter((t) => t.abierta);

// Esto solo funciona dentro de un navegador: es el entorno.
document.querySelector('#lista-tareas');

Si ejecutas la segunda línea en Node —el mismo motor V8, el mismo lenguaje— obtienes ReferenceError: document is not defined. No es que Node tenga un JavaScript peor: es que Node no es un navegador y no tiene documento que mostrar. A cambio te da fs, process y otras APIs que el navegador no tiene.

flowchart TD
    ES["ECMAScript · el lenguaje<br/>let · funciones · clases · Promise · Map"]
    NAV["Entorno navegador<br/>document · window · addEventListener · fetch"]
    NODE["Entorno Node<br/>fs · process · path · http"]
    ES --> NAV
    ES --> NODE

Esta separación tiene una consecuencia práctica muy concreta que te acompañará el resto del curso: el modelo de Nómada Tareas no debe saber que existe el DOM. Tarea y Tablero son JavaScript puro; funcionan igual en el navegador, en Node y en las pruebas del Módulo 8. Toda la parte que habla con la página vivirá en una carpeta nueva, js/vista/, y esa separación es lo que hará el proyecto probable y mantenible.

  1. window y document: el objeto global y la puerta de entrada

En un navegador, el objeto global se llama window. Representa la pestaña (o la ventana, o el iframe) y contiene absolutamente todo lo que el entorno ofrece: las APIs, las variables globales y también el propio documento.

console.log(typeof window);            // 'object'
console.log(window === globalThis);    // true   ← el global estándar del lenguaje
console.log(window.document === document); // true
console.log(window.innerWidth);        // 1280   ← ancho útil de la ventana, en píxeles

Que window sea el objeto global explica un detalle histórico: al escribir document, alert o setTimeout a secas, en realidad estás accediendo a window.document, window.alert y window.setTimeout. El prefijo es opcional y casi nadie lo escribe.

La división de trabajo entre los dos es sencilla:

Objeto De qué se ocupa Ejemplos
window La ventana y el entorno innerWidth, location, history, setTimeout, alert
document El contenido de la página title, body, head, querySelector, createElement
console.log(document.title);       // 'Nómada Tareas · Taller Nómada'
console.log(document.body.tagName); // 'BODY'
console.log(document.nodeType);     // 9   ← veremos qué significa en el apartado siguiente

Dos atajos que el navegador ofrece y que conviene conocer: document.documentElement es el nodo <html>, document.head es el <head> y document.body es el <body>. Ojo con este último: si tu <script> se ejecuta antes de que el analizador haya llegado al <body>, document.body vale null. Es el primer aviso del problema de temporización que resolveremos en el apartado 8.

  1. Los tipos de nodo, y por qué los espacios en blanco cuentan

En el árbol no todo son etiquetas. El DOM tiene varios tipos de nodo, y cada uno tiene un número identificador accesible en la propiedad nodeType. Estos son los que te vas a encontrar:

nodeType Constante Qué representa Ejemplo en el HTML
1 Node.ELEMENT_NODE Un elemento, es decir, una etiqueta <li class="tarea">
3 Node.TEXT_NODE Texto, incluidos saltos de línea y sangrías Rediseñar la sala
8 Node.COMMENT_NODE Un comentario HTML <!-- pendiente -->
9 Node.DOCUMENT_NODE El documento entero El objeto document
11 Node.DOCUMENT_FRAGMENT_NODE Un contenedor ligero fuera del árbol DocumentFragment (lección 06-05)

El que sorprende a todo el mundo es el tipo 3. Fíjate en este HTML, escrito como lo escribiría cualquiera:

<ul id="lista-tareas">
  <li class="tarea">Rediseñar la sala polivalente</li>
  <li class="tarea">Cartelería del taller de serigrafía</li>
</ul>

Entre <ul> y el primer <li> hay un salto de línea y dos espacios. Para el navegador eso es un nodo de texto, igual de real que los <li>. Lo mismo entre los dos <li> y entre el último y </ul>. Resultado:

const lista = document.getElementById('lista-tareas');

console.log(lista.childNodes.length);  // 5  ← texto, li, texto, li, texto
console.log(lista.children.length);    // 2  ← solo los elementos

for (const nodo of lista.childNodes) {
  console.log(nodo.nodeType, JSON.stringify(nodo.nodeName), JSON.stringify(nodo.textContent));
}
// 3 "#text" "\n  "
// 1 "LI"    "Rediseñar la sala polivalente"
// 3 "#text" "\n  "
// 1 "LI"    "Cartelería del taller de serigrafía"
// 3 "#text" "\n"

Cinco nodos hijos donde a simple vista hay dos. Esta es la causa número uno de bugs de principiante al recorrer el DOM: lista.firstChild no es el primer <li>, es un nodo de texto con un salto de línea y dos espacios. De ahí que exista una familia paralela de propiedades que ignoran todo lo que no sea un elemento, y que es la que usarás el 99 % de las veces.

Tres propiedades de nodo que conviene distinguir:

  • nodeType: el número de la tabla.
  • nodeName: el nombre del nodo. Para elementos es la etiqueta en mayúsculas ('LI'); para texto, '#text'; para el documento, '#document'.
  • tagName: solo existe en elementos, y es la etiqueta en mayúsculas. Si dudas entre nodeName y tagName, usa tagName cuando sepas que tienes un elemento.

  1. Navegar por el árbol: padres, hijos y hermanos

Cada nodo conoce a sus vecinos. Estas son las propiedades de navegación, agrupadas por lo que devuelven:

Propiedad Devuelve ¿Incluye nodos de texto?
parentNode El nodo padre Sí (puede ser el document)
parentElement El elemento padre No (devuelve null si el padre no es un elemento)
childNodes Todos los hijos, como NodeList viva
children Los hijos elemento, como HTMLCollection viva No
firstChild / lastChild Primer y último hijo
firstElementChild / lastElementChild Primer y último hijo elemento No
previousSibling / nextSibling Hermano anterior/siguiente
previousElementSibling / nextElementSibling Hermano elemento anterior/siguiente No
childElementCount Número de hijos elemento No

La regla mnemotécnica es sencilla: si el nombre lleva Element, ignora el texto y los comentarios. Y esas son las que quieres casi siempre.

Con el HTML del apartado anterior:

const lista = document.getElementById('lista-tareas');

const primera = lista.firstElementChild;
console.log(primera.textContent);            // 'Rediseñar la sala polivalente'

const segunda = primera.nextElementSibling;
console.log(segunda.textContent);            // 'Cartelería del taller de serigrafía'

console.log(segunda.nextElementSibling);     // null  ← no hay tercera
console.log(primera.parentElement === lista); // true
console.log(lista.parentElement.tagName);    // 'SECTION'

Subir por el árbol también funciona, y encadena tantas veces como quieras:

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

Y aquí un consejo importante: encadenar parentElement es frágil. Si mañana envuelves la lista en un <div> por motivos de diseño, todas esas cadenas se rompen en silencio. En la lección siguiente aprenderás closest(), que sube buscando el primer antepasado que cumpla un selector, y que es la forma robusta de hacer esto mismo.

Un ejemplo real, contando cuántos nodos hay en el árbol completo con la recursividad de 03-07:

/** Cuenta nodos del árbol, separando elementos, texto y comentarios. */
function censarNodos(nodo, cuenta = { elementos: 0, texto: 0, comentarios: 0 }) {
  if (nodo.nodeType === Node.ELEMENT_NODE) cuenta.elementos += 1;
  else if (nodo.nodeType === Node.TEXT_NODE) cuenta.texto += 1;
  else if (nodo.nodeType === Node.COMMENT_NODE) cuenta.comentarios += 1;

  for (const hijo of nodo.childNodes) censarNodos(hijo, cuenta);
  return cuenta;
}

console.log(censarNodos(document.body));
// { elementos: 24, texto: 31, comentarios: 1 }   ← los números dependen de tu HTML

Que haya más nodos de texto que elementos en un HTML bien indentado no es un error: es exactamente lo que explicaba el apartado 4.

  1. HTMLCollection y NodeList: colecciones que no son arrays

lista.children parece un array: tiene length, se accede con corchetes y contiene elementos. No es un array. Es una HTMLCollection, y lista.childNodes es una NodeList. Ninguna de las dos hereda de Array.prototype, así que no tienen map, filter, reduce ni find.

const items = lista.children;         // HTMLCollection
console.log(items.length);            // 2      ✓
console.log(items[0].tagName);        // 'LI'   ✓
console.log(Array.isArray(items));    // false  ← ojo
console.log(items.map);               // undefined
// items.map((li) => li.textContent);
// ✗ TypeError: items.map is not a function

La diferencia crítica entre ambas no es solo qué contienen, sino si están vivas: una colección viva se actualiza sola cuando el DOM cambia; una estática es una foto fija del momento en que se creó.

Colección La devuelven Contiene ¿Viva? forEach
HTMLCollection children, getElementsByClassName, getElementsByTagName Solo elementos No
NodeList viva childNodes Cualquier nodo
NodeList estática querySelectorAll (lección 06-02) Cualquier nodo No

Que una colección esté viva suena cómodo y en realidad es una fuente clásica de bucles infinitos:

const items = lista.children;   // viva
for (let i = 0; i < items.length; i++) {
  lista.append(document.createElement('li'));  // ← cada vuelta añade un hijo…
}
// items.length crece en cada iteración: el bucle no termina nunca.

La solución, y la práctica recomendada en general, es convertir a array en cuanto la obtienes, usando Array.from o el spread que aprendiste en 04-03 y 04-07:

const items = Array.from(lista.children);   // array de verdad, estático
// o bien
const items2 = [...lista.children];

const titulos = items
  .filter((li) => !li.classList.contains('tarea--hecha'))
  .map((li) => li.textContent.trim());

console.log(titulos);
// ['Rediseñar la sala polivalente', 'Cartelería del taller de serigrafía']

Ambas conversiones funcionan porque tanto HTMLCollection como NodeList son iterables (implementan Symbol.iterator, exactamente el protocolo de 05-08). Por eso también puedes recorrerlas con for...of sin convertir nada:

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

Array.from tiene además un segundo parámetro que hace de map en la misma pasada, y que ahorra un array intermedio:

const titulos = Array.from(lista.children, (li) => li.textContent.trim());

  1. Inspeccionar el DOM en las DevTools

Todo esto se ve mucho mejor tocándolo. Abre index.html con Live Server, pulsa F12 y ve a la pestaña Elementos (Elements).

Lo que ves ahí no es tu fichero HTML: es una representación del DOM actual. Si tu JavaScript añade un <li>, aparecerá en ese panel aunque no esté en el fichero. Cosas que puedes hacer y que conviene practicar hoy mismo:

  • Desplegar el árbol con los triángulos, y comprobar que la jerarquía coincide con el diagrama del apartado 1.
  • Seleccionar un elemento haciendo clic. Verás sus estilos aplicados en el panel de la derecha y, muy útil, la ruta de migas en la parte inferior: body > main > section.tablero > ul#lista-tareas > li.tarea.
  • Editar en caliente: doble clic sobre un texto o un atributo para cambiarlo. Los cambios se ven al instante y se pierden al recargar. Es el laboratorio perfecto para probar una clase CSS antes de escribir el JavaScript que la aplica.
  • Usar $0 en la consola: después de seleccionar un elemento en el panel Elementos, la variable $0 en la consola apunta a ese elemento. $0.children, $0.tagName, $0.classList… es la forma más rápida de explorar.
  • console.dir(elemento) frente a console.log(elemento): el primero te muestra el elemento como objeto, con todas sus propiedades desplegables; el segundo te lo muestra como HTML. Para aprender qué propiedades tiene un nodo, console.dir es infinitamente mejor.
const lista = document.getElementById('lista-tareas');
console.log(lista);   // <ul id="lista-tareas">…</ul>       ← vista HTML
console.dir(lista);   // ul#lista-tareas { children: …, id: …, … }  ← vista objeto

  1. El ciclo de vida de la página: DOMContentLoaded y load

Aquí está el problema de temporización más clásico de todos. El navegador analiza el HTML de arriba abajo, y ejecuta un <script> clásico en el momento exacto en que lo encuentra. Si el script está en el <head>, cuando se ejecuta el <body> todavía no existe:

<head>
  <script>
    // El analizador aún no ha llegado al body.
    console.log(document.getElementById('lista-tareas'));  // null ✗
  </script>
</head>

No da error: da null, que es peor, porque el fallo aparece dos líneas más abajo con un TypeError: Cannot read properties of null.

Hay dos momentos con nombre en la carga de una página:

Evento Cuándo se dispara Qué garantiza
DOMContentLoaded Cuando el HTML se ha analizado por completo y el árbol del DOM está construido Todos los elementos existen
load (en window) Cuando además han terminado de descargarse imágenes, hojas de estilo, fuentes e iframes Todo está descargado y con su tamaño real
document.addEventListener('DOMContentLoaded', () => {
  console.log('DOM listo:', document.querySelectorAll('li.tarea').length, 'tareas');
});

window.addEventListener('load', () => {
  console.log('Todo descargado, imágenes incluidas');
});

La regla es clara: para trabajar con el DOM te basta DOMContentLoaded; esperar a load retrasa el arranque de la aplicación hasta que se descargue la última imagen, y casi nunca hace falta. Solo necesitas load si dependes de las dimensiones reales de una imagen o de un recurso externo.

flowchart LR
    A["Llega el HTML"] --> B["El navegador analiza<br/>y construye el DOM"]
    B --> C["DOMContentLoaded<br/>el árbol está completo"]
    C --> D["Descarga de imágenes,<br/>fuentes, iframes"]
    D --> E["load<br/>todo está listo"]

  1. Por qué <script type="module"> ya se comporta como defer

La solución histórica al problema anterior era colocar el <script> justo antes de </body>, o añadirle el atributo defer, que le dice al navegador: «descarga este fichero en paralelo, pero no lo ejecutes hasta que el DOM esté construido».

Y aquí llega la buena noticia, que enlaza directamente con 05-04: un <script type="module"> es defer por defecto. No hay que escribir nada. Un módulo:

  1. Se descarga en paralelo sin bloquear el analizador de HTML.
  2. Se ejecuta después de que el documento esté completamente analizado.
  3. Se ejecuta antes del evento DOMContentLoaded.
Forma de incluir el script ¿Bloquea el análisis? ¿Cuándo se ejecuta? ¿El DOM está listo?
<script> en el <head> Al encontrarlo No
<script> antes de </body> Sí, pero al final Al encontrarlo
<script defer> No Tras analizar el documento
<script type="module"> No Tras analizar el documento
<script async> No En cuanto se descargue, en cualquier orden No garantizado

Como Nómada Tareas ya usa <script type="module" src="js/app.js"></script>, tu app.js puede seleccionar elementos desde su primera línea sin envolver nada en un manejador de DOMContentLoaded. Esto es una de esas cosas que se ven en mil tutoriales antiguos y que hoy sobra:

// js/app.js — módulo: esto ya es seguro, sin envolturas
const lista = document.getElementById('lista-tareas');
console.log(lista.children.length);   // 3 ✓

Sigue habiendo un caso en que necesitas el evento: si tu módulo hace await de algo lento antes de tocar el DOM, o si escribes código que puede ejecutarse tanto como módulo como en un <script> clásico. Para esos casos, el patrón defensivo es comprobar document.readyState:

function alEstarListo(fn) {
  if (document.readyState === 'loading') {
    document.addEventListener('DOMContentLoaded', fn, { once: true });
  } else {
    fn();                       // el DOM ya estaba listo: ejecuta ya
  }
}

document.readyState pasa por tres valores: 'loading' mientras se analiza, 'interactive' cuando el DOM está listo (justo antes de DOMContentLoaded) y 'complete' tras el evento load.

  1. Nómada Tareas: el index.html inicial

Es el momento de escribir la página. Este HTML es la base sobre la que crecerá todo el módulo: en 06-02 pintarás estos elementos, en 06-03 les pondrás eventos, en 06-05 los crearás desde JavaScript y en 06-07 completarás el formulario. De momento las tres tareas están escritas a mano para tener algo que inspeccionar.

<!DOCTYPE html>
<html lang="es">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Nómada Tareas · Taller Nómada</title>
  <link rel="stylesheet" href="css/estilos.css">
</head>
<body>
  <header class="cabecera">
    <h1>Nómada Tareas</h1>
    <p class="cabecera__sub">
      Taller Nómada · <time datetime="2026-09-20">20 de septiembre de 2026</time>
    </p>
  </header>

  <main>
    <section class="tablero" aria-labelledby="titulo-tablero">
      <h2 id="titulo-tablero">Backlog</h2>
      <p id="resumen" class="resumen" role="status">Cargando el tablero…</p>

      <ul id="lista-tareas" class="lista-tareas">
        <li class="tarea" data-id="1">
          <span class="tarea__titulo">Rediseñar la sala polivalente</span>
          <span class="tarea__meta">Iván · 12 h · en-curso</span>
        </li>
        <li class="tarea" data-id="2">
          <span class="tarea__titulo">Cartelería del taller de serigrafía</span>
          <span class="tarea__meta">Marta · 6 h · pendiente</span>
        </li>
        <li class="tarea" data-id="6">
          <span class="tarea__titulo">Presupuesto de la carpintería</span>
          <span class="tarea__meta">Iván · 5 h · pendiente</span>
        </li>
      </ul>
    </section>

    <section aria-labelledby="titulo-nueva">
      <h2 id="titulo-nueva">Nueva tarea</h2>
      <form id="nueva-tarea" class="formulario" novalidate>
        <p>El formulario se completa en la lección 06-07.</p>
      </form>
    </section>
  </main>

  <footer class="pie">
    <p>Taller Nómada · coworking, serigrafía, encuadernación y carpintería</p>
  </footer>

  <script type="module" src="js/app.js"></script>
</body>
</html>

Merece la pena justificar cada decisión, porque no son decorativas:

  • <html lang="es">: los lectores de pantalla eligen la voz y la pronunciación a partir de este atributo. Sin él, un lector configurado en inglés leerá «Presupuesto de la carpintería» con fonética inglesa.
  • HTML semántico: <header>, <main>, <section>, <footer> en lugar de <div> para todo. Las tecnologías de asistencia exponen esos elementos como puntos de referencia navegables; un usuario de lector de pantalla puede saltar directamente al contenido principal.
  • aria-labelledby en cada <section>: le da a la sección un nombre accesible tomado del <h2>. Una sección sin nombre no aparece en la lista de puntos de referencia.
  • <ul> con <li>: una lista de tareas es una lista. El lector de pantalla anunciará «lista de 3 elementos», información que un montón de <div> no daría.
  • role="status" en el párrafo de resumen: cuando su texto cambie desde JavaScript, el lector de pantalla lo anunciará sin robar el foco. Es la forma correcta de comunicar «45 h abiertas» a quien no ve la pantalla.
  • data-id en cada <li>: es la pieza clave del resto del módulo. Conecta cada elemento de la página con el id de su Tarea en el modelo. En 06-02 lo leerás con dataset, y en 06-04 será la base de la delegación de eventos.
  • <time datetime="2026-09-20">: la fecha legible para las personas y la fecha canónica para las máquinas, en el mismo elemento.

Y el CSS mínimo. La consigna del módulo es «solo lo justo»: clases con nombres claros, sin florituras.

/* css/estilos.css */
:root {
  --color-alta: #c0392b;
  --color-media: #b7791f;
  --color-baja: #2b7a78;
  --gris: #6b7280;
  --borde: #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;
}

.cabecera__sub { color: var(--gris); margin-top: -0.5rem; }
.resumen { font-weight: 600; }

.tablero { border: 1px solid var(--borde); border-radius: 0.5rem; padding: 1rem; }

.lista-tareas { list-style: none; padding: 0; margin: 0; }

.tarea {
  display: flex;
  justify-content: space-between;
  gap: 1rem;
  padding: 0.6rem 0.8rem;
  border-left: 4px solid var(--gris);
  border-bottom: 1px solid var(--borde);
}

.tarea--alta  { border-left-color: var(--color-alta); }
.tarea--media { border-left-color: var(--color-media); }
.tarea--baja  { border-left-color: var(--color-baja); }

.tarea--hecha .tarea__titulo { text-decoration: line-through; color: var(--gris); }
.tarea--vencida .tarea__meta { color: var(--color-alta); font-weight: 600; }

.tarea__meta { color: var(--gris); font-size: 0.9rem; white-space: nowrap; }

/* El foco visible es obligatorio: quien navega con teclado necesita verlo. */
:focus-visible { outline: 3px solid #2563eb; outline-offset: 2px; }

Fíjate en la convención de nombres: .tarea es el bloque, .tarea__titulo una parte suya y .tarea--alta una variante. Esa disciplina hará que en 06-02, cuando manipules clases con classList, sepas exactamente qué añadir y qué quitar.

Y el app.js de esta lección, que todavía no modifica nada: solo comprueba que el árbol es el que esperamos.

// js/app.js
import { Tablero } from './modelo/tablero.js';
import { crearBacklog } from './datos/backlog.js';
import { HOY } from './util/fechas.js';

const tablero = new Tablero('Taller Nómada', crearBacklog());
const resumen = tablero.resumen(HOY);
console.log(resumen);
// { total: 6, abiertas: 5, horasTotales: 48, horasAbiertas: 45, vencidas: 1, esfuerzo: 124 }

// El DOM ya está construido: es un módulo, y los módulos son 'defer'.
const lista = document.getElementById('lista-tareas');
console.log('Elementos <li> en la página:', lista.children.length);   // 3
console.log('Nodos hijos totales:', lista.childNodes.length);         // 7 (los textos cuentan)

for (const li of lista.children) {
  console.log(li.dataset.id, '→', li.firstElementChild.textContent);
}
// 1 → Rediseñar la sala polivalente
// 2 → Cartelería del taller de serigrafía
// 6 → Presupuesto de la carpintería

Los números canónicos del modelo (48 h totales, 45 abiertas, 1 vencida, esfuerzo 124) siguen intactos, y ahora conviven en la misma página con tres <li> escritos a mano. Cerrar esa brecha —que la lista de la página salga del Tablero y no del HTML— es el trabajo de las seis lecciones siguientes.

Errores Comunes y Consejos

  • Creer que document es parte de JavaScript. No lo es, y por eso el mismo código falla en Node. Mantén el modelo (modelo/, datos/, util/) libre de cualquier referencia al DOM: te lo agradecerás en el Módulo 8, cuando escribas pruebas que se ejecutan sin navegador.
  • Confundir childNodes con children. Es la trampa clásica: childNodes incluye los nodos de texto que genera tu propia indentación. Usa siempre la familia con Element en el nombre (children, firstElementChild, nextElementSibling) salvo que de verdad necesites el texto.
  • Tratar una HTMLCollection como un array. No tiene map, filter ni reduce. Convierte con Array.from(coleccion) o [...coleccion] en cuanto la obtengas; además así dejas de tener una colección viva que cambia bajo tus pies.
  • Modificar el DOM mientras recorres una colección viva. El bucle puede no terminar nunca, o saltarse elementos. Convertir a array antes del bucle elimina el problema de raíz.
  • Poner un <script> clásico en el <head> sin defer. Todos los getElementById devolverán null. Con type="module" el problema desaparece; si aun así tu código puede ejecutarse en contextos distintos, usa el patrón alEstarListo del apartado 9.
  • Esperar a load cuando basta DOMContentLoaded. Retrasas el arranque hasta que se descargue el último recurso externo. Solo espera a load si necesitas dimensiones reales de imágenes.
  • Consejo: usa console.dir(elemento) para explorar. console.log te enseña el HTML; console.dir te enseña el objeto con todas sus propiedades. Y combina el panel Elementos con $0 en la consola: es la forma más rápida de descubrir la API sin buscar en la documentación.
  • Consejo: escribe HTML semántico desde el primer minuto. Añadir accesibilidad al final es carísimo; escribirla bien desde el principio no cuesta nada. Un <ul> de <li> con aria-labelledby en la sección ya te da media accesibilidad hecha.

Ejercicios

Ejercicio 1 · El censo del árbol

Escribe en js/app.js una función describirArbol(raiz, nivel = 0) que imprima por consola el árbol de elementos que cuelga de raiz, con una sangría de dos espacios por nivel, mostrando la etiqueta en minúsculas, el id si lo tiene y las clases si las tiene. Debe ignorar los nodos de texto y de comentario. Aplícala a document.body.

Salida esperada (recortada):

body
  header .cabecera
    h1
    p .cabecera__sub
      time
  main
    section .tablero

Ejercicio 2 · Tres formas de llegar al mismo sitio

Partiendo del <li> con data-id="2", obtén el elemento <section class="tablero"> de tres maneras distintas: (a) encadenando parentElement; (b) con un bucle while que suba hasta encontrar un elemento cuya lista de clases contenga tablero; (c) explicando, sin escribir el código, qué método de la lección siguiente resolvería esto en una línea. Comenta cuál de las dos primeras te parece más robusta y por qué.

Ejercicio 3 · Titulares de las tareas abiertas

Sin usar querySelector (todavía no lo hemos visto), obtén un array con los títulos de los <li> de la lista cuyo texto de meta no contenga 'hecha'. Usa Array.from, filter y map. Después imprime cuántos hay y comprueba que el resultado sigue siendo un array de verdad con Array.isArray.

Soluciones

Ejercicio 1

function describirArbol(raiz, nivel = 0) {
  const sangria = '  '.repeat(nivel);
  const id = raiz.id ? ` #${raiz.id}` : '';
  const clases = raiz.classList.length ? ' .' + [...raiz.classList].join(' .') : '';
  console.log(`${sangria}${raiz.tagName.toLowerCase()}${id}${clases}`);

  for (const hijo of raiz.children) {     // 'children' ya filtra texto y comentarios
    describirArbol(hijo, nivel + 1);
  }
}

describirArbol(document.body);

Puntos clave: se recorre children (no childNodes), así que no hace falta comprobar nodeType; la recursividad es la de 03-07, con el nivel como acumulador; y [...raiz.classList] funciona porque classList también es iterable. Si un elemento no tiene clases, classList.length es 0 y la cadena queda vacía en lugar de mostrar un punto suelto.

Ejercicio 2

const li = document.getElementById('lista-tareas').children[1];

// (a) Encadenando parentElement
const seccionA = li.parentElement.parentElement;
console.log(seccionA.tagName, seccionA.className);   // SECTION tablero

// (b) Subiendo con un bucle hasta encontrar la clase
function subirHasta(nodo, clase) {
  let actual = nodo;
  while (actual !== null && !actual.classList?.contains(clase)) {
    actual = actual.parentElement;
  }
  return actual;
}
const seccionB = subirHasta(li, 'tablero');
console.log(seccionA === seccionB);                  // true

(c) li.closest('.tablero') hace exactamente lo mismo que la versión (b) en una sola llamada, y lo verás en 06-02.

La versión (b) es claramente más robusta. La (a) codifica una distancia exacta en el árbol: si mañana envuelves el <ul> en un <div class="scroll">, seccionA pasará a ser el <div> y nadie se enterará hasta que algo se rompa dos pantallas más allá. La (b) codifica una intención («sube hasta el tablero»), y sobrevive a los cambios de maquetación. El ?. antes de contains es la protección de 01-06: cuando el bucle llega a document, classList es undefined y sin el encadenamiento opcional habría un TypeError.

Ejercicio 3

const lista = document.getElementById('lista-tareas');

const abiertas = Array.from(lista.children)
  .filter((li) => !li.lastElementChild.textContent.includes('hecha'))
  .map((li) => li.firstElementChild.textContent.trim());

console.log(abiertas);
// ['Rediseñar la sala polivalente', 'Cartelería del taller de serigrafía', 'Presupuesto de la carpintería']
console.log(abiertas.length);        // 3
console.log(Array.isArray(abiertas)); // true

Lo esencial: sin el Array.from inicial, la línea del filter lanzaría TypeError: lista.children.filter is not a function, porque children es una HTMLCollection. El .trim() limpia los saltos de línea y la indentación que el HTML introduce dentro del <span>, exactamente los nodos de texto del apartado 4. Y fíjate en lo frágil que resulta leer el estado de una tarea buscando la subcadena 'hecha' dentro de un texto: es justo lo que dejarás de hacer en la lección siguiente, cuando guardes esa información en atributos data-* y en clases en lugar de en prosa.

Conclusión

Ya sabes qué es el DOM y, casi más importante, qué no es. No es tu fichero HTML, sino el árbol de objetos que el navegador construye a partir de él y que a partir de ese momento vive por su cuenta en memoria. No es parte del lenguaje JavaScript, sino una API que aporta el entorno del navegador: por eso document no existe en Node y por eso el modelo de Nómada Tareas —Tarea, Tablero, crearBacklog— debe seguir sin mencionarlo nunca. Y no es lo que se ve: lo que se ve son píxeles que el navegador pinta combinando el DOM con el CSS, en una cadena HTML → DOM → CSS → píxeles sobre la que tú actúas en el segundo eslabón.

Conoces la anatomía del árbol: window como objeto global y document como puerta de entrada al contenido; los tipos de nodo con sus nodeType (1 elemento, 3 texto, 8 comentario, 9 documento) y la sorpresa de que tu propia indentación genera nodos de texto reales, que es la razón de que childNodes devuelva cinco hijos donde children devuelve dos. Sabes moverte entre nodos con parentElement, children, firstElementChild y nextElementSibling, con la regla de oro de preferir siempre las propiedades que llevan Element en el nombre. Y sabes que las colecciones que devuelve el DOM —HTMLCollection y NodeListno son arrays: son iterables, así que for...of funciona, pero para usar map y filter hay que pasar por Array.from o el spread, algo que además te libra de los sustos de las colecciones vivas.

También tienes resuelto el problema de temporización que arruina el primer día de tanta gente: el analizador construye el árbol de arriba abajo, DOMContentLoaded avisa de que ya está completo, load espera además a las imágenes, y un <script type="module"> ya se comporta como defer, de modo que tu app.js puede seleccionar elementos desde su primera línea. Y tienes escrito el index.html de Nómada Tareas con estructura semántica de verdad —header, main, section.tablero, ul#lista-tareas, form#nueva-tarea, role="status", aria-labelledby— junto con un CSS mínimo de clases claras (.tarea, .tarea--alta, .tarea--hecha, .tarea--vencida) y, sobre todo, un data-id en cada <li> que será la bisagra entre la pantalla y el modelo durante todo el módulo.

Lo que todavía no sabes hacer es encontrar un elemento concreto sin ir saltando de padre en hijo, y cambiarlo. Recorrer el árbol a mano con firstElementChild.nextElementSibling.parentElement funciona, pero es exactamente el tipo de código frágil que se rompe al mover un <div>. Hay una forma mucho mejor: describir lo que buscas con un selector CSS'.tarea--alta', '#lista-tareas > li', '[data-id="6"]'— y dejar que el navegador lo encuentre. Eso, junto con la forma correcta de cambiar textos, atributos, clases y estilos (y por qué innerHTML es un agujero de seguridad esperando a ocurrir), es Selección y Manipulación de Elementos del DOM, donde por fin pintarás en la pantalla el estado y la prioridad reales de las tareas de Taller Nómada.

Curso de JavaScript: De Principiante a Avanzado

Módulo 1: Introducción a JavaScript

Módulo 2: Estructuras de Control

Módulo 3: Funciones

Módulo 4: Objetos y Arrays

Módulo 5: Objetos y Funciones Avanzadas

Módulo 6: El Modelo de Objetos del Documento (DOM)

Módulo 7: APIs del Navegador y Temas Avanzados

Módulo 8: Pruebas y Depuración

Módulo 9: Rendimiento y Optimización

Módulo 10: Frameworks y Librerías de JavaScript

Módulo 11: Proyecto Final

© Copyright 2026. Todos los derechos reservados