Ir al contenido
wannacode

Eventos

En resumen: Los eventos avisan a JavaScript de que alguien hizo clic o escribió: addEventListener escucha, casi todos los eventos suben por el árbol, y la delegación usa un solo listener para muchos elementos.

Problema

El producto nuevo no se añade

El catálogo muestra dos productos. «Mostrar más» añade un tercero. «Añadir» en los dos primeros sube el carrito, pero «Añadir» en el producto nuevo no hace nada. La consola no muestra ningún error.

HTML, CSS, JS

Haz clic en «Añadir» en el pan, luego en «Mostrar más» y luego en «Añadir» en el queso. El carrito se queda en 1. El botón del queso se ve igual que los demás y tiene el mismo texto. ¿Qué le falta?

La idea

Evento y listener

Cuando la compradora hace clic, pulsa una tecla o escribe en un campo, el navegador crea un evento: un objeto que dice qué pasó y dónde. El código lo espera con un listener (un oyente):

const button = document.querySelector('#plus');
button.addEventListener('click', (event) => {
console.log(event.type, event.target);
});
  • El primer argumento es el tipo de evento: click, keydown, input, submit.
  • El segundo es una función. El navegador la llama en cada evento y le pasa el objeto del evento. Las funciones flecha vienen de la lección sobre lambdas del curso de JavaScript.
  • Pasas la función, no la llamas: addEventListener('click', save), sin paréntesis. save() la llamaría enseguida.

El objeto del evento tiene campos:

  • event.target es el elemento donde ocurrió el evento: el botón en el que se hizo clic.
  • event.currentTarget es el elemento cuyo listener se está ejecutando ahora.
  • event.key es la tecla en keydown: 'Enter', 'Escape', 'a'.

Ahora está claro qué falta en el problema. Los listeners se ponen una vez, al cargar la página, en los botones que existen en ese momento. El botón del queso se crea más tarde y no tiene listener.

Pruébalo

Más y menos

Haz los botones de cantidad: «+» aumenta la cantidad en 1 y «−» la reduce en 1, pero no por debajo de 1. La cantidad se muestra en #qty.

HTML, CSS, JS

La idea

El evento sube

El botón está dentro de <li>, <li> está dentro de <ul>, y <ul> está dentro de <main>. Un clic en el botón es también un clic en cada uno de ellos. Así que el evento no se queda en el botón. Empieza en event.target y sube (bubbling) por los padres hasta document y window. Cada padre con un listener de click lo oye.

document.querySelector('ul').addEventListener('click', (event) => {
console.log(event.target); // el botón en el que se hizo clic
console.log(event.currentTarget); // <ul>: el elemento con el listener
});

Casi todos los eventos suben: click, keydown, input, submit. Algunos no: focus y blur. Para ellos existen focusin y focusout, que sí suben.

Míralo

Mira cómo sube el clic

A la izquierda está el árbol del catálogo. El listener está en <ul>, y el clic es en el botón de la leche.

const list = document.querySelector('#products');
list.addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
const item = button.closest('li');
console.log('Añadir', item.dataset.id);
});
Paso a paso, en palabras
  1. Hay un solo listener, y está en la lista <ul>, no en los botones. Espera click.
  2. La compradora hace clic en «Añadir» de la leche. El evento empieza en el elemento más interno bajo el cursor: el botón. Ese es event.target.
  3. Después sube: del botón a su padre <li>, luego a <ul>. Cada padre también oye el clic.
  4. <ul> tiene un listener y la función arranca. event.currentTarget es <ul>, el elemento con el listener. event.target sigue siendo el botón.
  5. closest('button') empieza en el propio event.target y sube. El destino es un botón, así que se encuentra enseguida.
  6. closest('li') sube desde el botón y se detiene en el primer <li>: la leche. Su data-id dice de qué producto se trata.
  7. dataset.id es "2". El carrito sabe qué tiene que añadir.
  8. El evento sigue subiendo hasta <main>, <body>, document y window. Ahí no hay listeners y no pasa nada más.
  9. Un clic en el nombre del pan también sube hasta <ul>. Pero el destino es <h2>: closest('button') no encuentra ningún botón por encima y devuelve null, y la función se detiene.
  10. Un solo listener atiende todos los botones de la lista, también los que se añaden más tarde: también están dentro de <ul>. Esto se llama delegación.

Un clic en el nombre del pan también llega a <ul>. Por eso el listener comprueba primero que se haya hecho clic en un botón.

La idea

Delegación

En lugar de un listener por botón, pones uno en el padre común. Él averigua con event.target en qué botón se hizo clic. Esto se llama delegación.

