Skip to content

Eventos ​

Un clic en "Agregar al carrito", una tecla presionada, un formulario enviado, el mouse que pasa sobre una imagen: todo eso son eventos. Escucharlos y reaccionar a ellos es lo que hace interactiva una página. En esta lección aprenderás a manejarlos como un profesional, incluida la técnica de delegación, imprescindible en listas dinámicas.

Concepto ​

Imagina un timbre: tú no te quedas parado junto a la puerta todo el día; instalas un timbre y cuando alguien lo toca, vas a abrir. En JavaScript:

  • El evento es que alguien toque el timbre (click).
  • El listener (escuchador) es el timbre instalado en un elemento.
  • El handler (manejador) es lo que haces cuando suena: una función.
js
const boton = document.querySelector('#comprar');

boton.addEventListener('click', () => {
  console.log('¡Clic en comprar!');
});

addEventListener ​

js
elemento.addEventListener(tipoDeEvento, funcion, opciones);
js
const boton = document.querySelector('#saludar');

// Con función con nombre (reutilizable y removible)
function saludar() {
  alert('¡Hola!');
}
boton.addEventListener('click', saludar);   // ¡sin paréntesis!

// Con función flecha
boton.addEventListener('click', () => console.log('Otro listener'));

Error común

boton.addEventListener('click', saludar()) ejecuta saludar inmediatamente y registra su resultado (undefined) como listener. Pasa la función, no su llamada: saludar sin paréntesis.

¿Y onclick?

También existe boton.onclick = saludar y el atributo HTML onclick="saludar()". Evítalos: sólo permiten un manejador por evento y mezclan HTML con JavaScript. addEventListener es el estándar profesional.

El objeto event ​

El navegador le pasa a tu función un objeto con toda la información del evento:

js
document.addEventListener('click', (event) => {
  console.log(event.type);          // 'click'
  console.log(event.target);        // el elemento exacto donde se hizo clic
  console.log(event.currentTarget); // el elemento que tiene el listener
  console.log(event.clientX, event.clientY); // posición del mouse
});

document.addEventListener('keydown', (e) => {
  console.log(e.key);       // 'a', 'Enter', 'Escape', 'ArrowUp'...
  console.log(e.code);      // 'KeyA' (tecla física)
  console.log(e.ctrlKey, e.shiftKey, e.altKey, e.metaKey);

  // Atajo Ctrl + K (o Cmd + K en Mac)
  if ((e.ctrlKey || e.metaKey) && e.key === 'k') {
    e.preventDefault();
    abrirBuscador();
  }
});

Eventos más usados ​

EventoCuándo ocurre
click, dblclickClic o doble clic
mouseenter, mouseleaveEl mouse entra o sale de un elemento
keydown, keyupSe presiona o suelta una tecla
inputEl valor de un campo cambia (en cada tecla)
changeEl valor cambia y el campo pierde el foco (o en select/checkbox)
submitSe envía un formulario
focus, blurUn campo gana o pierde el foco
scrollSe hace scroll
resizeCambia el tamaño de la ventana
DOMContentLoadedEl HTML terminó de cargarse
loadCargó todo (imágenes incluidas)
js
// input: búsqueda en vivo
const buscador = document.querySelector('#buscar');
buscador.addEventListener('input', (e) => {
  console.log('Buscando:', e.target.value);
});

// change: select
document.querySelector('#pais').addEventListener('change', (e) => {
  console.log('País elegido:', e.target.value);
});

// checkbox
document.querySelector('#acepto').addEventListener('change', (e) => {
  botonEnviar.disabled = !e.target.checked;
});

preventDefault: cancelar el comportamiento por defecto ​

Algunos elementos tienen acciones automáticas: un enlace navega, un formulario recarga la página al enviarse. preventDefault() las cancela.

js
const form = document.querySelector('#contacto');

form.addEventListener('submit', (e) => {
  e.preventDefault();          // evita que la página se recargue
  const datos = new FormData(form);
  console.log(Object.fromEntries(datos));
});

// Enlace que abre un modal en lugar de navegar
document.querySelector('#ver-terminos').addEventListener('click', (e) => {
  e.preventDefault();
  abrirModal();
});

Propagación: bubbling y capturing ​

