Volver al curso

JavaScript Desde Cero: Tu Primer Lenguaje de Programación

leccion
14 / 22
beginner
8 horas
Arrays, Objetos y el DOM

Eventos en JavaScript: De Click a Drag & Drop

Lectura
40 min~8 min lectura

Eventos en JavaScript: De Click a Drag & Drop

Objetivos de aprendizaje

Al finalizar esta lección serás capaz de:

  • Utilizar addEventListener para manejar eventos de forma profesional
  • Entender el event object y extraer información útil de cada evento
  • Aplicar event delegation para manejar eventos en listas dinámicas
  • Implementar eventos de teclado, mouse, formulario y scroll
  • Crear interacciones complejas combinando múltiples tipos de eventos

1. El sistema de eventos de JavaScript

Los eventos son acciones que ocurren en la página web: un click, una tecla presionada, el scroll de la página, un formulario enviado, etc. JavaScript te permite "escuchar" estos eventos y ejecutar código cuando ocurren.

addEventListener: la forma correcta

const boton = document.querySelector("#mi-boton");

// Agregar un event listener
boton.addEventListener("click", function(evento) {
  console.log("¡Botón clickeado!");
  console.log("Evento:", evento);
});

// Con arrow function (más conciso)
boton.addEventListener("click", (e) => {
  console.log("Click en posición:", e.clientX, e.clientY);
});

// Función nombrada (permite remover después)
function manejarClick(e) {
  console.log("Click!");
}
boton.addEventListener("click", manejarClick);
boton.removeEventListener("click", manejarClick); // Remover

El objeto Event

Cada evento recibe un objeto con información detallada:

document.querySelector("#formulario").addEventListener("submit", (e) => {
  // Propiedades comunes del evento
  console.log(e.type);         // "submit" (tipo de evento)
  console.log(e.target);       // El elemento que disparó el evento
  console.log(e.currentTarget); // El elemento que tiene el listener
  console.log(e.timeStamp);    // Cuándo ocurrió el evento (ms)
  
  // Métodos importantes
  e.preventDefault();   // Prevenir comportamiento por defecto
  e.stopPropagation(); // Detener la propagación (bubbling)
});

// Eventos de mouse
document.addEventListener("click", (e) => {
  console.log(e.clientX, e.clientY); // Posición relativa al viewport
  console.log(e.pageX, e.pageY);     // Posición relativa al documento
  console.log(e.button);              // 0=izquierdo, 1=medio, 2=derecho
  console.log(e.altKey);              // ¿Alt presionado?
  console.log(e.ctrlKey);             // ¿Ctrl presionado?
  console.log(e.shiftKey);            // ¿Shift presionado?
});

// Eventos de teclado
document.addEventListener("keydown", (e) => {
  console.log(e.key);     // "Enter", "a", "ArrowUp", etc.
  console.log(e.code);    // "Enter", "KeyA", "ArrowUp", etc.
  console.log(e.repeat);  // true si la tecla se mantiene presionada
});

Tipos de eventos más comunes

// MOUSE
elemento.addEventListener("click", fn);       // Click izquierdo
elemento.addEventListener("dblclick", fn);    // Doble click
elemento.addEventListener("contextmenu", fn); // Click derecho
elemento.addEventListener("mouseenter", fn);  // Mouse entra al elemento
elemento.addEventListener("mouseleave", fn);  // Mouse sale del elemento
elemento.addEventListener("mousemove", fn);   // Mouse se mueve sobre el elemento

// TECLADO
document.addEventListener("keydown", fn);     // Tecla presionada
document.addEventListener("keyup", fn);       // Tecla soltada

// FORMULARIO
formulario.addEventListener("submit", fn);    // Formulario enviado
input.addEventListener("input", fn);          // Cada cambio de valor
input.addEventListener("change", fn);         // Cambio confirmado (blur)
input.addEventListener("focus", fn);          // Input obtiene foco
input.addEventListener("blur", fn);           // Input pierde foco

// VENTANA/DOCUMENTO
window.addEventListener("scroll", fn);        // Scroll de la página
window.addEventListener("resize", fn);        // Cambio de tamaño
window.addEventListener("load", fn);          // Todo cargado (imágenes incluidas)
document.addEventListener("DOMContentLoaded", fn); // DOM listo

2. Event Bubbling y Event Delegation

Bubbling: los eventos "burbujean" hacia arriba

Cuando hacés click en un botón dentro de un div dentro del body, el evento se dispara primero en el botón, luego "burbujea" al div, luego al body, luego al document.

// HTML: <div id="padre"><button id="hijo">Click</button></div>

document.querySelector("#padre").addEventListener("click", () => {
  console.log("Click en el padre");
});

