Tres horas. Media hora para entender cómo se escucha lo que hace una persona, cómo viaja un evento por el árbol y cómo se crea un elemento, y dos horas y media haciendo que tu página reaccione y genere su propio marcado.
Hoy · Hoja de ruta
- 1. Aprende: Qué es un evento y dónde se escucha, qué información trae, qué es delegar, y cómo se crea e inserta un elemento sin perder la semántica.
- 2. Haz: Haz que tu página reaccione al clic, al teclado y al envío del formulario; atiende todas las tarjetas con un solo manejador; genera la tarjeta desde un objeto.
- 3. Comprueba: Todo lo que funciona con ratón funciona con teclado, y el HTML generado pasa el validador.
Antes de empezar · 5 minutos, sin apuntes
- Enumera cinco cosas que puede hacer una persona en tu página. ¿Cuáles se pueden hacer sin ratón?
- Si tienes cincuenta botones iguales, ¿registrarías cincuenta escuchas?
- Si generas HTML desde código, ¿quién garantiza que sigue siendo semántico?
Se explica
25 minutos · conceptos y demostración
Tres piezas que hoy encajan entre sí: escuchar, averiguar quién provocó la acción, y crear los elementos sobre los que se actuará. La tercera obliga a la primera a hacerse de una forma concreta, y de ahí sale la delegación.
Escuchar
const boton = document.querySelector("[data-js='ordenar']");
boton.addEventListener("click", () => {
console.log("Han pulsado");
});
Tres piezas: el elemento que escucha, el tipo de evento y la función que se ejecutará. Esa función es un callback, el concepto de la sesión 3 de la UD3: tú no la llamas, la llama el navegador cuando ocurre algo.
| Evento | Ocurre cuando |
|---|---|
click |
Se pulsa, con ratón o con teclado sobre un control enfocable |
input |
Cambia el contenido de un campo, con cada tecla |
change |
El campo pierde el foco tras cambiar, o cambia una selección |
submit |
Se envía un formulario |
keydown |
Se pulsa una tecla |
focus / blur |
Un elemento gana o pierde el foco |
DOMContentLoaded |
El documento está listo |
Escucha en el elemento correcto
Para un formulario, el evento es submit en el formulario, no click en el botón. Solo así funcionan también la tecla Intro dentro de un campo y cualquier otra forma de enviar.
Y para un control que se pulsa, usa un elemento que sea pulsable de verdad. Un div con un manejador de clic no recibe el foco, no responde a la tecla Intro y no se anuncia como control: es inaccesible por construcción.
const formulario = document.querySelector("[data-js='contacto']");
formulario.addEventListener("submit", (evento) => {
evento.preventDefault(); // impide que se recargue la página
console.log("Enviando…");
});
Sin preventDefault, el navegador hace lo suyo: envía y recarga. Verás tu mensaje en consola durante un instante y desaparecerá. Es el desconcierto clásico de esta sesión.
Qué trae el evento, y por dónde pasa
catalogo.addEventListener("click", (evento) => {
evento.target; // el elemento exacto donde se pulsó
evento.currentTarget; // el elemento que escucha
evento.type; // "click"
evento.key; // en eventos de teclado
evento.preventDefault();
evento.stopPropagation();
});
La pareja target / currentTarget es la clave de la sesión: puedes escuchar en el contenedor y averiguar dentro cuál de sus hijos recibió la acción. Eso es posible porque un clic no ocurre solo en un elemento, sino que recorre el árbol:
- Captura · del documento hacia abajo
- Objetivo · el elemento pulsado
- Burbujeo · hacia arriba, hasta el documento
El burbujeo explica también un fallo característico: pulsar en un botón dentro de una tarjeta que también escucha el clic dispara los dos manejadores.
stopPropagation es el último recurso
Detener la propagación arregla el síntoma y deja una trampa: cualquier manejador que alguien registre más arriba —cerrar un menú, un contador de uso— dejará de enterarse, y quien lo escriba no entenderá por qué.
Antes de detenerla, comprueba si el manejador de arriba puede filtrar por target. Casi siempre puede.
Delegar
catalogo.addEventListener("click", (evento) => {
const boton = evento.target.closest("[data-accion='añadir']");
if (!boton) return; // el clic no era en un botón nuestro
const id = Number(boton.dataset.id);
añadirAlCarrito(id);
});
Tres ventajas, y son grandes: una sola escucha en lugar de cincuenta; funciona con las tarjetas que tu código creará después; y no hay que registrar nada de nuevo tras volver a pintar.
closest
Sube por los antepasados desde el elemento pulsado hasta encontrar uno que case con el selector. Resuelve el problema de que el clic caiga en un icono o en el texto de dentro del botón, y no en el botón mismo.
El teclado se atiende con su propio evento cuando hace falta una tecla concreta:
buscador.addEventListener("keydown", (evento) => {
if (evento.key === "Escape") limpiarBusqueda();
});
Si delegas clics en elementos que son botones de verdad, en cambio, el teclado ya funciona: pulsar Intro sobre un botón genera un click. Es otra razón para usar el elemento correcto en lugar de un div decorado.
Crear, rellenar, insertar
const item = document.createElement("li");
item.className = "producto";
item.dataset.id = producto.id;
const titulo = document.createElement("h3");
titulo.textContent = producto.nombre;
item.append(titulo);
catalogo.append(item); // al final
| Método | Qué hace |
|---|---|
append |
Añade al final del contenido |
prepend |
Añade al principio |
before / after |
Inserta como hermano |
remove |
Se elimina a sí mismo |
replaceChildren |
Sustituye la totalidad del contenido en una sola operación |
Añadir cincuenta elementos uno a uno hace que el navegador recalcule la página cincuenta veces. Se construyen aparte y se insertan juntos:
const fragmento = document.createDocumentFragment();
for (const producto of productos) {
fragmento.append(crearTarjeta(producto));
}
catalogo.replaceChildren(fragmento);
Para vaciar un contenedor:
catalogo.replaceChildren(); // lo deja vacío
catalogo.innerHTML = ""; // funciona, pero pasa por el analizador
replaceChildren() es más claro y más seguro, y admite también los nuevos hijos de una vez.
Lo que genera tu código pasa las mismas normas de la UD1
Es fácil que la lista de productos acabe siendo una pila de contenedores genéricos con clases. Si al escribirlo a mano usabas una lista de artículos con su encabezado, su imagen con texto alternativo y su precio, el código tiene que generar exactamente eso.
La comprobación: abre Elements, copia el marcado generado, pégalo en el validador del W3C. La jerarquía de encabezados sigue siendo exigible sobre ese marcado generado.
La función que lo construye devuelve el elemento sin insertarlo: quien la llama decide dónde va. Es la misma regla de la UD3 —calcular y devolver— aplicada al DOM.
export function crearTarjeta(producto) {
const item = document.createElement("li");
item.className = "producto";
item.dataset.id = producto.id;
const titulo = document.createElement("h3");
titulo.textContent = producto.nombre;
const imagen = document.createElement("img");
imagen.src = producto.imagen;
imagen.alt = producto.textoAlternativo;
imagen.loading = "lazy";
const precio = document.createElement("p");
precio.className = "precio";
precio.textContent = formatearPrecio(producto.precio);
item.append(imagen, titulo, precio);
return item;
}
El diagnóstico de esta unidad
- Un console.log como primera línea del manejador
- ¿Aparece? El evento llega: el fallo es de tu lógica
- ¿No aparece? ¿Existe el elemento, o era null?
- ¿El nombre del evento está bien escrito?
- ¿El elemento se creó después de registrar la escucha?
Ese último caso es el que resuelve la delegación, y por eso los tres bloques de hoy son en realidad uno.
Se trabaja
150 minutos · práctica sobre tu propio proyecto
Los tres primeros pasos construyen la interacción; los dos últimos comprueban lo que casi nadie comprueba: que el marcado generado siga siendo válido y que todo se pueda usar sin ratón.
Paso 1 · Tu página reacciona · 30 min
- Un botón que alterna la vista compacta del catálogo.
- Un campo de búsqueda que escribe en consola lo tecleado con
input. - El formulario de contacto que evita la recarga y muestra los valores.
- Un botón que solo funcione una vez, con
{ once: true }. - Prueba la diferencia entre
inputychangeen el mismo campo, y anótala.
Consultar · quitar un manejador
function alPulsar() { /* ... */ }
boton.addEventListener("click", alPulsar);
boton.removeEventListener("click", alPulsar);
Para poder quitarlo hace falta la misma referencia de función, así que una flecha escrita en el sitio no se puede retirar. También está la opción { once: true } para que se ejecute una sola vez.
Antes de continuar: el formulario no recarga la página, y sabes decir en qué línea lo has impedido.
Paso 2 · Un manejador para todos · 40 min
- Añade a cada tarjeta de tu catálogo un botón con
data-accionydata-id. - Registra una sola escucha en el contenedor.
- Usa
closestpara localizar el botón ydatasetpara el identificador. - Añade una segunda acción y distínguela por su
data-accion. - Comprueba con el teclado que ambas funcionan.
- Registra una escucha en el contenedor y otra en la tarjeta, y observa el orden en que se disparan. Anótalo: es el burbujeo, visto en tu propia página.
Paso 3 · La tarjeta generada · 45 min
Es el trabajo central de la sesión.
- Escribe
crearTarjeta(producto)enjs/render.js, que devuelva el elemento sin insertarlo. - Genera con ella las tarjetas de tres productos y añádelas con un fragmento.
- Añade un botón de acción con sus atributos
data-. - Escribe
vaciarCatalogo()conreplaceChildren()y comprueba que las escuchas delegadas siguen funcionando después de vaciar y volver a pintar. - Añade el estado «agotado» como clase para el CSS y como texto para quien no ve la pantalla. No basta con el color.
Consultar · la otra forma, con template
HTML tiene un elemento pensado para esto: <template> guarda marcado que no se pinta hasta que lo clonas.
<template data-js="plantilla-producto">
<li class="producto">
<h3></h3>
<p class="precio"></p>
</li>
</template>
const plantilla = document.querySelector("[data-js='plantilla-producto']");
const copia = plantilla.content.cloneNode(true);
copia.querySelector("h3").textContent = producto.nombre;
La estructura vive en el HTML y el código solo rellena. Tiene la ventaja de que el marcado se ve y se valida donde siempre, y el inconveniente de repartir la tarjeta entre dos ficheros. Las dos formas son correctas.
Paso 4 · Valida lo que ha escrito tu código · 15 min
- Compara el marcado generado en el paso 3 con el que tenías escrito a mano en la UD1. ¿Es igual de semántico?
- Abre Elements, copia el bloque del catálogo entero y pásalo por
validator.w3.org. - Comprueba la jerarquía de encabezados del documento con las tarjetas ya pintadas. Si tu
h3cuelga de unh1sinh2en medio, el salto lo ha creado tu JavaScript. - Revisa que cada imagen generada tiene su
alt, y que esealtsale de un campo de datos y no de una cadena fija.
| Comprobación | Resultado | Corrección |
|---|---|---|
| Validador del W3C sobre el marcado generado | ||
| Jerarquía de encabezados sin saltos | ||
Todas las imágenes con alt que aporta algo |
||
Los botones son button, no div |
Paso 5 · Todo esto, sin ratón · 20 min
Suelta el ratón y recorre la página con Tab, Shift + Tab, Intro y Espacio.
- ¿Puedes llegar a los botones generados por tu código?
- ¿Se ve el foco sobre ellos, con el
:focus-visiblede la UD2? - ¿Funcionan las dos acciones delegadas?
- Añade un atajo:
Escapelimpia la búsqueda, eIntroen el buscador aplica el filtro. - Después de vaciar y repintar el catálogo, ¿dónde se ha quedado el foco? Anótalo. Es un problema real que resolverás en la sesión 6.
Ampliación si has completado el trabajo
Primero termina y comprueba los cinco pasos. Los dos retos miden el coste de dos atajos que parecen inofensivos.
Reto 1 · Cuánto cuesta un botón falso
<div class="boton" onclick="comprar()">Comprar</div>
- Móntalo y comprueba con el teclado qué no hace: enumera las cuatro cosas que un
<button>trae de fábrica y este no. - Recontrúyelas una a una: hazlo enfocable, que responda a
Introy aEspacio, que se anuncie como botón, y que tenga foco visible. Cuenta las líneas de HTML, CSS y JavaScript que te ha costado. - Compara ese recuento con escribir
<button type="button">. - Queda todavía una diferencia que no has podido reconstruir del todo: averigua qué ocurre con ese falso botón dentro de un formulario, y con el modo de alto contraste del sistema.
- Escribe en tres líneas por qué este ejercicio no va de accesibilidad, sino de no reprogramar lo que la plataforma ya hace. Es la misma conclusión que sacaste en la UD1 con
detailsyprogress.
Reto 2 · El menú que deja de cerrarse
Monta esta situación, que es completamente habitual:
· Un menú desplegable que se abre con un botón.
· Una escucha en `document` que lo cierra al pulsar en cualquier otro sitio.
· Un catálogo de tarjetas; cada tarjeta abre el detalle al pulsarla.
· Dentro de cada tarjeta, un botón «Añadir» que NO debe abrir el detalle.
- Resuelve el botón «Añadir» con
stopPropagationy comprueba que funciona. - Ahora abre el menú y pulsa ese botón. El menú no se cierra. Explica exactamente por qué, siguiendo el camino del evento.
- Resuélvelo de nuevo sin
stopPropagation: el manejador de la tarjeta debe filtrar portargety salir pronto si el clic venía de un botón de acción. - Comprueba que ahora el menú sí se cierra y que el detalle sigue sin abrirse.
- Enuncia la regla en una frase: quién debe decidir si un evento le incumbe, el que lo emite o el que lo escucha.
submit sin recarga, una escucha delegada que distingue dos acciones, y tarjetas generadas semánticas.stopPropagation.Cierre
5 minutos · comprobación y recuerdo
Lista de verificación de la sesión
- Usas
submiten el formulario ypreventDefault. - Distingues
inputdechange. - Usas una sola escucha delegada por contenedor, con
closest. - Tu página responde al ratón y al teclado por igual.
- Generas elementos con marcado semántico, validado desde Elements.
- Insertas en bloque con un fragmento, no de uno en uno.
Antes de cerrar · 3 minutos, sin mirar
- ¿Por qué escuchamos
submity no el clic del botón? - ¿Qué hace
preventDefault? - ¿Qué diferencia hay entre
targetycurrentTarget? - ¿Qué ventaja tiene delegar, y para qué sirve
closest? - ¿Por qué evitamos
stopPropagation? - ¿Por qué se insertan los elementos con un fragmento?
Ver respuestas
1 · Porque un formulario también se envía con Intro, y el clic no cubre ese caso.
2 · Cancela el comportamiento por defecto del navegador para ese evento.
3 · target es el elemento exacto donde ocurrió; currentTarget, el que tiene registrada la escucha.
4 · Una sola escucha que funciona también con los elementos creados después; closest sube desde el punto pulsado hasta el contenedor que interesa.
5 · Porque impide que otros manejadores más arriba se enteren del evento, y eso rompe cosas a distancia.
6 · Porque cada inserción individual obliga al navegador a recalcular la página.
Microprueba semanal 2 · 5–10 minutos
Individual, sin IA y sin apuntes.
- Escribe la escucha que impide que un formulario recargue la página.
- Explica por qué una escucha delegada funciona con elementos que aún no existen.
- ¿Qué diferencia hay entre
targetycurrentTarget?