Un listener por elemento
N botones, N listeners, memoria que crece con la lista. Un solo listener en el padre resuelve todo.
- delegación
- burbujeo
- closest
El problema
Una lista de productos con un botón “añadir al carrito” en cada uno. Registras un listener por botón. Funciona. Pero luego el catálogo crece a 200 productos, cada uno con su listener; y cuando llegan productos nuevos por scroll infinito, esos no tienen listener y no responden hasta que vuelvas a registrar todos. La memoria y el mantenimiento crecen con la lista.
Hay un patrón que resuelve las dos cosas a la vez, aprovechando algo que el DOM hace por defecto: los eventos burbujean.
El código “roto”
const botones = document.querySelectorAll(".add-to-cart");
botones.forEach((boton) => {
boton.addEventListener("click", (e) => {
const id = e.target.dataset.id;
console.log("Añadido al carrito:", id);
});
});
Las pistas
- Un solo listener en el contenedor padre.
e.target.closest(".add-to-cart")para saber qué se pulsó.- Guard clause para ignorar clicks fuera de botón.
Ver la solución
const listaArticulos = document.getElementById("lista-articulos");
listaArticulos.addEventListener("click", (e) => {
const boton = e.target.closest(".add-to-cart");
if (boton) {
const id = boton.dataset.id;
console.log("Añadido al carrito:", id);
}
});El patrón se llama delegación de eventos. Un evento click en el
DOM burbujea: sube desde el elemento pulsado hasta document, pasando
por cada ancestro. Un listener en el contenedor padre recibe todos los
clicks de sus descendientes, y con e.target sabe cuál fue el
elemento original.
closest(".add-to-cart") es la pieza clave. e.target es exactamente
el nodo que recibió el click; si tu botón contiene un <svg> de icono,
el usuario pulsa sobre el icono y e.target es el <svg>, no el botón.
closest sube por la cadena hasta encontrar un ancestro (o el mismo
elemento) que cumpla el selector. Si no lo encuentra devuelve null,
y por eso la guard clause if (boton) ignora los clicks que caen
fuera de un botón.
Dos ganancias directas: un solo listener en memoria en vez de N, y los productos que se añadan dinámicamente después funcionan sin volver a registrar nada. Es el patrón por defecto para cualquier UI con listas o grids interactivos.
N listeners por botón vs delegación con closest
Cada botón tiene un icono (+) y un texto dentro. En modosin delegación: se registra un listener por botón, ye.target.dataset.id devuelve undefined cuando pulsas el icono o el texto (porque el target es el hijo, no el botón). Además, si añades productos nuevos, sus botones no responden — nadie les registró listener. En modo con delegación: un solo listener en el contenedor con closest(".add-to-cart") resuelve las dos cosas.
Comprueba lo que has aprendido
1. ¿Qué problema hay con registrar un listener por cada botón?
2. ¿En qué consiste la delegación de eventos?
3. ¿Por qué usar `e.target.closest(".add-to-cart")` en lugar de comparar `e.target` directamente?
4. ¿Qué es el burbujeo de eventos?
5. En el patrón de delegación, ¿qué hace la guard clause `if (boton)`?