Saltar a contenido

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.target sería el icono, no el botón.
  • data-accion guarda en el HTML lo que quiere hacer cada botón. Se lee con dataset (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.
  • DocumentFragment permite 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.
  • textContent en lugar de innerHTML: 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.