← El DOM: la web que responde

Sesión 1

La página como objetos

Tres horas. Media hora para entender qué es el DOM y cómo se localiza y se modifica un elemento, y dos horas y media preparando tu página para que el código pueda trabajar con ella sin romperse al primer cambio de diseño.

Hoy · Hoja de ruta

  1. 1. Aprende: Qué es el DOM y por qué no es tu fichero HTML, cómo se localiza un elemento, y cómo se cambia su contenido y su aspecto sin meter diseño en el código.
  2. 2. Haz: Explora el DOM de tu web, prepárala con anclajes pensados para el código y cambia el estado visual del catálogo desde JavaScript.
  3. 3. Comprueba: Ninguna selección depende de la posición ni de una clase de estilo, y cambias clases en lugar de estilos en línea.

Antes de empezar · 5 minutos, sin apuntes

  1. Cuando el navegador lee tu HTML, ¿qué crees que hace con él?
  2. Si el diseñador cambia la clase .tarjeta por .card, ¿se rompe tu código?
  3. ¿Qué riesgo ves en insertar texto escrito por un usuario dentro del HTML?

Se explica

25 minutos · conceptos y demostración

Hoy empieza la tercera capa, y la idea que la gobierna es la misma de las dos anteriores: cada cosa en su sitio. El código dice qué está pasando; el CSS, cómo se ve. Todo lo que hagamos hoy sirve para no mezclarlas.

El documento, convertido en objetos

Cuando el navegador lee tu HTML construye en memoria un árbol de objetos: cada etiqueta pasa a ser un objeto con propiedades y métodos. Ese árbol es el DOM, y es lo que tu código manipula.

DOM · Document Object Model

La representación en memoria del documento. Tu fichero .html es el punto de partida; el DOM es lo que existe mientras la página está abierta, y puede acabar siendo muy distinto del fichero.

Del fichero a la pantalla
  1. Fichero HTML
  2. El navegador lo analiza
  3. Árbol DOM en memoria
  4. Se pinta en pantalla

Compruébalo: en la pestaña Elements borra un párrafo; desaparece de la pantalla, pero tu fichero sigue intacto. Recarga y vuelve. Lo que ves en Elements no es tu código, es el DOM.

En sentido inverso: si tu código crea diez artículos, aparecerán en Elements y no constarán en el archivo. Por eso, cuando en la sesión 3 pintes el catálogo, «ver código fuente» no te servirá de nada: hay que mirar Elements.

document                       // el documento entero
document.body                  // el cuerpo
document.title                 // el título de la pestaña

const main = document.querySelector("main");
main.children                  // sus elementos hijos
main.parentElement             // su contenedor
main.firstElementChild
main.nextElementSibling

Un detalle que confunde a todo el mundo: el DOM también guarda como nodos los espacios y saltos de línea entre etiquetas. Por eso usamos siempre las propiedades que hablan de elementos (children, firstElementChild) y no las que hablan de nodos en general (childNodes, firstChild).

Si el código se ejecuta antes de que exista el documento, además, no encontrará nada:

const titulo = document.querySelector("h1");
console.log(titulo);      // null si el script se ejecutó demasiado pronto

Esto ya lo resolviste en la UD3: con defer, o con type="module", el navegador espera a tener el documento completo. Recuérdalo, porque ese null es el error número uno de la unidad.

Los dos métodos que vas a usar

const titulo = document.querySelector("h1");            // el primero, o null
const tarjetas = document.querySelectorAll(".producto"); // todos, siempre una lista

Aceptan cualquier selector CSS, exactamente los que aprendiste en la UD2:

document.querySelector("#buscador");
document.querySelector(".catalogo .producto");
document.querySelector("[data-categoria='teclados']");
document.querySelectorAll("article:not(.agotado)");

Existen métodos más antiguos —getElementById, getElementsByClassName— que verás en tutoriales; con estos dos sirve.

const tarjetas = document.querySelectorAll(".producto");

tarjetas.length          // sí
tarjetas.forEach(...)    // sí
tarjetas.map(...)        // TypeError: no es una función

[...tarjetas].map(...)   // convertido en array, ya sí

Es una NodeList. Tiene length y forEach, pero no los métodos de la UD3. Los tres puntos la convierten en un array de verdad.

