Eventos
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.