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
- Del texto HTML al árbol de nodos
- El DOM no forma parte del lenguaje JavaScript
windowydocument: el objeto global y la puerta de entrada- Los tipos de nodo, y por qué los espacios en blanco cuentan
- Navegar por el árbol: padres, hijos y hermanos
HTMLCollectionyNodeList: colecciones que no son arrays- Inspeccionar el DOM en las DevTools
- El ciclo de vida de la página:
DOMContentLoadedyload - Por qué
<script type="module">ya se comporta comodefer - Nómada Tareas: el
index.htmlinicial - Errores Comunes y Consejos
- Ejercicios
- Conclusión
- 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:
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.documentno 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 nodolies hijo del nodoul. 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.htmldel 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.
- 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.
window y document: el objeto global y la puerta de entrada
window y document: el objeto global y la puerta de entradaEn 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íxelesQue 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 siguienteDos 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.
- 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 entrenodeNameytagName, usatagNamecuando sepas que tienes un elemento.
- 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 |
Sí |
children |
Los hijos elemento, como HTMLCollection viva |
No |
firstChild / lastChild |
Primer y último hijo | Sí |
firstElementChild / lastElementChild |
Primer y último hijo elemento | No |
previousSibling / nextSibling |
Hermano anterior/siguiente | Sí |
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:
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 HTMLQue 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.
HTMLCollection y NodeList: colecciones que no son arrays
HTMLCollection y NodeList: colecciones que no son arrayslista.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 functionLa 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 | Sí | No |
NodeList viva |
childNodes |
Cualquier nodo | Sí | Sí |
NodeList estática |
querySelectorAll (lección 06-02) |
Cualquier nodo | No | Sí |
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:
Array.from tiene además un segundo parámetro que hace de map en la misma pasada, y que ahorra un array intermedio:
- 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
$0en la consola: después de seleccionar un elemento en el panel Elementos, la variable$0en 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 aconsole.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.dires 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
- El ciclo de vida de la página:
DOMContentLoaded y load
DOMContentLoaded y loadAquí 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"]
- Por qué
<script type="module"> ya se comporta como defer
<script type="module"> ya se comporta como deferLa 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:
- Se descarga en paralelo sin bloquear el analizador de HTML.
- Se ejecuta después de que el documento esté completamente analizado.
- 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> |
Sí | Al encontrarlo | No |
<script> antes de </body> |
Sí, pero al final | Al encontrarlo | Sí |
<script defer> |
No | Tras analizar el documento | Sí |
<script type="module"> |
No | Tras analizar el documento | Sí |
<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.
- Nómada Tareas: el
index.html inicial
index.html inicialEs 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-labelledbyen 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-iden cada<li>: es la pieza clave del resto del módulo. Conecta cada elemento de la página con elidde suTareaen el modelo. En 06-02 lo leerás condataset, 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íaLos 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
documentes 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
childNodesconchildren. Es la trampa clásica:childNodesincluye los nodos de texto que genera tu propia indentación. Usa siempre la familia conElementen el nombre (children,firstElementChild,nextElementSibling) salvo que de verdad necesites el texto. - Tratar una
HTMLCollectioncomo un array. No tienemap,filternireduce. Convierte conArray.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>sindefer. Todos losgetElementByIddevolveránnull. Contype="module"el problema desaparece; si aun así tu código puede ejecutarse en contextos distintos, usa el patrónalEstarListodel apartado 9. - Esperar a
loadcuando bastaDOMContentLoaded. Retrasas el arranque hasta que se descargue el último recurso externo. Solo espera aloadsi necesitas dimensiones reales de imágenes. - Consejo: usa
console.dir(elemento)para explorar.console.logte enseña el HTML;console.dirte enseña el objeto con todas sus propiedades. Y combina el panel Elementos con$0en 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>conaria-labelledbyen 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):
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)); // trueLo 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 NodeList— no 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
- ¿Qué es JavaScript?
- Configuración de tu Entorno de Desarrollo
- Tu Primer Programa en JavaScript
- Sintaxis y Conceptos Básicos de JavaScript
- Variables y Tipos de Datos
- Operadores Básicos
- Conversión de Tipos y Comparaciones
- El Proyecto del Curso: Nómada Tareas
Módulo 2: Estructuras de Control
- Sentencias Condicionales
- Bucles: for, while, do-while
- Sentencias Switch
- Control del Flujo: break, continue y Bucles Anidados
- Manejo de Errores con try-catch
Módulo 3: Funciones
- Definición y Llamada de Funciones
- Expresiones de Función y Funciones Flecha
- Parámetros y Valores de Retorno
- Ámbito y Closures
- Hoisting y el Contexto de Ejecución
- Funciones de Orden Superior
- Recursividad
Módulo 4: Objetos y Arrays
- Introducción a los Objetos
- Métodos de Objeto y la Palabra Clave
this - Arrays: Conceptos Básicos y Métodos
- Iteración sobre Arrays
- Buscar, Ordenar y Agregar Datos: find, sort y reduce
- Desestructuración de Arrays
- Desestructuración de Objetos, Spread y Rest
- JSON y Copias de Objetos
Módulo 5: Objetos y Funciones Avanzadas
- Prototipos y Herencia
- Clases y Programación Orientada a Objetos
- Encapsulación: Getters, Setters y Campos Privados
- Módulos e Importación/Exportación
- JavaScript Asíncrono: Callbacks
- Promesas y Async/Await
- El Bucle de Eventos y la Cola de Microtareas
- Iteradores y Generadores
Módulo 6: El Modelo de Objetos del Documento (DOM)
- Introducción al DOM
- Selección y Manipulación de Elementos del DOM
- Manejo de Eventos
- Propagación, Delegación y Eventos Personalizados
- Creación y Eliminación de Elementos del DOM
- Renderizado de Listas y Plantillas HTML
- Manejo y Validación de Formularios
Módulo 7: APIs del Navegador y Temas Avanzados
- Almacenamiento Local y de Sesión
- Fetch API y AJAX
- Peticiones Robustas: Errores, Timeouts y AbortController
- WebSockets
- Service Workers y Aplicaciones Web Progresivas (PWAs)
- APIs del Navegador Esenciales
- Introducción a WebAssembly
Módulo 8: Pruebas y Depuración
- Depuración de JavaScript
- Calidad de Código: ESLint, Prettier y Convenciones
- Pruebas Unitarias con Jest
- Dobles de Prueba: Mocks, Stubs y Spies
- Pruebas de Integración
- Pruebas de Extremo a Extremo con Cypress
Módulo 9: Rendimiento y Optimización
- Medir Antes de Optimizar: DevTools y Web Vitals
- Optimización del Rendimiento de JavaScript
- Gestión de Memoria
- Manipulación Eficiente del DOM
- Carga Perezosa y División de Código
Módulo 10: Frameworks y Librerías de JavaScript
- Por Qué Existen los Frameworks
- Introducción a React
- Gestión de Estado con Redux
- Conceptos Básicos de Vue.js
- Conceptos Básicos de Angular
- Elegir el Framework Adecuado