Una lista vacía, además, no es null: si el selector no encuentra nada, querySelectorAll devuelve una lista de longitud cero, mientras que querySelector devuelve null. Confundir esos dos casos es el segundo error más común de la unidad.

Anclajes pensados para el código

No selecciones por clases de estilo ni por posición

Si tu código busca .tarjeta-azul o el tercer elemento de una lista, cualquier retoque de diseño lo romperá, y quien lo rompa no sabrá que lo ha roto.

Usa un atributo data- pensado para eso: data-js="buscador". Así el HTML declara explícitamente «aquí hay un punto de conexión con el código», y las clases quedan libres para el CSS.

<input type="search" id="buscador" data-js="buscador" aria-label="Buscar productos">
<ul class="catalogo" data-js="catalogo"></ul>
<p data-js="resultado" role="status"></p>

Esas referencias se buscan una sola vez, al arrancar:

// Mal: busca en el árbol cada vez que el usuario escribe
function alEscribir() {
  document.querySelector("[data-js='catalogo']").textContent = "";
}

// Bien: se busca una vez, al arrancar
const elementos = {
  buscador: document.querySelector("[data-js='buscador']"),
  catalogo: document.querySelector("[data-js='catalogo']"),
  resultado: document.querySelector("[data-js='resultado']")
};

Además de ser más rápido, agrupa en un sitio todo lo que tu código espera encontrar en la página. Si algo sale null, se ve al arrancar y no en mitad de un evento.

Cambiar el contenido

const titulo = document.querySelector("h1");

titulo.textContent = "Catálogo de productos";        // texto plano: lo habitual
titulo.innerHTML = "Catálogo <strong>2026</strong>"; // interpreta etiquetas

Por defecto, textContent

Insertar con innerHTML un texto que venga de fuera —lo que alguien escribió en un campo, lo que devolvió una API— permite colar etiquetas y código en tu página. Es la vía de entrada de los ataques de inyección.

textContent escribe texto y solo texto: el contenido se muestra literalmente, sin interpretarse ni ejecutarse. Usa innerHTML únicamente con marcado que hayas escrito tú, y nunca con datos del usuario.

Cambiar el aspecto sin escribir diseño

tarjeta.classList.add("agotado");
tarjeta.classList.remove("agotado");
tarjeta.classList.toggle("destacado");
tarjeta.classList.contains("agotado");    // true / false

JavaScript pone clases; el CSS decide cómo se ven

Es la separación de capas de siempre, ahora en la tercera. Tu código dice qué está pasando —«esto está agotado», «esto está cargando»— y la hoja de estilos dice cómo se representa.

Si en lugar de eso escribes elemento.style.backgroundColor = "grey", has metido una decisión de diseño dentro del comportamiento: nadie la encontrará buscando en el CSS, y no responderá al modo oscuro ni al tema de la UD2.

Hay un caso en el que tocar style sí es legítimo: cuando el valor es un dato y la regla sigue viviendo en la hoja de estilos.

elemento.style.setProperty("--altura-imagen", "220px");

Cambiar así una variable CSS de las que definiste en la UD2 no mete diseño en el código: mete un número, y el diseño lo sigue decidiendo el CSS.

Los atributos se leen y se escriben con sus propios métodos:

enlace.getAttribute("href");
enlace.setAttribute("href", "productos.html");
enlace.removeAttribute("target");
enlace.hasAttribute("download");

Los data- tienen además un acceso propio, que será la forma de saber en la sesión 2 a qué producto corresponde el botón que se acaba de pulsar:

tarjeta.dataset.id;            // "7"  ← siempre texto
Number(tarjeta.dataset.id);    // 7
tarjeta.dataset.categoria = "teclados";

Conviene observar de nuevo el mismo detalle de la UD3: el valor llega como texto.

Se trabaja

150 minutos · práctica sobre tu propio proyecto

Hoy la página deja de ser algo que solo se mira y pasa a ser algo con lo que el código habla. Los dos primeros pasos preparan esa conversación; los tres últimos la ponen a prueba.

Paso 1 · Explora tu propia web · 30 min