Cuando haces clic en un botón dentro de una tarjeta dentro de una sección, el clic ocurre en los tres. El evento viaja en tres fases:

  1. Captura: baja desde document hasta el elemento.
  2. Objetivo: llega al elemento donde ocurrió.
  3. Burbujeo (bubbling): sube de regreso hasta document.

Por defecto, los listeners se ejecutan en la fase de burbujeo: primero el del botón, luego el de la tarjeta, luego el de la sección.

html
<section id="seccion">
  <div id="tarjeta">
    <button id="boton">Clic</button>
  </div>
</section>
js
const log = (nombre) => () => console.log(nombre);

document.querySelector('#seccion').addEventListener('click', log('sección'));
document.querySelector('#tarjeta').addEventListener('click', log('tarjeta'));
document.querySelector('#boton').addEventListener('click', log('botón'));

// Al hacer clic en el botón: botón → tarjeta → sección

// Escuchar en fase de captura (se ejecuta ANTES, de afuera hacia adentro)
document.querySelector('#seccion').addEventListener('click', log('sección (captura)'), { capture: true });

Para detener la propagación usa e.stopPropagation(). Úsalo con moderación: romper el burbujeo puede hacer que otros componentes (menús que se cierran al hacer clic fuera, analíticas) dejen de funcionar.

Delegación de eventos ​

Supón una lista de tareas donde cada una tiene un botón "Eliminar". Podrías poner un listener en cada botón... pero:

  • Si tienes 500 tareas, son 500 listeners.
  • Las tareas nuevas que agregues no tendrán listener.

La delegación aprovecha el burbujeo: pones un solo listener en el contenedor y averiguas con event.target dónde se hizo clic.

html
<ul id="tareas">
  <li data-id="1">Diseñar logo <button class="eliminar">✕</button></li>
  <li data-id="2">Maquetar <button class="eliminar">✕</button></li>
</ul>
js
const lista = document.querySelector('#tareas');

lista.addEventListener('click', (e) => {
  // closest sube desde el elemento clicado hasta encontrar el botón
  // (funciona aunque el clic sea en un ícono dentro del botón)
  const btn = e.target.closest('.eliminar');
  if (!btn) return;                    // el clic no fue en un botón eliminar

  const li = btn.closest('li');
  console.log('Eliminar tarea', li.dataset.id);
  li.remove();
});

// Las tareas nuevas funcionan automáticamente, sin listeners extra
lista.insertAdjacentHTML('beforeend',
  '<li data-id="3">Programar <button class="eliminar">✕</button></li>');

Con varias acciones, usa un atributo data-accion:

js
lista.addEventListener('click', (e) => {
  const btn = e.target.closest('[data-accion]');
  if (!btn) return;
  const id = Number(btn.closest('li').dataset.id);

  const acciones = {
    completar: () => completarTarea(id),
    editar: () => editarTarea(id),
    eliminar: () => eliminarTarea(id)
  };
  acciones[btn.dataset.accion]?.();
});
Delegación de eventos

Buena práctica

Usa delegación siempre que tengas listas generadas dinámicamente (resultados de búsqueda, carritos, tablas). Es más eficiente y evita errores de "los elementos nuevos no responden".

Quitar listeners y opciones útiles ​

js
function manejarScroll() { /* ... */ }

window.addEventListener('scroll', manejarScroll, { passive: true });
window.removeEventListener('scroll', manejarScroll); // misma función exacta

// once: se ejecuta una sola vez y se elimina solo
boton.addEventListener('click', () => console.log('Sólo una vez'), { once: true });

// AbortController: quitar varios listeners de golpe
const controlador = new AbortController();
window.addEventListener('resize', alRedimensionar, { signal: controlador.signal });
window.addEventListener('keydown', alPresionar, { signal: controlador.signal });
controlador.abort(); // elimina ambos

Error común

removeEventListener necesita la misma referencia de función. Esto no funciona:

js
btn.addEventListener('click', () => hacerAlgo());
btn.removeEventListener('click', () => hacerAlgo()); // es OTRA función

Guarda la función en una constante o usa AbortController.

Componentes reales con eventos ​

Un modal debe cerrarse con su botón, al hacer clic en el fondo oscuro y con la tecla Escape:

js
const modal = document.querySelector('#modal');
const abrir = document.querySelector('#abrir-modal');

function abrirModal() {
  modal.classList.add('visible');
  modal.querySelector('button').focus();      // accesibilidad: mover el foco
  document.addEventListener('keydown', cerrarConEscape);
}

