Eventos en JavaScript: clics, formularios y delegación

Un evento es algo que pasa en la página: un clic, una tecla, el envío de un formulario. Con addEventListener le dices al navegador qué función ejecutar cuando ocurra.

Un contador, una lista y un formulario

Para probarlo, guarda el código en un archivo .html, ábrelo en el navegador y abre la consola con F12 (en Mac, Cmd+Opción+J). Todos los ejemplos de esta página son un único archivo, sin nada más que instalar.

<!doctype html>
<html lang="es">
<head><meta charset="utf-8"><title>Eventos</title></head>
<body>
  <button id="contador">Pulsado 0 veces</button>

  <ul id="tareas">
    <li>Comprar pan <button class="borrar">×</button></li>
    <li>Llamar a Luis <button class="borrar">×</button></li>
  </ul>

  <form id="alta">
    <input name="tarea" placeholder="Nueva tarea" required>
    <button>Añadir</button>
  </form>

  <script type="module">
    // 1. Un evento en un elemento
    const boton = document.querySelector("#contador");
    let veces = 0;
    boton.addEventListener("click", () => {
      veces++;
      boton.textContent = `Pulsado ${veces} veces`;
    });

    // 2. Delegación: un solo escuchador para todos los botones de la lista,
    //    también para los que se añadan después
    const tareas = document.querySelector("#tareas");
    tareas.addEventListener("click", (evento) => {
      const borrar = evento.target.closest("button.borrar");
      if (!borrar) return;
      const li = borrar.closest("li");
      console.log("borro:", li.firstChild.textContent.trim());
      li.remove();
    });

    // 3. Formularios: impedir que la página se recargue
    const alta = document.querySelector("#alta");
    alta.addEventListener("submit", (evento) => {
      evento.preventDefault();
      const datos = new FormData(alta);
      const li = document.createElement("li");
      li.append(datos.get("tarea") + " ");
      const b = document.createElement("button");
      b.className = "borrar";
      b.textContent = "×";
      li.append(b);
      tareas.append(li);
      console.log("añadida:", datos.get("tarea"));
      alta.reset();
    });
  </script>
</body>
</html>

Pulsamos tres veces el contador, borramos «Comprar pan», añadimos «Pagar la luz» con el formulario y la borramos después:

consolaborro: Comprar pan
añadida: Pagar la luz
borro: Pagar la luz

El botón acabó diciendo «Pulsado 3 veces», en la lista quedó solo «Llamar a Luis» y la dirección de la página no cambió: el formulario no la recargó.

Las tres ideas del ejemplo

La función recibe el evento. Es un objeto con los detalles de lo que pasó: evento.target es el elemento exacto donde ocurrió, y métodos como preventDefault() permiten cancelar lo que el navegador haría por su cuenta.

preventDefault() en el formulario. Al enviar un formulario, el navegador recarga la página o navega a otra. Si lo vas a gestionar con JavaScript, hay que impedirlo, y FormData recoge los valores de todos sus campos por su atributo name.

Delegación. En vez de poner un escuchador en cada botón «×», hay uno solo en la lista. Funciona porque el clic en un botón también llega a la lista que lo contiene; closest("button.borrar") averigua si venía de un botón de borrar. Y tiene una ventaja enorme: sirve también para las tareas que se añaden después, como «Pagar la luz», que no existía cuando se registró el escuchador.

Por qué funciona la delegación: captura y burbuja

Fase de captura de fuera hacia dentro y fase de burbuja de dentro hacia fueraUn clic viaja dos veces: baja capturando y sube burbujeandodiv#fueradiv#mediobutton#dentro1. capturafuera, medio,dentro{ capture: true }2. burbujadentro, medio,fueralo normalLa delegación aprovecha la burbuja: un escuchador en el ul atiende los clics de todos sus li.event.target es donde se hizo clic; event.currentTarget, quien escucha.