Desde la consola de tu página de productos:

  1. Cuenta cuántos enlaces hay: document.querySelectorAll("a").length.
  2. Obtén el texto del h1.
  3. Recorre las secciones y muestra su primer encabezado.
  4. Cambia el título de la pestaña y observa el efecto.
  5. Borra un elemento desde Elements y explica por escrito por qué el fichero no cambia.
  6. Dibuja en papel el árbol de las tres primeras ramas de tu página.
  7. Compara main.children con main.childNodes y anota cuántos elementos devuelve cada uno y por qué no coinciden.

Antes de continuar: puedes explicar en una frase la diferencia entre lo que escribiste y lo que el navegador construyó.

Paso 2 · Preparar el terreno · 35 min

Sobre tu página de productos:

  1. Añade atributos data-js a los seis elementos con los que vas a trabajar.
  2. Crea js/dom.js que los seleccione y los exporte en un objeto.
  3. Escribe una comprobación que avise por consola si alguno es null, diciendo qué anclaje falta por su nombre.
  4. Selecciona todas las tarjetas y cuéntalas.
  5. Convierte la NodeList en array y obtén sus textos con map.
  6. Quita a propósito un data-js del HTML y comprueba que tu aviso lo detecta al arrancar.

El apartado 6 es el que importa: un anclaje que falta debe delatarse al cargar la página, no veinte minutos después dentro de un evento.

Paso 3 · Estado visual desde el código · 40 min

Es el trabajo central de la sesión. Con el catálogo todavía escrito a mano en HTML:

  1. Marca con la clase agotado los productos sin stock, leyendo el dato de dataset. Recuerda convertir.
  2. Escribe en un párrafo cuántos productos hay disponibles.
  3. Añade un botón que alterne una clase de vista compacta en la lista, aplicándola a todas las tarjetas de una vez.
  4. Cambia el title de la pestaña para incluir el número de resultados.
  5. Define en el CSS cómo se ve .agotado y cómo se ve la vista compacta. Todo el aspecto vive ahí, y tu JavaScript no contiene un solo color.

Paso 4 · Clases frente a estilos en línea · 20 min

Repite el apartado 1 del paso anterior con element.style en lugar de classList, y compara las dos versiones:

Pregunta Con classList Con style
¿Dónde está escrito el color del estado agotado?
¿Cuántos ficheros hay que tocar para cambiarlo?
¿Responde al modo oscuro de la UD2?
¿Qué gana la regla en la cascada, y por qué?
¿Puede el CSS sobrescribirlo sin !important?

La última fila conecta con la sesión 1 de la UD2: un estilo en línea gana a casi todo, y eso lo convierte en un problema de cascada además de en un problema de organización.

Quédate con la versión de classList y borra la otra.

Paso 5 · Un selector que no falla en silencio · 25 min

document.querySelector devuelve null cuando no encuentra nada, y ese null viaja tranquilamente hasta que algo intenta usarlo. Escribe tu propia función de selección que no lo permita:

export function seleccionar(ancla, contexto = document) {
  // devuelve el elemento, o lanza un error que diga qué ancla faltaba
}
  1. Escríbela y úsala en dom.js para los seis anclajes.
  2. El mensaje del error debe incluir el nombre del ancla y ser suficiente para arreglarlo sin abrir el código.
  3. Escribe también seleccionarTodos(ancla), que devuelva un array en vez de una NodeList. Decide si esta debe lanzar error con cero resultados, y justifica tu decisión: no es la misma que la anterior.
  4. Prueba las dos quitando anclajes del HTML.
  5. Anota en un comentario en qué caso preferirías seguir usando querySelector a secas.

Ampliación si has completado el trabajo

Primero termina y comprueba los cinco pasos. Los dos retos enseñan, cada uno por su lado, por qué las reglas de hoy no eran manías.

Reto 1 · El rediseño que rompe el código

Este JavaScript funciona hoy:

const tarjetas = document.querySelectorAll(".card-azul");
const boton = document.querySelector("main div div:nth-child(3) button");
const titulo = document.querySelector("section.destacada h2");

tarjetas[0].classList.add("primero");
boton.textContent = "Comprar";

Mañana el equipo de diseño hace cuatro cambios, todos razonables y ninguno pensado para fastidiarte:

