Pregunta 1Con saveButton.addEventListener("click", save());, ¿cuándo se llama a save?
Eventos — addEventListener y propagación
addEventListener y event.target para responder a clics, delegación de eventos con bubbling, stopPropagation, y cómo quitar listeners con once y removeEventListener.
Asignar a textContent cambia el elemento una sola vez, en el momento en que se ejecuta esa línea. Si quieres sumar 1 a un contador cada vez que alguien pulsa un botón, no te bastan las líneas que se ejecutan de arriba abajo, porque el código no sabe cuándo va a llegar el clic.
En este artículo verás addEventListener, que llama a una función cuando ocurre un clic, y cómo responder desde el elemento padre a los clics en sus hijos.
Llamar a una función al hacer clic — addEventListener
En la página de un producto, cada clic en el botón «Agregar al carrito» debe sumar 1 al número de productos del carrito y mostrarlo. Si escribes sin más la línea que actualiza el número, se ejecuta una sola vez, al cargar la página, y los clics posteriores en el botón no lo cambian.
Para que se llame a una función cuando ocurre un evento (algo que pasa en la página, como un clic o una entrada de texto), la registras pasando el nombre del evento y la función, como en addEventListener("click", función). A la función registrada se le llama event listener (o simplemente listener). En la consola no puedes hacer clic con el mouse, así que provocas los clics con el método click, como en cartButton.click().
document.body.innerHTML = `<button id="add-to-cart">Agregar al carrito</button><p>Carrito: <span id="cart-count">0</span> productos</p>`;
const cartButton = document.querySelector("#add-to-cart");
const cartCount = document.querySelector("#cart-count");
// La función que se llama al hacer clic (aquí solo se define)
function addToCart() {
cartCount.textContent = Number(cartCount.textContent) + 1;
}
// Pasa la propia función para registrarla (en esta línea no se llama a addToCart)
cartButton.addEventListener("click", addToCart);
console.log(cartCount.textContent); // 0
// Cada vez que provocas un clic, se llama al addToCart registrado
cartButton.click();
cartButton.click();
console.log(cartCount.textContent); // 2
En una página real no escribes click(): el navegador llama a addToCart en cuanto el usuario pulsa el botón. Solo responde a los clics que ocurren después del registro; los que se hicieron antes de la línea del registro no cuentan.
Con () se ejecuta una sola vez, en la línea del registro
Si escribes addEventListener("click", addToCart()), addToCart se llama una vez en la línea del registro, y lo que se pasa es su valor de retorno, undefined. Como no aparece ningún error, cuesta darse cuenta de por qué el número no sube al hacer clic. Al registrar, pasa solo el nombre de la función, sin ().
Recibir en el padre el clic de un hijo — bubbling y delegación
Ahora quieres leer el número de pedido de la fila que se pulsa en una lista de pedidos. Si reúnes las filas con querySelectorAll y registras un listener en cada una con forEach, solo tienen listener las filas que ya estaban en la página en ese momento, así que, si haces clic en una fila que agregaste después con append, no pasa nada.
Gracias al bubbling (el mecanismo por el que un evento que ocurre en un elemento pasa a su padre, luego al padre de ese padre, y así sucesivamente), un listener registrado en el ul padre también se llama cuando se hace clic en un li. El navegador le pasa al listener un objeto de evento (un objeto con la información del evento que ocurrió) como argumento. En el código de abajo, el argumento se llama event, y con su target lees el elemento donde se originó el clic.
document.body.innerHTML = `<section id="order-panel"><ul id="order-list"><li data-id="A-101">Pedido A-101</li><li data-id="A-102">Pedido A-102</li></ul></section>`;
const orderList = document.querySelector("#order-list");
const orderPanel = document.querySelector("#order-panel");
// Regístralo en el ul padre y en el section que lo rodea, no en los li
orderList.addEventListener("click", (event) => {
console.log(`Recibido en ul: ${event.target.dataset.id}`);
});
orderPanel.addEventListener("click", (event) => {
console.log(`Recibido en section: ${event.target.dataset.id}`);
});
// Provoca un clic en el li A-102
document.querySelector('[data-id="A-102"]').click();
// Recibido en ul: A-102
// Recibido en section: A-102
event.target.dataset.ides A-102
event.target.dataset.ides A-102
- El clic no llega a los li hermanos
event.targetes este li- Desde aquí el clic empieza a subir
Registrar un solo listener en el padre y usar event.target para saber en qué hijo se hizo clic se llama delegación de eventos. El clic de un li se recibe después de subir hasta el ul, así que el mismo listener se llama también para un li que agregaste con append después del registro. event.target es el elemento más interno en el que se hizo clic, así que, si el li contiene un span, apunta al span.
Impedir que el clic llegue al padre — stopPropagation
Piensa en una fila de pedido que abre su detalle al hacer clic y que contiene un botón «Eliminar» para borrar el pedido. El botón es hijo de la fila, así que el clic en «Eliminar» también sube hasta la fila por bubbling, y además de eliminar el pedido, se abre su detalle.
Que un evento pase a los elementos exteriores se llama propagación. stopPropagation (un método que detiene la propagación en el elemento cuyo listener se está ejecutando) se usa escribiendo event.stopPropagation() dentro del listener del botón. Una vez detenida, los listeners registrados en elementos exteriores, como la fila o la lista, no se llaman.
document.body.innerHTML = `<ul><li id="order-row">Pedido A-201 <button id="favorite">Favorito</button><button id="delete">Eliminar</button></li></ul>`;
const orderRow = document.querySelector("#order-row");
const favoriteButton = document.querySelector("#favorite");
const deleteButton = document.querySelector("#delete");
// Al hacer clic en la fila, se abre el detalle
orderRow.addEventListener("click", () => console.log("Abrir detalle"));
// Favorito: también puede abrir el detalle, así que no lo detiene y el clic llega a la fila
favoriteButton.addEventListener("click", () => console.log("Agregado a favoritos"));
favoriteButton.click(); // Agregado a favoritos, Abrir detalle (2 líneas)
// Eliminar: llama a stopPropagation() para que el clic no llegue a la fila
deleteButton.addEventListener("click", (event) => {
event.stopPropagation();
console.log("Pedido eliminado");
});
deleteButton.click(); // Pedido eliminado
En el listener de eliminar, «Pedido eliminado» se muestra aunque esté escrito después de stopPropagation(). Lo que se detiene es el paso del evento a los elementos exteriores, no el resto del listener en el que se llamó.
Un clic detenido tampoco llega al ul que delega
stopPropagation() impide que el evento llegue no solo a la fila que rodea el botón, sino a todos los elementos que hay más afuera. Aunque el ul de la lista tenga un listener delegado que registra las acciones del usuario, los clics en el botón de eliminar no quedan registrados. Detén solo los clics que ningún elemento exterior necesita recibir.
Quitar un listener — once y removeEventListener
Si un usuario pulsa dos veces seguidas el botón «Confirmar pedido», el listener registrado se llama dos veces y el mismo pedido se envía dos veces. Una vez aceptado el primer envío, tienes que lograr que el listener no se llame aunque vuelvan a pulsar el mismo botón.
Si pasas { once: true } como tercer argumento de addEventListener, el listener se quita solo después de llamarse una vez. Para quitarlo en el momento que quieras, llama a removeEventListener (un método que recibe un nombre de evento y una función, busca entre los listeners registrados la función que sea === a esa y la quita).
document.body.innerHTML = `<button id="confirm">Confirmar pedido</button>`;
const confirmButton = document.querySelector("#confirm");
const sendOrder = () => console.log("Pedido enviado");
// { once: true }: se quita solo después de llamarse una vez
confirmButton.addEventListener("click", sendOrder, { once: true });
confirmButton.click(); // Pedido enviado
confirmButton.click(); // (no se muestra nada)
// Si pasas el sendOrder registrado, se quita ese listener
confirmButton.addEventListener("click", sendOrder);
confirmButton.removeEventListener("click", sendOrder);
confirmButton.click(); // (no se muestra nada)
// Una arrow function escrita de nuevo es otra función, aunque tenga el mismo cuerpo, así que no se quita nada
confirmButton.addEventListener("click", () => console.log("Pedido enviado"));
confirmButton.removeEventListener("click", () => console.log("Pedido enviado"));
confirmButton.click(); // Pedido enviado
() => … crea una función nueva cada vez que la escribes, así que, aunque el cuerpo sea igual, al compararla con === con la función registrada da false. Si vas a quitar un listener más adelante, guarda antes la función en una variable, como sendOrder, y registra esa variable. La tabla de abajo resume las formas de trabajar con eventos que viste en este artículo.
| Código | Qué hace | Qué tener en cuenta |
|---|---|---|
| addEventListener("click", fn) | Llama a fn en cada clic | Pasa fn sin () |
| event.target | El elemento pulsado | El más interno, aun en el padre |
| Uno en el padre (delegación) | Recibe los clics de los hijos | También hijos agregados después |
| event.stopPropagation() | Detiene el paso hacia afuera | Ni el padre que delega lo recibe |
| { once: true } | Lo quita tras una llamada | Va en el 3.er argumento |
| removeEventListener("click", fn) | Quita el listener | Pasa la misma función |
Verificación de conocimientos
Responde cada pregunta una a una.
Pregunta 2Dentro de un listener registrado en un ul, ¿qué es event.target cuando llamas a click() sobre un li de ese ul?
Pregunta 3Después de registrar () => send(), ¿qué pasa si llamas a removeEventListener("click", () => send())?