Eventos
Hasta ahora tu JavaScript sabe hacer cosas, pero no sabe cuándo hacerlas. Un botón de "Añadir al carrito" que no hace nada al pulsarlo es tan útil como un timbre desconectado. Los eventos son el puente entre lo que el usuario hace (hacer clic, escribir, enviar un formulario) y lo que tu código ejecuta. Sin eventos, tu página es un póster; con eventos, es una aplicación.
addEventListener: la forma moderna
Quizá hayas visto código con onclick="..." directamente en el HTML. Olvídalo. Eso mezcla estructura y comportamiento, y limita lo que puedes hacer. La forma moderna es addEventListener:
<!-- ❌ La forma antigua — NO hagas esto -->
<button onclick="alert('Hola')">Saludar</button>
<!-- ✅ La forma moderna — HTML limpio -->
<button id="btn-saludar">Saludar</button>
// Seleccionar el elemento
const btnSaludar = document.querySelector('#btn-saludar');
// Escuchar el evento "click"
btnSaludar.addEventListener('click', () => {
alert('¡Hola! Bienvenido a la tienda.');
});
// ¿Por qué addEventListener y no onclick?
// 1. Puedes añadir VARIOS listeners al mismo elemento
// 2. Puedes eliminar listeners cuando ya no los necesitas
// 3. Tu HTML queda limpio, sin JavaScript mezclado
La sintaxis siempre es la misma: elemento.addEventListener(tipoDeEvento, función). El tipo de evento es un string (\'click\', \'submit\', \'input\'...) y la función es lo que se ejecuta cuando ocurre.
El objeto Event: quién, qué, dónde
Cuando un evento se dispara, el navegador crea un objeto Event con información sobre lo que acaba de pasar. Tu función listener lo recibe automáticamente como primer argumento:
const boton = document.querySelector('#mi-boton');
boton.addEventListener('click', (event) => {
console.log(event.type); // "click"
console.log(event.target); // El elemento que disparó el evento
console.log(event.currentTarget); // El elemento al que le pusiste el listener
console.log(event.clientX); // Posición X del ratón al hacer clic
console.log(event.clientY); // Posición Y del ratón al hacer clic
});
// event.target vs event.currentTarget:
// - target: el elemento exacto donde hizo clic el usuario
// - currentTarget: el elemento que tiene el addEventListener
// A veces son el mismo, pero con delegación de eventos son diferentes
Eventos de clic en botones
El evento más común. Imagina un contador de "me gusta" como el de cualquier red social:
<div class="like-section">
<button id="btn-like" class="btn-like">
❤️ <span id="like-count">0</span> Me gusta
</button>
</div>
const btnLike = document.querySelector('#btn-like');
const likeCount = document.querySelector('#like-count');
let likes = 0;
btnLike.addEventListener('click', () => {
likes += 1;
likeCount.textContent = likes;
// Pequeña animación visual
btnLike.classList.add('liked');
setTimeout(() => btnLike.classList.remove('liked'), 300);
});
Eventos de formulario: submit y preventDefault
Cuando un formulario se envía, el navegador por defecto recarga la página. En aplicaciones modernas, quieres manejar los datos con JavaScript sin recargar nada. Para eso existe preventDefault():
<form id="form-contacto">
<input type="text" name="nombre" placeholder="Tu nombre" required>
<input type="email" name="email" placeholder="Tu email" required>
<textarea name="mensaje" placeholder="Tu mensaje" required></textarea>
<button type="submit">Enviar mensaje</button>
</form>
<div id="form-resultado" hidden></div>
const formulario = document.querySelector('#form-contacto');
const resultado = document.querySelector('#form-resultado');
formulario.addEventListener('submit', (event) => {
// ¡Esto es clave! Evita que el navegador recargue la página
event.preventDefault();
// Recoger los datos del formulario
const datos = new FormData(formulario);
const nombre = datos.get('nombre');
const email = datos.get('email');
const mensaje = datos.get('mensaje');
// Hacer algo con los datos (aquí los mostramos, luego los enviarías a un servidor)
resultado.hidden = false;
resultado.innerHTML = `
<p>Gracias, <strong>${nombre}</strong>. Hemos recibido tu mensaje.</p>
<p>Te responderemos en <strong>${email}</strong>.</p>
`;
// Limpiar el formulario
formulario.reset();
});
new FormData(formulario)es la forma más limpia de extraer todos los valores de un formulario. Funciona con cualquier tipo de campo: text, email, checkbox, select, file...
Eventos de input y change: feedback en tiempo real
El evento input se dispara cada vez que el usuario escribe un carácter. El evento change se dispara cuando el usuario termina de editar (sale del campo). La diferencia importa:
<div class="campo-password">
<label for="password">Contraseña</label>
<input type="password" id="password" placeholder="Mínimo 8 caracteres">
<div id="password-feedback" class="feedback"></div>
</div>
<div class="campo-username">
<label for="username">Nombre de usuario</label>
<input type="text" id="username" placeholder="solo letras y números" maxlength="20">
<span id="username-counter">0/20</span>
</div>
// Validación de contraseña en tiempo real con "input"
const passwordInput = document.querySelector('#password');
const passwordFeedback = document.querySelector('#password-feedback');
passwordInput.addEventListener('input', (event) => {
const valor = event.target.value;
if (valor.length === 0) {
passwordFeedback.textContent = '';
passwordFeedback.className = 'feedback';
} else if (valor.length < 8) {
passwordFeedback.textContent = 'Demasiado corta';
passwordFeedback.className = 'feedback feedback--error';
} else if (!/[A-Z]/.test(valor) || !/[0-9]/.test(valor)) {
passwordFeedback.textContent = 'Incluye mayúsculas y números';
passwordFeedback.className = 'feedback feedback--warning';
} else {
passwordFeedback.textContent = 'Contraseña fuerte ✓';
passwordFeedback.className = 'feedback feedback--success';
}
});
// Contador de caracteres con "input"
const usernameInput = document.querySelector('#username');
const usernameCounter = document.querySelector('#username-counter');
usernameInput.addEventListener('input', (event) => {
const longitud = event.target.value.length;
usernameCounter.textContent = `${longitud}/20`;
usernameCounter.style.color = longitud >= 18 ? '#ef4444' : '#888';
});
// "change" se dispara al salir del campo — útil para validaciones finales
usernameInput.addEventListener('change', (event) => {
const valor = event.target.value;
if (valor && !/^[a-zA-Z0-9]+$/.test(valor)) {
alert('El nombre de usuario solo puede contener letras y números.');
}
});
Delegación de eventos: el truco de los profesionales
Imagina una lista de tareas donde el usuario puede añadir y eliminar elementos. Si añades un listener a cada botón de eliminar, ¿qué pasa con los botones que aún no existen? No puedes poner un listener a algo que no está en la página. La solución es delegación de eventos: pones el listener en el contenedor padre y compruebas qué elemento se pulsó:
<div class="todo-app">
<h2>Mis Tareas</h2>
<form id="todo-form">
<input type="text" id="todo-input" placeholder="Añadir tarea..." required>
<button type="submit">Añadir</button>
</form>
<ul id="todo-list">
<!-- Las tareas se crean dinámicamente -->
</ul>
</div>
const todoForm = document.querySelector('#todo-form');
const todoInput = document.querySelector('#todo-input');
const todoList = document.querySelector('#todo-list');
// Añadir tareas
todoForm.addEventListener('submit', (event) => {
event.preventDefault();
const texto = todoInput.value.trim();
if (!texto) return;
const li = document.createElement('li');
li.className = 'todo-item';
li.innerHTML = `
<span class="todo-text">${texto}</span>
<button class="btn-done" data-action="done">✓</button>
<button class="btn-delete" data-action="delete">✕</button>
`;
todoList.appendChild(li);
todoInput.value = '';
todoInput.focus();
});
// Delegación de eventos: UN solo listener para toda la lista
todoList.addEventListener('click', (event) => {
const boton = event.target;
const accion = boton.dataset.action;
// Si no pulsó un botón con data-action, ignorar
if (!accion) return;
const tarea = boton.closest('.todo-item');
if (accion === 'done') {
tarea.classList.toggle('completada');
}
if (accion === 'delete') {
tarea.remove();
}
});
// ¿Por qué funciona esto?
// 1. El click ocurre en el botón (event.target)
// 2. El evento "burbujea" hacia arriba: botón → li → ul
// 3. El listener está en ul, así que lo captura
// 4. Comprobamos qué botón se pulsó con dataset.action
// 5. Funciona para tareas que ya existen Y las que se creen después
La delegación de eventos se basa en el burbujeo (event bubbling): cuando haces clic en un botón, el evento también se dispara en su padre, en el padre de su padre, y así sucesivamente hasta llegar al document. Por eso puedes escuchar en el contenedor padre.
Cuándo usar delegación
- Listas dinámicas: elementos que se añaden o eliminan con JavaScript.
- Muchos elementos similares: en vez de 100 listeners, solo 1 en el padre.
- Tablas con acciones: botones de editar/eliminar en cada fila.
Eventos de teclado: keydown y keyup
Los eventos de teclado son útiles para atajos, juegos, o mejorar la experiencia de formularios:
// Añadir tarea al pulsar Enter (sin necesidad de hacer clic en el botón)
todoInput.addEventListener('keydown', (event) => {
if (event.key === 'Enter') {
// El formulario ya maneja esto, pero si no tuvieras formulario:
console.log('Enter pulsado');
}
});
// Atajo de teclado global: Ctrl+K para abrir una búsqueda
document.addEventListener('keydown', (event) => {
if (event.ctrlKey && event.key === 'k') {
event.preventDefault(); // Evita el comportamiento por defecto del navegador
document.querySelector('#barra-busqueda')?.focus();
}
});
// Propiedades útiles del evento de teclado:
// event.key → "Enter", "Escape", "a", "ArrowUp", etc.
// event.code → "KeyA", "Enter" (identifica la tecla física)
// event.ctrlKey → true si Ctrl estaba pulsado
// event.shiftKey → true si Shift estaba pulsado
// event.altKey → true si Alt estaba pulsado
Eliminar event listeners
A veces necesitas dejar de escuchar un evento. Para eso, la función que pasaste a addEventListener tiene que estar guardada en una variable (no puede ser anónima):
// ❌ No puedes eliminar un listener anónimo
boton.addEventListener('click', () => {
console.log('Clic');
});
// boton.removeEventListener('click', ???) — no tienes referencia a la función
// ✅ Guarda la función en una variable
const manejarClic = () => {
console.log('Clic');
};
boton.addEventListener('click', manejarClic);
// Ahora sí puedes eliminarla
boton.removeEventListener('click', manejarClic);
// Caso práctico: un botón que solo funciona una vez
const btnOferta = document.querySelector('#btn-oferta');
const reclamarOferta = () => {
alert('¡Oferta reclamada! 🎉');
btnOferta.textContent = 'Oferta reclamada';
btnOferta.disabled = true;
// Ya no necesitamos escuchar más clics
btnOferta.removeEventListener('click', reclamarOferta);
};
btnOferta.addEventListener('click', reclamarOferta);
// Alternativa moderna: { once: true }
btnOferta.addEventListener('click', () => {
alert('¡Oferta reclamada! 🎉');
}, { once: true }); // Se elimina automáticamente después del primer clic
Ejemplo completo: formulario con validación en tiempo real
Vamos a juntar todo lo aprendido en un formulario de registro que valida mientras el usuario escribe:
<form id="form-registro" novalidate>
<div class="campo">
<label for="reg-nombre">Nombre</label>
<input type="text" id="reg-nombre" name="nombre" required>
<span class="campo-error" id="error-nombre"></span>
</div>
<div class="campo">
<label for="reg-email">Email</label>
<input type="email" id="reg-email" name="email" required>
<span class="campo-error" id="error-email"></span>
</div>
<div class="campo">
<label for="reg-password">Contraseña</label>
<input type="password" id="reg-password" name="password" required>
<span class="campo-error" id="error-password"></span>
</div>
<button type="submit" id="btn-registro" disabled>Crear cuenta</button>
</form>
const formRegistro = document.querySelector('#form-registro');
const btnRegistro = document.querySelector('#btn-registro');
// Reglas de validación para cada campo
const validaciones = {
nombre: {
validar: (valor) => valor.trim().length >= 2,
mensaje: 'El nombre debe tener al menos 2 caracteres',
},
email: {
validar: (valor) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(valor),
mensaje: 'Introduce un email válido',
},
password: {
validar: (valor) => valor.length >= 8 && /[A-Z]/.test(valor) && /[0-9]/.test(valor),
mensaje: 'Mínimo 8 caracteres, una mayúscula y un número',
},
};
// Estado de validación
const estadoCampos = { nombre: false, email: false, password: false };
// Validar un campo individual
const validarCampo = (nombre, valor) => {
const regla = validaciones[nombre];
const errorSpan = document.querySelector(`#error-${nombre}`);
const input = document.querySelector(`[name="${nombre}"]`);
if (!valor) {
errorSpan.textContent = '';
input.classList.remove('valido', 'invalido');
estadoCampos[nombre] = false;
} else if (regla.validar(valor)) {
errorSpan.textContent = '';
input.classList.remove('invalido');
input.classList.add('valido');
estadoCampos[nombre] = true;
} else {
errorSpan.textContent = regla.mensaje;
input.classList.remove('valido');
input.classList.add('invalido');
estadoCampos[nombre] = false;
}
// Activar/desactivar botón según si todos los campos son válidos
const todosValidos = Object.values(estadoCampos).every(Boolean);
btnRegistro.disabled = !todosValidos;
};
// Delegación de eventos en el formulario para todos los inputs
formRegistro.addEventListener('input', (event) => {
const { name, value } = event.target;
if (validaciones[name]) {
validarCampo(name, value);
}
});
// Manejar envío
formRegistro.addEventListener('submit', (event) => {
event.preventDefault();
const datos = Object.fromEntries(new FormData(formRegistro));
console.log('Datos de registro:', datos);
// Aquí enviarías los datos al servidor con fetch (lo veremos en la siguiente lección)
alert(`¡Cuenta creada para ${datos.nombre}!`);
});
Resumen de eventos más usados
click: el usuario hace clic en un elemento.submit: un formulario se envía. UsapreventDefault()para manejarlo con JS.input: el valor de un campo cambia (se dispara con cada carácter).change: el valor de un campo cambia y el usuario sale del campo.keydown/keyup: el usuario pulsa o suelta una tecla.focus/blur: un campo recibe o pierde el foco.scroll: la página o un elemento se desplaza.DOMContentLoaded: el HTML se ha cargado completamente (en eldocument).
Construye una lista de tareas interactiva
Crea una aplicación de lista de tareas completa con estos requisitos:
- Un formulario con un campo de texto para añadir tareas (usa el evento
submitconpreventDefault) - Cada tarea tiene un botón para marcarla como completada (tachada) y otro para eliminarla
- Usa delegación de eventos: un solo listener en el
ulcontenedor, no uno por cada botón - Al pulsar Enter en el campo de texto, se añade la tarea (ya pasa con submit, pero comprueba que funciona)
- Añade un contador que muestre "3 tareas pendientes" y se actualice en tiempo real
- Bonus: añade un botón "Eliminar completadas" que borre solo las tareas marcadas como hechas
lightbulb Pistas
Usa data-action en los botones para identificar la acción en la delegación de eventos. Para el contador, crea una función actualizarContador que cuente los .todo-item que NO tienen la clase completada y llámala después de cada acción. Para "Eliminar completadas", usa querySelectorAll('.completada') y un forEach con .remove().