1. Renombra .card-azul como .card--primaria, porque el azul cambió a verde.
2. Envuelve el contenido en un <div class="wrapper"> más para centrarlo.
3. La sección destacada pasa de ser <section> a <article>.
4. El catálogo se reordena, y la tarjeta que iba primera ahora va tercera.
  1. Para cada uno de los cuatro cambios, di qué línea del JavaScript se rompe y qué error exacto aparecería en consola. Dos de ellos no dan error: producen un null o una lista vacía y el fallo se manifiesta más tarde. Identifícalos.
  2. Reescribe las cuatro selecciones con anclajes data-js y comprueba que los cuatro cambios de diseño dejan de afectarte.
  3. La línea de tarjetas[0] sigue siendo frágil aunque uses data-js. Explica por qué y propón qué debería decidir cuál es «la primera».
  4. Escribe en tres líneas qué le dirías al equipo de diseño para que en el futuro sepa qué puede tocar sin avisarte. La respuesta no es «que no toque nada».

Reto 2 · Por qué innerHTML con datos ajenos es un problema

Monta una página mínima con un campo de texto y un contenedor donde se muestre lo escrito usando innerHTML.

  1. Escribe en el campo <strong>hola</strong> y observa qué aparece.
  2. Escribe ahora <img src="x" onerror="alert('código ejecutado')">. Anota qué pasa. No hace falta que entiendas el detalle: basta con que veas que se ha ejecutado código que tú no escribiste.
  3. Cambia innerHTML por textContent y repite las dos pruebas. Describe la diferencia.
  4. Ahora el caso realista: tu catálogo pinta el nombre del producto con innerHTML porque quieres que algunos lleven <em>. Un producto viene de un formulario y su nombre contiene una comilla y un signo de menor que. Propón dos soluciones distintas, y di cuál elegirías y por qué.
  5. Este ataque tiene nombre en la disciplina. Búscalo, escríbelo, y explica en dos líneas por qué el problema no está en el usuario que escribe, sino en el código que confía.
Objetivo mínimoSeis anclajes seleccionados una vez y comprobados, y clases aplicadas según los datos con todo el aspecto en el CSS.
Si lo tienesLa tabla de comparación entre classList y style contestada, y las dos funciones de selección con errores claros.
RetoLos cuatro cambios de diseño diagnosticados y reparados, y la demostración de inyección con su solución razonada.

Cierre

5 minutos · comprobación y recuerdo

Lista de verificación de la sesión

  • Explicas qué es el DOM y por qué no es tu fichero.
  • Seleccionas por anclajes pensados para el código, no por clases de estilo.
  • Guardas las referencias una sola vez y avisas si falta alguna.
  • Cambias texto con textContent y aspecto con clases.
  • Tu JavaScript no contiene ningún color ni medida de diseño.
  • Sabes por qué innerHTML con datos ajenos es un riesgo.

Antes de cerrar · 3 minutos, sin mirar

  1. ¿Qué es el DOM?
  2. ¿Por qué «ver código fuente» no muestra lo que crea tu código?
  3. ¿Qué devuelve querySelector si no encuentra nada? ¿Y querySelectorAll?
  4. ¿Por qué no seleccionamos por clases de CSS?
  5. ¿Por qué cambiamos una clase en lugar de un estilo en línea?
  6. ¿De qué tipo es siempre lo que devuelve dataset?
Ver respuestas

1 · El árbol de objetos que el navegador construye a partir del HTML y que existe mientras la página está abierta.

2 · Porque el código fuente es el fichero recibido, y lo que creas después solo existe en el DOM: se ve en Elements.

3 · null el primero; una lista vacía el segundo.

4 · Porque las clases son del diseño y cambian: el código quedaría atado a decisiones visuales.

5 · Para que el aspecto siga viviendo en el CSS, responda al tema y se pueda sobrescribir sin pelear con la cascada.

6 · Texto. Hay que convertirlo si se va a operar con él.

Microprueba semanal 1 · 5–10 minutos

Individual, sin IA y sin apuntes.

  1. Explica en dos líneas la diferencia entre tu fichero HTML y el DOM.
  2. Escribe la selección de un elemento por atributo data-, y di qué devuelve si no lo encuentra.
  3. ¿Por qué cambiamos una clase en lugar de un estilo en línea?