document.querySelector("#hijo").addEventListener("click", (e) => {
  console.log("Click en el hijo");
  // e.stopPropagation(); // Descomentar para detener el bubbling
});

// Al hacer click en el botón, se imprime:
// "Click en el hijo"
// "Click en el padre" (bubbling)

Event Delegation: la técnica más importante

En vez de agregar un listener a cada elemento de una lista, agregás UNO al contenedor padre y usás e.target para saber cuál fue clickeado:

// ❌ INEFICIENTE: un listener por cada item
const items = document.querySelectorAll(".item");
items.forEach(item => {
  item.addEventListener("click", () => {
    console.log("Click en:", item.textContent);
  });
});
// Problema: si agregás items dinámicamente, los nuevos no tienen listener

// ✅ EFICIENTE: Event Delegation
const lista = document.querySelector("#lista");
lista.addEventListener("click", (e) => {
  // Verificar que el click fue en un item
  const item = e.target.closest(".item");
  if (!item) return; // Click fuera de un item
  
  console.log("Click en:", item.textContent);
  console.log("ID:", item.dataset.id);
});

// Ahora funciona para items existentes Y futuros
// También ahorrás memoria (1 listener en vez de N)

Ejemplo práctico: lista dinámica con delegation

const contenedor = document.querySelector("#tareas");
const input = document.querySelector("#nueva-tarea");
const btnAgregar = document.querySelector("#btn-agregar");

let tareas = [];
let siguienteId = 1;

function renderizarTareas() {
  contenedor.innerHTML = tareas.map(t => `
    <li class="tarea ${t.completada ? 'completada' : ''}" data-id="${t.id}">
      <input type="checkbox" class="tarea-check" ${t.completada ? 'checked' : ''}>
      <span class="tarea-texto">${t.texto}</span>
      <button class="tarea-eliminar" title="Eliminar">X</button>
    </li>
  `).join("");
}

// Event Delegation en el contenedor
contenedor.addEventListener("click", (e) => {
  const li = e.target.closest(".tarea");
  if (!li) return;
  
  const id = Number(li.dataset.id);
  
  // Click en checkbox → toggle completada
  if (e.target.classList.contains("tarea-check")) {
    const tarea = tareas.find(t => t.id === id);
    if (tarea) tarea.completada = !tarea.completada;
    renderizarTareas();
  }
  
  // Click en botón eliminar
  if (e.target.classList.contains("tarea-eliminar")) {
    tareas = tareas.filter(t => t.id !== id);
    renderizarTareas();
  }
});

btnAgregar.addEventListener("click", () => {
  const texto = input.value.trim();
  if (!texto) return;
  
  tareas.push({ id: siguienteId++, texto, completada: false });
  input.value = "";
  renderizarTareas();
});

// Enter también agrega
input.addEventListener("keydown", (e) => {
  if (e.key === "Enter") btnAgregar.click();
});

3. Eventos avanzados y patrones

Debounce y Throttle

Cuando escuchás eventos que se disparan muchas veces por segundo (scroll, resize, input), necesitás limitar la frecuencia de ejecución:

// Debounce: ejecutar después de que el usuario DEJA de hacer algo
function debounce(fn, delay) {
  let timer;
  return function(...args) {
    clearTimeout(timer);
    timer = setTimeout(() => fn.apply(this, args), delay);
  };
}

// Uso: buscar mientras el usuario escribe (esperar 300ms desde el último teclazo)
const inputBusqueda = document.querySelector("#busqueda");
inputBusqueda.addEventListener("input", debounce((e) => {
  console.log("Buscando:", e.target.value);
  // Acá harías la llamada a la API
}, 300));

// Throttle: ejecutar como máximo una vez cada X ms
function throttle(fn, limit) {
  let ultimaEjecucion = 0;
  return function(...args) {
    const ahora = Date.now();
    if (ahora - ultimaEjecucion >= limit) {
      ultimaEjecucion = ahora;
      fn.apply(this, args);
    }
  };
}

// Uso: rastrear scroll (máximo 1 vez cada 100ms)
window.addEventListener("scroll", throttle(() => {
  const scrollPercent = (window.scrollY / (document.body.scrollHeight - window.innerHeight)) * 100;
  console.log(`Scroll: ${scrollPercent.toFixed(0)}%`);
}, 100));

Formularios completos

const form = document.querySelector("#registro");

form.addEventListener("submit", (e) => {
  e.preventDefault();
  
  // Recoger datos del formulario
  const formData = new FormData(form);
  const datos = Object.fromEntries(formData);
  
  console.log("Datos del formulario:", datos);
  // { nombre: "Ana", email: "[email protected]", password: "123456" }
  
  // Validar
  const errores = validarFormulario(datos);
  if (errores.length > 0) {
    mostrarErrores(errores);
    return;
  }
  
  // Enviar
  enviarRegistro(datos);
});

