Maquetación Web Dinámica / JavaScript para maquetadores: darle vida al DOM
Delegación de eventos: un listener para muchos
Tienes una lista de 100 elementos, cada uno con un botón de borrar. ¿Pones 100 listeners? ¿Y qué pasa cuando agregas el elemento 101 — le pones otro a mano? Hay una forma mucho mejor: UN solo listener en el padre que atiende a todos sus hijos, presentes y futuros. Se llama delegación de eventos, y es uno de esos trucos que, una vez lo entiendes, usas para siempre.
El burbujeo: el evento sube por el árbol
El patrón: escuchar en el padre, identificar con closest
// UN listener en el contenedor padre, no en cada hijo
lista.addEventListener("click", (event) => {
// ¿el clic salió de un botón .borrar (o algo dentro de él)?
const boton = event.target.closest(".borrar");
if (!boton) return; // el clic fue en otra parte: ignora
boton.closest("li").remove(); // borra el <li> que lo contiene
});La pieza clave es event.target.closest(selector): sube desde el
elemento clicado buscando el ancestro más cercano que coincida con el
selector (incluido él mismo). Resuelve el problema de "el usuario pudo
hacer clic en el icono DENTRO del botón, no en el botón exacto" — closest
encuentra el botón de todos modos. Si no hay coincidencia, devuelve null
y el return ignora el clic.
La magia: funciona con elementos futuros
<div class="app">
<button id="agregar">+ Agregar tarea</button>
<ul id="lista">
<li><span>Estudiar el DOM</span><button class="borrar" aria-label="Borrar">✕</button></li>
<li><span>Practicar delegación</span><button class="borrar" aria-label="Borrar">✕</button></li>
</ul>
</div>body { font-family: system-ui; padding: 20px; background: #f8fafc; }
.app { max-width: 340px; }
#agregar { margin-bottom: 12px; padding: 9px 14px; border: 0; border-radius: 8px; background: #6d28d9; color: white; font-weight: 600; font-size: 14px; cursor: pointer; }
ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 8px; }
li { display: flex; align-items: center; justify-content: space-between; gap: 10px; background: white; border: 1px solid #e2e8f0; border-radius: 8px; padding: 10px 12px; font-size: 14px; color: #1e293b; }
.borrar { border: 0; background: #fee2e2; color: #b91c1c; width: 26px; height: 26px; border-radius: 6px; font-size: 13px; cursor: pointer; }const lista = document.querySelector("#lista");
const agregar = document.querySelector("#agregar");
let n = 3;
// UN listener para TODOS los botones de borrar (presentes y futuros)
lista.addEventListener("click", (event) => {
const boton = event.target.closest(".borrar");
if (!boton) return;
boton.closest("li").remove();
});
// agrega tareas nuevas: NO necesitan su propio listener
agregar.addEventListener("click", () => {
const li = document.createElement("li");
li.innerHTML = "<span>Tarea nueva " + n + "</span><button class='borrar' aria-label='Borrar'>✕</button>";
lista.appendChild(li);
n++;
});Aquí está lo poderoso: agrega tareas nuevas con el botón y bórralas — SE
BORRAN, aunque nunca les pusiste un listener. Como el único listener vive
en el <ul> padre y los clics de los hijos nuevos también burbujean hasta
él, funcionan automáticamente. Con listeners individuales tendrías que
acordarte de añadir uno a cada elemento creado; con delegación, el padre
ya los cubre a todos.
Tienes una lista donde el usuario AGREGA elementos dinámicamente. Le pusiste un listener a cada botón con querySelectorAll en la carga. Los botones viejos funcionan, los nuevos no. ¿Por qué, y la solución?
Mini-reto
Extiende el ejemplo: además de borrar, haz que un clic sobre el TEXTO de
una tarea (el <span>) le ponga una clase .hecha que la tache (usa
event.target.closest("span") o revisa event.target.tagName). Todo con
el MISMO listener del <ul>: dentro, decides qué hacer según de dónde
salió el clic. Un solo listener, varias acciones — la delegación en su
máxima expresión.
Qué sigue
Ya borras y agregas elementos sueltos. La próxima lección formaliza eso en un patrón central de las apps modernas: RENDERIZAR desde datos. Tienes un array de objetos (noticias, productos, tareas) y generas el HTML de la lista a partir de él — el puente entre "tengo datos" y "se ven en pantalla".