function cerrarModal() {
  modal.classList.remove('visible');
  abrir.focus();                              // devolver el foco
  document.removeEventListener('keydown', cerrarConEscape);
}

function cerrarConEscape(e) {
  if (e.key === 'Escape') cerrarModal();
}

abrir.addEventListener('click', abrirModal);
modal.addEventListener('click', (e) => {
  // si el clic fue en el fondo (no dentro de la caja), cerrar
  if (e.target === modal || e.target.closest('[data-cerrar]')) cerrarModal();
});
Modal accesible

Alternativa nativa

HTML ya trae la etiqueta <dialog> con showModal(), close() y cierre con Escape incluido. Para proyectos nuevos es una gran opción.

Pestañas (tabs) ​

js
const tabs = document.querySelectorAll('[role="tab"]');
const paneles = document.querySelectorAll('[role="tabpanel"]');

tabs.forEach((tab) => {
  tab.addEventListener('click', () => {
    tabs.forEach((t) => t.setAttribute('aria-selected', 'false'));
    paneles.forEach((p) => (p.hidden = true));

    tab.setAttribute('aria-selected', 'true');
    document.getElementById(tab.getAttribute('aria-controls')).hidden = false;
  });
});
Pestañas accesibles

Optimizar eventos frecuentes: debounce ​

input, scroll y resize se disparan decenas de veces por segundo. Con debounce esperas a que el usuario deje de escribir antes de actuar (por ejemplo, para no llamar a una API en cada tecla):

js
function debounce(fn, espera = 300) {
  let temporizador;
  return (...args) => {
    clearTimeout(temporizador);
    temporizador = setTimeout(() => fn(...args), espera);
  };
}

buscador.addEventListener('input', debounce((e) => {
  buscarEnApi(e.target.value);
}, 400));

Ejercicios ​

  1. Crea un botón que cambie el color de fondo de la página a uno aleatorio en cada clic.
  2. Muestra en un <p> cuántos caracteres lleva un <textarea> mientras escribes, con un máximo de 140; pon el texto en rojo si se pasa.
  3. Con delegación, haz que al hacer clic en cualquier <li> de una lista se marque/desmarque con la clase seleccionado.
  4. Haz que al presionar la tecla / el foco vaya a un input de búsqueda (sin escribir la barra).
  5. Crea un menú desplegable que se cierre al hacer clic fuera de él.
Ver solución
js
// 1
document.querySelector('#color').addEventListener('click', () => {
  const hue = Math.floor(Math.random() * 360);
  document.body.style.backgroundColor = `hsl(${hue} 70% 85%)`;
});

// 2
const area = document.querySelector('#mensaje');
const contador = document.querySelector('#contador');
area.addEventListener('input', () => {
  const n = area.value.length;
  contador.textContent = `${n}/140`;
  contador.style.color = n > 140 ? 'red' : '';
});

// 3
document.querySelector('#lista').addEventListener('click', (e) => {
  const li = e.target.closest('li');
  if (li) li.classList.toggle('seleccionado');
});

// 4
document.addEventListener('keydown', (e) => {
  const escribiendo = ['INPUT', 'TEXTAREA'].includes(document.activeElement.tagName);
  if (e.key === '/' && !escribiendo) {
    e.preventDefault();
    document.querySelector('#buscar').focus();
  }
});

// 5
const menu = document.querySelector('.menu');
const toggle = document.querySelector('.menu-toggle');
toggle.addEventListener('click', () => menu.classList.toggle('abierto'));
document.addEventListener('click', (e) => {
  if (!menu.contains(e.target) && !toggle.contains(e.target)) {
    menu.classList.remove('abierto');
  }
});

Resumen ​

  • addEventListener(tipo, funcion) es la forma estándar de escuchar eventos; pasa la función sin paréntesis.
  • El objeto event trae target, key, coordenadas y más.
  • preventDefault() cancela acciones por defecto (envío de formularios, navegación).
  • Los eventos burbujean del elemento hacia sus ancestros.
  • Delegación: un listener en el contenedor + e.target.closest(...) para listas dinámicas.
  • Usa once, removeEventListener o AbortController para limpiar, y debounce para eventos frecuentes.

Siguiente: Formularios y validación

Academia Web — aprende, copia y construye.