En la unidad anterior registraste listeners en elementos concretos. Pero un clic sobre un <li> también es un clic sobre la lista que lo contiene, sobre el <body> y sobre el documento entero. El navegador lo resuelve haciendo viajar el evento por el árbol, y ese viaje permite una técnica muy útil: atender los eventos de muchos elementos con un solo listener.
1 // Las tres fases de un evento
Cuando ocurre un evento sobre un elemento, el navegador no avisa sólo a ese elemento. Lo hace recorrer el árbol DOM en tres fases:
- Captura: el evento baja desde
windowpasando por cada antepasado hasta llegar al elemento. - Destino: el evento llega al elemento donde se originó, el target.
- Burbuja: el evento sube de vuelta desde el elemento hasta
window, pasando otra vez por cada antepasado.
En cada paso, el navegador ejecuta los listeners que haya registrados en ese elemento para ese tipo de evento. Por defecto, addEventListener escucha en la fase de burbuja. Si pasas la opción { capture: true }, escucha en la de captura, algo que rara vez necesitarás.
lista.addEventListener('click', () => console.log('ul'));
item.addEventListener('click', () => console.log('li'));
// Al hacer clic en el li se muestra: 'li' y después 'ul'La mayoría de eventos suben por burbuja, pero no todos: focus y blur no lo hacen. Si necesitas algo equivalente que suba, existen focusin y focusout.
2 // target y currentTarget
Como un mismo evento pasa por varios elementos, el objeto evento distingue dos cosas:
| Propiedad | Qué es | ¿Cambia durante el viaje? |
|---|---|---|
e.target |
El elemento donde se originó el evento | No |
e.currentTarget |
El elemento cuyo listener se está ejecutando | Sí, en cada paso |
Si el <li> contiene un <span> y el usuario pulsa justo sobre el texto del span, e.target será el span, no el li. Este detalle es la causa de muchos errores y es la razón por la que se usa closest, como verás enseguida.
3 // Detener la propagación
El método e.stopPropagation() detiene el viaje del evento: los listeners de los elementos siguientes en el recorrido no se ejecutarán.
botonInterior.addEventListener('click', (e) => {
e.stopPropagation(); // la tarjeta que lo contiene no se entera del clic
borrarTarjeta();
});No confundas stopPropagation() con preventDefault(). El primero corta el viaje del evento por el árbol; el segundo cancela la acción por defecto del navegador (enviar, navegar). Son independientes: puedes usar uno, el otro o los dos. Y usa stopPropagation con moderación, porque todo lo que escuche más arriba dejará de recibir el evento.
4 // Delegación de eventos
Imagina una lista de cien tareas, cada una con un botón «Borrar». Podrías registrar cien listeners, pero hay dos problemas: consumen memoria y, sobre todo, las tareas que añadas después no tendrán listener, porque se crearon cuando el registro ya se había hecho.
La delegación aprovecha la burbuja: se registra un único listener en un contenedor que siempre existe y, dentro, se averigua qué elemento concreto originó el clic.
const lista = document.querySelector('#lista');
lista.addEventListener('click', (e) => {
const boton = e.target.closest('.borrar');
if (!boton) return; // el clic no fue en un botón de borrar
const item = boton.closest('li');
item.remove();
});El patrón tiene siempre la misma forma:
- Un listener en el contenedor.
e.target.closest(selector)para encontrar el elemento que te interesa, aunque el clic haya caído en un icono o un texto de dentro.- Si no hay coincidencia, salir sin hacer nada.
- Si la hay, actuar sobre él, normalmente usando sus atributos
data-*para saber a qué dato corresponde.
Usa delegación siempre que los elementos se creen o destruyan dinámicamente, o cuando haya muchos elementos iguales. Un único listener en el contenedor funciona para los elementos actuales y para todos los que se añadan en el futuro.
Una lista de tareas se pinta desde un array con la función pintar de la unidad anterior. Cada li tiene data-id y dentro un botón con la clase completar. Al pulsarlo, la tarea debe marcarse como hecha en los datos y la lista debe volver a pintarse.
lista.addEventListener('click', (e) => {
const boton = e.target.closest('.completar');
if (!boton) return;
const id = Number(boton.closest('li').dataset.id);
const tarea = tareas.find(t => t.id === id);
tarea.hecha = !tarea.hecha;
pintar(tareas);
});- Un listener, registrado una vez: está en
#lista, que no se sustituye nunca;pintarsólo cambia sus hijos. - Identificar el botón:
closest('.completar')funciona tanto si el clic cae en el botón como en un icono de dentro. - Pasar del DOM a los datos:
dataset.ides texto, así que se convierte conNumberpara compararlo con===. - Cambiar los datos y repintar: no se toca la clase del
lia mano; se actualiza el array y se vuelve a generar la lista.
pintar destruya y recree todos los li en cada cambio, porque el listener no está en ellos sino en su contenedor. Con listeners individuales, tras el primer repintado ningún botón respondería.5 // Para practicar
Observa el viaje de un evento por la consola y aplica la delegación a una lista que se repinta entera. Cada ejercicio trae el enunciado en los comentarios del código.
Fases de captura y burbujatraining-daw-sv/modulo-01/04-propagacion-y-delegacion/01_event_phasesLista de tareas con delegacióntraining-daw-sv/modulo-01/04-propagacion-y-delegacion/02_task_delegationAutoevaluación
Tienes un listener de click en un <ul> y haces clic en un <li> de dentro. ¿Qué valen e.target y e.currentTarget en ese listener?
e.target es el <li> pulsado y e.currentTarget, el <ul>. target es donde se originó el evento y no cambia durante todo el recorrido. currentTarget es el elemento cuyo listener se está ejecutando en ese momento, que aquí es la lista.Añades con JavaScript nuevas filas a una tabla, y los botones «Borrar» de las filas nuevas no funcionan, mientras que los de las filas iniciales sí. ¿Qué ha pasado y cómo lo resuelve la delegación?
¿Por qué no es buena idea usar stopPropagation() para evitar que un clic dentro de un menú lo cierre?
document dejan de enterarse. Suele ser mejor que el listener que cierra el menú compruebe con closest si el clic fue dentro.