list.addEventListener('click', (event) => {
const button = event.target.closest('button');
if (!button) return;
const id = button.closest('li').dataset.id;
// …añadir el producto id
});
  • closest('button') empieza en el propio elemento y sube hasta el primero que coincide con el selector. Si no hay ninguno, devuelve null. Aquí protege de un clic en el nombre o en el espacio vacío del <li>.
  • Los datos del producto están en el <li> (data-id), no en el botón. closest('li') llega hasta ellos.

Hay dos ventajas. Los productos nuevos funcionan sin hacer nada más: también están en la lista. Y hay un solo listener, sean cuantos sean los productos.

Pruébalo

Arregla el catálogo

Arregla el catálogo con un solo listener en #products. «Añadir» en cada producto, incluido el que se añade con «Mostrar más», sube el carrito y escribe «producto añadido» y el data-id del producto en la consola. Un clic en el nombre de un producto no añade nada.

HTML, CSS, JS

La idea

La acción por defecto

Algunos eventos tienen una acción que el navegador hace por sí mismo, después de los listeners. Un clic en un enlace abre su dirección. submit en un formulario envía los campos y carga la página nueva. Esta es la acción por defecto (default action).

El formulario de búsqueda de aquí muestra el resultado en la página. Pruébalo.

HTML, CSS, JS

El listener escribe «Buscas: leche», pero después el navegador envía el formulario. En un navegador real la página se carga otra vez con ?q=leche en la dirección, y el resultado desaparece. El sandbox es una sola página: impide el envío y escribe «El formulario se envió» en la consola.

Añade event.preventDefault(); al principio de la función y ejecútala de nuevo. La línea de la consola desaparece: la acción está detenida, y el listener sigue haciendo su trabajo. Lo mismo vale para un enlace que abre algo dentro de la página en lugar de ir a otra dirección.

event.stopPropagation() detiene la subida. Rara vez lo necesitas, y casi siempre es mala señal. También detiene listeners que el código no conoce: la delegación de arriba, cerrar un menú al hacer clic fuera, contar clics. Si un listener «oye demasiado», comprueba event.target, como hace closest en la delegación.

Pruébalo

Encuentra el error

El listener muestra el resultado, pero el formulario se envía igualmente.

Elige la línea con el error.

La idea

input o change

Los campos tienen dos eventos para un cambio:

  • input llega con cada cambio del valor: cada letra, cada borrado, cada pegado.
  • change llega cuando la compradora termina. En un campo de texto, es cuando sale de él. En una casilla, un botón de opción y un menú desplegable, es justo después de elegir.

Para una búsqueda que filtra mientras escribes, usa input. Para un menú desplegable «Ordenar por precio», usa change.

Más precisamente, para curiosos

Un tercer argumento. addEventListener('click', f, { once: true }) escucha solo una vez. removeEventListener('click', f) quita el listener, pero solo si pasas la misma función. Una función flecha escrita otra vez es otra función distinta.

onclick. button.onclick = f también funciona, pero es un único campo: un segundo onclick borra el primero. addEventListener permite muchos listeners. El atributo onclick="…" en HTML mezcla el código con la página, y una política de seguridad estricta (CSP) lo bloquea.

Propagación y captura. El recorrido del evento tiene también otra dirección: primero de window hacia abajo hasta el destino (captura), luego hacia arriba (bubbling). addEventListener('click', f, { capture: true }) escucha en el camino de bajada. Casi siempre quieres el bubbling.

El teclado. Un <button> recibe click también con Enter y con la barra espaciadora. Por eso click en un botón funciona con el teclado. keydown en un <div> no lo sustituye (mira la lección sobre accesibilidad).

event.key frente a event.code. key es el carácter ('a' en un teclado QWERTY, 'q' para la misma tecla en un AZERTY francés), y code es dónde está la tecla ('KeyA' en ambos). Para atajos como Escape y Enter, usa key.

Ejercicio

Búsqueda en vivo

Haz que la búsqueda filtre la lista mientras la compradora escribe: solo se muestran los productos que contienen el texto, sean mayúsculas o minúsculas. Un producto oculto tiene hidden. #found muestra cuántos se ven («Encontrados: 2»). Escape en el campo lo vacía y muestra todos. Enter no envía el formulario.

HTML, CSS, JS

Test

Ponte a prueba

1. El listener está en <ul> y la compradora hace clic en un botón dentro de un <li>. ¿Qué son event.target y event.currentTarget?
2. ¿Qué tiene de malo button.addEventListener('click', save())?
3. ¿Por qué la delegación funciona también con los productos añadidos después de cargar la página?
4. Un formulario de búsqueda tiene un listener de submit que muestra el resultado. ¿Para qué necesitas event.preventDefault()?
5. Una búsqueda que filtra la lista mientras la compradora escribe. ¿Qué evento?

Creado por Ivo Gergov como parte de la campaña de Evo Tech por una educación accesible en programación ·