Un clic recorre el árbol del DOM dos veces. Casi todos los escuchadores actúan en la segunda pasada, la burbuja. Esquema: decodigo.com.
<!doctype html>
<html lang="es">
<head><meta charset="utf-8"><title>Captura y burbuja</title></head>
<body>
  <div id="fuera">
    <div id="medio">
      <button id="dentro">Púlsame</button>
    </div>
  </div>
  <script type="module">
    for (const id of ["fuera", "medio", "dentro"]) {
      const el = document.getElementById(id);
      el.addEventListener("click", (e) => console.log("captura:", id), { capture: true });
      el.addEventListener("click", (e) =>
        console.log("burbuja:", id, "| target:", e.target.id, "| currentTarget:", e.currentTarget.id));
    }
    document.getElementById("medio").addEventListener("click", (e) => {
      if (e.altKey) {
        e.stopPropagation();
        console.log("medio detiene la burbuja");
      }
    });
    document.getElementById("dentro").addEventListener("click", () => console.log("solo la primera vez"), { once: true });
  </script>
</body>
</html>

Un clic en el botón, luego otro, y un tercero con la tecla Alt pulsada:

consolacaptura: fuera
captura: medio
captura: dentro
burbuja: dentro | target: dentro | currentTarget: dentro
solo la primera vez
burbuja: medio | target: dentro | currentTarget: medio
burbuja: fuera | target: dentro | currentTarget: fuera
--- segundo clic
captura: fuera
captura: medio
captura: dentro
burbuja: dentro | target: dentro | currentTarget: dentro
burbuja: medio | target: dentro | currentTarget: medio
burbuja: fuera | target: dentro | currentTarget: fuera
--- con Alt
captura: fuera
captura: medio
captura: dentro
burbuja: dentro | target: dentro | currentTarget: dentro
burbuja: medio | target: dentro | currentTarget: medio
medio detiene la burbuja

Primero el evento baja desde fuera hasta el botón (captura), y después sube de vuelta (burbuja). En todos, target es el botón, pero currentTarget cambia: es el elemento cuyo escuchador se está ejecutando. Con { once: true }, el escuchador se borra solo tras la primera vez. Y stopPropagation(), en el tercer clic, detiene la burbuja en «medio»: «fuera» ya no se entera.

No abuses de stopPropagation(). Cortar la burbuja rompe cualquier delegación que haya más arriba, incluida la de bibliotecas o herramientas de estadísticas. Casi siempre basta con comprobar event.target.

El error frecuente: los paréntesis

<!doctype html>
<html lang="es">
<head><meta charset="utf-8"><title>Error frecuente</title></head>
<body>
  <button id="saludar">Saludar</button>
  <script type="module">
    function saludar() {
      console.log("¡hola!");
    }
    const boton = document.querySelector("#saludar");
    boton.addEventListener("click", saludar()); // mal: la llama ahora mismo
    console.log("página cargada");
  </script>
</body>
</html>
consola¡hola!
página cargada
(dos clics en el botón: no aparece nada más)

saludar(), con paréntesis, ejecuta la función en ese momento y le pasa a addEventListener lo que devuelve, que es undefined. Por eso «¡hola!» sale al cargar y los clics no hacen nada. Hay que pasar la función sin llamarla: addEventListener("click", saludar), o envolverla en una flecha si necesita argumentos: () => saludar("Ana").

Los eventos más usados

EventoCuándo salta
clickClic o toque; también al pulsar Enter sobre un botón
inputCada vez que cambia el texto de un campo, letra a letra
changeAl confirmar el cambio: salir del campo, elegir en una lista
submitAl enviar un formulario, con el botón o con Enter
keydownAl pulsar una tecla; evento.key dice cuál
DOMContentLoadedCuando el HTML está leído, sin esperar a las imágenes

Cada página de esta sección se abrió en Chromium 131, el motor de Chrome, controlado con Playwright, antes de publicarla; lo que aparece como consola es lo que imprimió de verdad. Documentación oficial: addEventListener en MDN.