// Validación en tiempo real
const emailInput = form.querySelector("[name='email']");
emailInput.addEventListener("blur", (e) => {
  const email = e.target.value;
  if (!email.includes("@")) {
    e.target.classList.add("error");
    mostrarMensaje("Email no válido", "error");
  } else {
    e.target.classList.remove("error");
  }
});

Atajos de teclado

const atajos = new Map([
  ["ctrl+s", () => guardarDocumento()],
  ["ctrl+z", () => deshacer()],
  ["ctrl+shift+z", () => rehacer()],
  ["escape", () => cerrarModal()],
]);

document.addEventListener("keydown", (e) => {
  const tecla = [
    e.ctrlKey && "ctrl",
    e.shiftKey && "shift",
    e.altKey && "alt",
    e.key.toLowerCase()
  ].filter(Boolean).join("+");
  
  const accion = atajos.get(tecla);
  if (accion) {
    e.preventDefault();
    accion();
  }
});

Errores comunes de principiantes
  1. No usar e.preventDefault() en formularios: Sin esto, la página se recarga y perdés todo el estado.

  2. Agregar listeners dentro de loops sin delegation: Creás N listeners en vez de 1. Usá event delegation.

  3. No remover listeners cuando ya no se necesitan: Memory leaks. Si un componente se destruye, remové sus listeners.

  4. No usar debounce en eventos frecuentes: Scroll, resize e input pueden dispararse cientos de veces por segundo.

  5. Usar arrow functions con removeEventListener: No funciona porque cada arrow crea una función nueva. Usá funciones nombradas.


Puntos clave de esta lección
  1. addEventListener() es la forma correcta de manejar eventos — nunca uses atributos inline.
  2. El objeto Event contiene toda la información: tipo, target, posición, teclas modificadoras.
  3. e.preventDefault() previene el comportamiento por defecto (recarga en forms, navegación en links).
  4. Los eventos burbujean hacia arriba por el DOM — esto habilita event delegation.
  5. Event Delegation es la técnica más importante: un listener en el padre maneja clicks de todos los hijos.
  6. Debounce espera a que el usuario termine; Throttle limita la frecuencia de ejecución.
  7. FormData + Object.fromEntries() es la forma moderna de recoger datos de formularios.

Quiz de autoevaluación

1. ¿Qué es event bubbling?
a) Los eventos se destruyen
b) Los eventos se propagan hacia arriba por el DOM
c) Los eventos se ejecutan en paralelo
d) Los eventos se cancelan

2. ¿Qué es event delegation?
a) Delegar eventos a otro navegador
b) Poner un listener en el padre para manejar clicks de los hijos
c) Crear múltiples listeners
d) Eliminar eventos

3. ¿Qué hace e.preventDefault()?
a) Elimina el evento
b) Previene el comportamiento por defecto del navegador
c) Detiene el bubbling
d) Previene errores

4. ¿Cuándo usarías debounce?
a) En un click de botón
b) En un evento de input (búsqueda mientras se escribe)
c) En un evento de carga de página
d) Nunca

5. ¿Qué retorna new FormData(form)?
a) Un objeto JSON
b) Un string
c) Un objeto FormData con todos los campos del formulario
d) Un array

Respuestas: 1-b, 2-b, 3-b, 4-b, 5-c


💡 Concepto Clave

Revisemos los puntos más importantes de esta lección antes de continuar.

Ejercicio práctico

Misión: Galería de imágenes interactiva

Creá una galería de imágenes que tenga:

  1. Grid de thumbnails (usá imágenes de https://picsum.photos)
  2. Click en thumbnail abre un modal con la imagen grande
  3. Tecla Escape cierra el modal
  4. Flechas izquierda/derecha navegan entre imágenes
  5. Click fuera de la imagen cierra el modal
  6. Lazy loading: las imágenes cargan cuando entran al viewport
  7. Event delegation para todos los clicks en thumbnails

Este ejercicio combina DOM manipulation, eventos, delegation y keyboard navigation.

🧠 Pon a prueba tu conocimiento
¿Cuál es el aspecto más importante que aprendiste en esta lección?
  • Comprendo el concepto principal y puedo explicarlo con mis palabras
  • Entiendo cómo aplicarlo en mi situación específica
  • Necesito repasar algunas partes antes de continuar
  • Quiero ver más ejemplos prácticos del tema
✅ ¡Excelente! Continúa con la siguiente lección para profundizar más.