WA2.1 Eventos a fondo: delegación, propagación y eventos propios¶
Cómo se han comprobado los ejemplos
Los editores con vista previa se ejecutan en tu navegador al abrir la página: puedes cambiarlos y ver el resultado. Los fragmentos «ejecutado con Node» (lógica sin document) se han ejecutado de verdad al construir la web y su salida es la real.
El problema: mil botones, mil oyentes¶
Imagina una lista de tareas con un botón «borrar» en cada fila. La solución ingenua es registrar un addEventListener en cada botón. Con cien filas son cien oyentes, y las filas que se añadan después no tienen ninguno.
Propagación: el evento sube¶
Cuando haces clic en un elemento, el navegador avisa a ese elemento y luego a todos sus padres, hacia arriba, hasta el documento. Es la fase de burbuja (bubbling). Eso es lo que hace posible la técnica más útil de esta página.
Un solo clic en el botón se oye tres veces: primero en hijo, luego en padre y por último en abuelo. Dos propiedades del evento son la clave:
| Propiedad | Qué es |
|---|---|
event.target |
El elemento donde ocurrió el clic (el más profundo) |
event.currentTarget |
El elemento al que pertenece el oyente que se está ejecutando |
Delegación de eventos¶
Como el evento sube, basta un solo oyente en el padre, que mira event.target para saber qué hijo lo provocó. Funciona para los elementos que ya existen y para los que se añadan después.
Prueba a añadir varias tareas, marcarlas y borrarlas: todas funcionan con un único addEventListener. Dos detalles importantes:
closest(selector)sube por los padres hasta encontrar uno que coincida. Es imprescindible porque el clic puede caer en un icono dentro del botón:e.targetsería el icono, no el botón.data-accionguarda en el HTML lo que quiere hacer cada botón. Se lee condataset(data-accion→dataset.accion).
Parar la propagación y cancelar la acción por defecto¶
Son dos cosas distintas que se confunden mucho:
| Método | Qué evita |
|---|---|
event.preventDefault() |
La acción por defecto del navegador (seguir un enlace, enviar un formulario, abrir el menú contextual) |
event.stopPropagation() |
Que el evento siga subiendo a los padres |
Usa stopPropagation con cuidado
Si cortas la propagación, otros oyentes de arriba dejan de enterarse (un menú que debía cerrarse al hacer clic fuera, un sistema de estadísticas...) y es muy difícil encontrar por qué. Casi siempre se puede resolver con una comprobación en el propio oyente.
Opciones de addEventListener¶
El tercer parámetro admite opciones:
| Opción | Efecto |
|---|---|
{ once: true } |
El oyente se ejecuta una sola vez y se elimina solo |
{ passive: true } |
Promete que no llamará a preventDefault: el navegador puede desplazar la página sin esperar. Útil en scroll, touchmove y wheel |
{ capture: true } |
Escucha en la fase descendente (de arriba abajo), antes que los hijos |
{ signal } |
Un AbortSignal: al cancelarlo, el oyente se elimina |
La última es la forma más cómoda de limpiar varios oyentes de golpe, sin tener que guardar cada función para llamar a removeEventListener:
Eventos propios: CustomEvent¶
Los elementos pueden emitir sus propios eventos. Es una forma de que las piezas de una página se comuniquen sin conocerse: una emite, otra escucha. EventTarget y CustomEvent existen también en Node, así que se puede comprobar con un ejemplo ejecutado:
const bus = new EventTarget(); // cualquier objeto «escuchable»
bus.addEventListener("pedido:nuevo", (e) => {
console.log("cocina recibe:", e.detail.plato, "x", e.detail.cantidad);
});
bus.addEventListener("pedido:nuevo", (e) => {
console.log("caja cobra:", (e.detail.cantidad * e.detail.precio).toFixed(2), "€");
});
bus.dispatchEvent(new CustomEvent("pedido:nuevo", { detail: { plato: "tortilla", cantidad: 2, precio: 4.5 } }));
bus.dispatchEvent(new CustomEvent("otro:evento")); // nadie lo escucha: no pasa nada
Salida (ejecutado con Node):
cocina recibe: tortilla x 2
caja cobra: 9.00 €
detail lleva los datos. En una página, el evento sale de un elemento y sube por el DOM si lo creas con { bubbles: true }:
Con eventos propios, el botón no necesita saber quién reacciona a «aceptado»: se pueden añadir oyentes (analítica, un aviso, un guardado) sin tocar el botón.
<template> y dataset: pintar listas sin pegar cadenas¶
Construir HTML pegando cadenas (innerHTML = "<li>" + dato + "</li>") es incómodo y, con datos del usuario, peligroso (XSS). El elemento <template> guarda un trozo de HTML que no se muestra, y se clona tantas veces como haga falta:
Tres ideas en un solo ejemplo:
template.content.cloneNode(true)da una copia nueva de la plantilla cada vez.DocumentFragmentpermite montar todo fuera de la página y añadirlo de una vez: tocar el DOM es lo caro, así que cuantas menos veces, mejor.textContenten lugar deinnerHTML: el<b>JS</b>del título aparece como texto, sin interpretarse. Si el dato viniera de un usuario, no podría colar código.
Errores frecuentes¶
| Error | Cómo evitarlo |
|---|---|
| Un oyente por cada elemento de una lista | Delegación: un oyente en el padre y e.target.closest(...) |
Usar e.target directamente cuando el botón tiene un icono dentro |
e.target.closest("button") |
Confundir preventDefault con stopPropagation |
Uno cancela la acción del navegador; el otro corta la subida |
stopPropagation por todas partes |
Comprobar la condición dentro del propio oyente |
| Oyentes que se acumulan al repintar una parte de la página | { signal } con un AbortController por cada repintado |
| Construir HTML pegando cadenas con datos del usuario | <template>, createElement y textContent |
| Añadir elementos uno a uno dentro de un bucle | DocumentFragment y una sola inserción |
Para practicar¶
Los ejercicios WA2.5 y WA2.6 de WA2 · Ejercicios son de DOM; el WA2.3 usa un bus de eventos.