Eventos

Cómo escuchar y reaccionar a la interacción del usuario: addEventListener, el objeto event, delegación de eventos y eventos personalizados.

Los eventos son la forma en que el navegador comunica al código JavaScript lo que ocurre: una pulsación de tecla, un clic, un formulario enviado, la página cargada. Sin eventos, el JavaScript sería código que solo se ejecuta una vez al cargar.


addEventListener

const btn = document.querySelector('#btn-enviar');

btn.addEventListener('click', function(event) {
  console.log('clic!', event);
});

// Con arrow function (más habitual en código moderno)
btn.addEventListener('click', (e) => {
  e.preventDefault();  // cancela el comportamiento por defecto
  console.log('enviado');
});

addEventListener acepta tres argumentos: el tipo de evento, la función listener y opcionalmente un objeto de opciones. Siempre es preferible a los atributos HTML (onclick="...") porque permite múltiples listeners y separa la lógica del markup.


El objeto event

El primer argumento que recibe el listener es el objeto event con información sobre lo que ocurrió:

document.querySelector('form').addEventListener('submit', (e) => {
  e.preventDefault()          // cancela el envío del formulario
  e.stopPropagation()         // detiene la propagación hacia arriba

  console.log(e.type)         // 'submit'
  console.log(e.target)       // el elemento que disparó el evento
  console.log(e.currentTarget) // el elemento donde está el listener
  console.log(e.timeStamp)    // cuándo ocurrió (en ms desde la carga)
});

// En eventos de mouse
document.addEventListener('mousemove', (e) => {
  console.log(e.clientX, e.clientY)  // coordenadas relativas al viewport
  console.log(e.pageX, e.pageY)      // coordenadas relativas al documento
  console.log(e.buttons)             // qué botón está pulsado
});

// En eventos de teclado
document.addEventListener('keydown', (e) => {
  console.log(e.key)         // 'Enter', 'a', 'ArrowLeft'...
  console.log(e.code)        // 'KeyA', 'Enter' — independiente del idioma
  console.log(e.ctrlKey)     // boolean — si Ctrl está pulsado
  console.log(e.shiftKey)    // boolean
  console.log(e.metaKey)     // boolean — Cmd en Mac
});

Tipos de eventos más usados

// Mouse
'click'         // clic del ratón
'dblclick'      // doble clic
'mouseenter'    // cursor entra (no burbujea)
'mouseleave'    // cursor sale (no burbujea)
'mouseover'     // cursor entra (burbujea)
'mouseout'      // cursor sale (burbujea)
'mousemove'     // cursor se mueve
'contextmenu'   // clic derecho

// Teclado
'keydown'       // tecla pulsada (se repite si se mantiene)
'keyup'         // tecla soltada
'keypress'      // deprecado — usa keydown

// Formularios
'submit'        // formulario enviado
'input'         // valor cambia en tiempo real (input, textarea)
'change'        // valor cambia y el campo pierde el foco
'focus'         // el campo recibe el foco
'blur'          // el campo pierde el foco

// Documento y ventana
'DOMContentLoaded'  // el HTML está cargado y parseado (en document)
'load'              // todo cargado incluyendo imágenes (en window)
'resize'            // ventana redimensionada
'scroll'            // se hace scroll

Propagación (bubbling)

Por defecto, los eventos se propagan hacia arriba por el árbol DOM desde el elemento origen hasta document. Esto se llama bubbling:

<div id="contenedor">
  <button id="btn">Clic</button>
</div>
document.querySelector('#btn').addEventListener('click', () => {
  console.log('1 - botón');
});
document.querySelector('#contenedor').addEventListener('click', () => {
  console.log('2 - contenedor');  // también se ejecuta al hacer clic en el btn
});
document.addEventListener('click', () => {
  console.log('3 - document');
});

// Resultado al clicar el botón:
// 1 - botón
// 2 - contenedor
// 3 - document

e.stopPropagation() detiene la propagación. e.stopImmediatePropagation() además evita que se ejecuten otros listeners del mismo elemento.


Delegación de eventos

En lugar de añadir un listener a cada elemento de una lista, añades uno solo al padre y usas e.target para saber en cuál se hizo clic. Más eficiente y funciona con elementos añadidos dinámicamente:

// ❌ Un listener por elemento — no escala, no funciona con elementos dinámicos
document.querySelectorAll('.btn-eliminar').forEach(btn => {
  btn.addEventListener('click', eliminar);
});

// ✅ Delegación — un listener en el padre
document.querySelector('#lista').addEventListener('click', (e) => {
  // e.target es el elemento exacto donde se hizo clic
  if (e.target.matches('.btn-eliminar')) {
    e.target.closest('li').remove();
  }

  if (e.target.matches('.btn-editar')) {
    const id = e.target.dataset.id;
    abrirEditor(id);
  }
});

closest() busca el ancestro más cercano que coincida con el selector (incluido el propio elemento):

e.target.closest('li')       // el li que contiene el botón
e.target.closest('[data-id]') // el ancestro con data-id

Eliminar listeners

function handleClick(e) {
  console.log('clic');
}

const btn = document.querySelector('#btn');
btn.addEventListener('click', handleClick);

// Para eliminar, necesitas la misma referencia a la función
btn.removeEventListener('click', handleClick);

// Con arrow functions no puedes eliminarlas — no hay referencia guardada
// Por eso las funciones nombradas son necesarias cuando necesitas removeEventListener

La opción once: true

Si solo necesitas que el listener se ejecute una vez:

btn.addEventListener('click', (e) => {
  iniciarAnimacion();
}, { once: true });  // se elimina automáticamente después del primer disparo

Otras opciones útiles:

btn.addEventListener('click', handler, {
  once: true,     // ejecutar solo una vez
  passive: true,  // el listener nunca llamará preventDefault — mejora rendimiento en scroll
  capture: true,  // escucha en la fase de captura (antes del bubbling)
});

Eventos personalizados

Puedes crear y disparar tus propios eventos para comunicar partes de la UI entre sí:

// Crear y disparar
const evento = new CustomEvent('producto:añadido', {
  bubbles: true,        // se propaga hacia arriba
  detail: { id: 42, nombre: 'Camiseta' }  // datos del evento
});
document.querySelector('.carrito').dispatchEvent(evento);

// Escuchar
document.addEventListener('producto:añadido', (e) => {
  console.log(e.detail.nombre);  // 'Camiseta'
  actualizarContador();
});

En la siguiente lección entramos en uno de los conceptos más importantes de JavaScript moderno: la asincronía, y cómo manejarla con promesas y async/await.