JS 13: Eventos Avanzados

Domina la interacción y el flujo de la información.

1. ¿Qué son los Eventos? Acción-Reacción

Una página web estática es aburrida. Los eventos permiten que la página reaccione cuando ocurre algo: el usuario hace click, pulsa una tecla, o el navegador termina de cargar una imagen.

Analogía: El Timbre de la Puerta

  • El Evento: Alguien pulsa el timbre (Click).
  • El Listener (Escuchador): El sistema eléctrico que espera a que se pulse el botón.
  • La Función (Handler): El sonido "Ding-Dong" que suena como respuesta.

2. Formas de Asignar Eventos: Evolución

Existen tres formas de "escuchar" un evento. De la más antigua (y peor) a la moderna.

A. Atributos HTML (Obsoleta)

Se escribe directamente en el HTML. No la uses, ensucia el código y dificulta el mantenimiento.

<button onclick="alert('Hola')">Click</button>

B. Propiedades JS (Limitada)

Se asigna desde JavaScript como una propiedad. Solo permite una función por evento. Si asignas una segunda, borras la primera.

btn.onclick = function() { console.log("Hola"); };

C. addEventListener (La Profesional)

Es la forma estándar. Permite múltiples funciones para un mismo evento y ofrece opciones avanzadas.

btn.addEventListener("click", () => console.log("¡Click 1!"));
btn.addEventListener("click", () => console.log("¡Click 2!")); // Ambos funcionan

Formas de Referenciar la Función

1. Función Anónima o Arrow

Rápida pero imposible de borrar individualmente después.

btn.addEventListener("click", (e) => {
    console.log("Hola");
});

2. Función Externa (Nombrada)

Limpio y permite usar removeEventListener.

function saludar() { console.log("Hola"); }

btn.addEventListener("click", saludar);
// Luego puedes borrarlo:
btn.removeEventListener("click", saludar);

Pasar Argumentos a la Función

Por defecto, el listener solo recibe el objeto e. Si necesitas pasar más datos, tienes dos trucos:

// Opción A: Función "Envoltorio" (Wrapper)
btn.addEventListener("click", (e) => {
    miFuncionPersonalizada(e, "mi_id_123");
});
// Opción B: Usar .bind() (Crea una nueva función con datos ligados)
btn.addEventListener("click", miFuncion.bind(null, "dato1", "dato2"));

⚠️ Regla de Oro: ¿Cuándo usar "on"?

Esta es la duda más común. Graba esto en tu mente:

Lugar / Método Prefijo "on" Ejemplo
Atributo HTML ✅ Sí <button onclick="...">
Propiedad JS ✅ Sí elemento.onclick = function...
addEventListener ❌ No elemento.addEventListener("click", ...)

¿Por qué? Porque addEventListener es inteligente y ya sabe que estás buscando un "escuchador de eventos", así que solo necesita el nombre del evento puro.

Ejemplos comparativos:

// 1. HTML (Con "on")
<button onclick="hacerAlgo()">Boton HTML</button>

// 2. Propiedad JS (Con "on")
btn.onclick = function() { ... };

// 3. addEventListener (Sin "on")
btn.addEventListener("click", () => { ... });

// ❌ ERROR COMÚN (Nunca hagas esto):
btn.addEventListener("onclick", () => { ... }); // ¡No funcionará!

3. El Objeto Evento (e): El Informe Policial

Cuando ocurre un evento, JavaScript crea un objeto e (el "informe") con todos los detalles. Aquí es donde surge la duda más importante: ¿Quién disparó el evento?

Analogy: El Dardo y la Diana

Imagina una Diana (tablero) con un "Bullseye" (centro) rojo en medio. Pones un listener de "golpe" en toda la Diana.

  • e.currentTarget (La Diana): Es el elemento que tiene el "oído" puesto (el que tiene el addEventListener).
  • e.target (El centro rojo): Es el punto exacto donde impactó el dardo.

Si disparas al centro, el dardo toca el centro (target) y el tablero vibra (currentTarget).

e.target

El elemento original que recibió la acción. Puede ser un hijo muy profundo (un icono dentro de un botón).

e.currentTarget

El elemento que está escuchando. Siempre es el elemento al que le pusiste el código.

btn.addEventListener("click", function(e) {
    console.log("Diste a:", e.target);        // Puede ser el <span> o <i> de dentro
    console.log("Escuchado por:", e.currentTarget); // Siempre será el <button>
});

La Pregunta del Millón: ¿DÓNDE hice click? (Coordenadas)

Una de las preguntas más comunes es: "¿En qué píxel ocurrió el evento?". El objeto evento tiene varias propiedades de coordenadas (`X` e `Y`), y elegir la correcta es crucial.

1. clientX / clientY (Ventana)

Son las coordenadas respecto a la ventana visible del navegador (viewport). Si haces scroll, el punto (0,0) sigue siendo la esquina superior izquierda de lo que ves.

Uso típico: Tooltips fijos (position: fixed) o elementos que siguen al ratón tipo "custom cursor".

// Mover un elemento 'fijo' con el ratón
window.addEventListener("mousemove", (e) => {
    tooltip.style.left = e.clientX + "px";
    tooltip.style.top = e.clientY + "px";
});

2. pageX / pageY (Documento)

Son relativas al documento completo. Si has hecho scroll hacia abajo 500px, pageY será 500px mayor que clientY. Es la más usada para posicionar elementos absolutos en la página.

Uso típico: Arrastrar y soltar (Drag & Drop), menús contextuales personalizados que aparecen donde haces click.

// Posicionar un menú justo donde hiciste click
document.addEventListener("click", (e) => {
    menu.style.position = "absolute";
    menu.style.left = e.pageX + "px";
    menu.style.top = e.pageY + "px";
});

3. screenX / screenY (Monitor)

Son relativas a la pantalla física de tu monitor. Raramente se usan en diseño web, salvo para juegos o popups.

Uso típico: Juegos de múltiples ventanas, analítica de usuario (mapas de calor), o popups en segunda pantalla.

// Abrir un popup en la esquina de la pantalla
document.addEventListener("click", (e) => {
    console.log(`Click físico en: ${e.screenX}, ${e.screenY}`);
    window.open(url, "_blank", `left=${e.screenX},top=${e.screenY}`);
});

4. offsetX / offsetY (Elemento)

Son relativas al borde superior-izquierdo del elemento donde ocurrió el evento. Muy útil para saber dónde hiciste click dentro de un botón o imagen.

Uso típico: Canvas de dibujo (paint), calcular dónde hiciste click en una barra de progreso de video.

// Dibujar en un canvas donde pulsas
canvas.addEventListener("mousedown", (e) => {
    ctx.moveTo(e.offsetX, e.offsetY);
    ctx.lineTo(e.offsetX + 1, e.offsetY + 1);
    ctx.stroke();
});

📍 Laboratorio de Coordenadas

Mueve el ratón por esta caja para ver los valores en tiempo real:

  • client: 0, 0
  • page: 0, 0
  • offset: 0, 0

Otras Propiedades Útiles

4. Catálogo de Eventos Imprescindibles

🖱️ Eventos de Ratón (Mouse Events)

Permiten interactuar con el puntero y los botones del ratón.

  • click: Se activa al presionar y soltar el botón principal.
  • dblclick: Pulsar dos veces seguidas rápidamente.
  • mousedown / mouseup: Se activan al presionar o soltar cualquier botón, respectivamente.
  • mouseenter / mouseleave: El puntero entra o sale de los límites de un elemento (no burbujean).
  • mouseover / mouseout: Similares a los anteriores, pero se activan también al entrar en elementos hijos.
  • mousemove: Se dispara continuamente mientras el ratón se mueve sobre un elemento.
  • contextmenu: Se activa al intentar abrir el menú contextual (clic derecho).

⌨️ Eventos de Teclado (Keyboard Events)

Detectan la interacción física con las teclas.

  • keydown: Se activa en el momento en que se presiona una tecla.
  • keyup: Se activa cuando se suelta la tecla.
  • keypress: (En desuso) Se activaba al presionar teclas que producen caracteres. Se recomienda usar keydown o el evento input.

📂 Otros Eventos Comunes

Esta lista incluye eventos de formularios, ventana y recursos.

De Formulario

  • submit: Se activa al enviar un formulario.
  • change: Cuando el valor de un elemento cambia y pierde el foco (en select u input).
  • input: Se dispara inmediatamente cada vez que el valor de un campo cambia (ideal para validación en tiempo real).
  • focus / blur: Un elemento gana o pierde el foco (clic en un input o tabulación).

⚡ Demo: Espejo en Tiempo Real (Input)

Resultado: ...

De Ventana y Carga

  • load: El navegador ha terminado de cargar toda la página (incluyendo imágenes y estilos).
  • DOMContentLoaded: El HTML ha sido completamente cargado y procesado, sin esperar a imágenes.
  • resize: Cuando se cambia el tamaño de la ventana del navegador.
  • scroll: Cuando el usuario se desplaza por el documento.

5. Control de Flujo: Propagación

Los eventos no se quedan quietos; viajan por el DOM. Por defecto usan el Bubbling (Burbujeo): el evento sube desde el hijo hasta el padre.

Stop Propagation: "¡Aquí se acaba todo!"

A veces quieres que un click en un botón dentro de un div NO dispare también el evento del div padre.

btn.addEventListener("click", (e) => {
    e.stopPropagation(); // Detiene la subida de la burbuja
    console.log("Acción del botón");
});

PreventDefault: "¡Anula el instinto!"

Este método es fundamental. Le dice al navegador: "Sé lo que ibas a hacer por defecto, pero NO lo hagas". Muchos elementos HTML tienen comportamientos automáticos (enlaces que navegan, formularios que se envían) que a veces queremos detener para controlarlos nosotros con JS.

Analogía: El Interruptor de Seguridad

Imagina que vas a entrar en una habitación oscura y estiras la mano para darle al interruptor (la acción natural). preventDefault() es como si alguien pusiera su mano sobre el interruptor antes de que lo pulses: tu intención estaba ahí, pero la luz no se enciende.

Ejemplos Claros de Uso:

1. En Formularios (El más usado)

Evita que la página se recargue al pulsar "Enviar", permitiéndonos validar los datos o enviarlos por Fetch/AJAX.

form.addEventListener("submit", (e) => {
    e.preventDefault(); 
    console.log("Validando... enviaré los datos sin recargar la página");
});

2. En Enlaces (<a>)

Evita que el navegador salte a otra URL. Útil para pestañas, abrir modales o efectos de scroll suave.

link.addEventListener("click", (e) => {
    e.preventDefault();
    console.log("El enlace no navegará, abriré un popup en su lugar");
});

3. Menú Contextual (Click derecho)

Puedes evitar que aparezca el menú típico del navegador para poner el tuyo propio o proteger contenido.

window.addEventListener("contextmenu", (e) => {
    e.preventDefault();
    alert("Menú contextual desactivado en esta zona");
});

4. Restricción de Teclado

Evita que el usuario escriba ciertos caracteres en un campo de texto.

input.addEventListener("keydown", (e) => {
    if (e.key === "e" || e.key === "E") {
        e.preventDefault(); // Impedimos escribir la letra 'e'
    }
});

6. Delegación de Eventos: El Patrón Pro

Si tienes una lista de 1000 items, no pongas 1000 listeners. Pon UNO solo en el padre y detecta quién disparó el evento usando e.target.

document.querySelector("ul").addEventListener("click", (e) => {
    // ⚠️ NOTA: tagName siempre devuelve MAYÚSCULAS en HTML ("LI", "DIV", etc.)
    if (e.target.tagName === "LI") {
        console.log("Has pulsado el item:", e.target.textContent);
    }
});

> [!TIP] > Una forma más moderna y flexible es usar e.target.matches("selector"), que permite usar cualquier selector CSS: if (e.target.matches("li.activo")) { ... }.

7. Opciones de addEventListener

elemento.addEventListener("scroll", handler, {
    once: true,    // Se ejecuta una sola vez y se borra solo
    passive: true, // Mejora el rendimiento en eventos de scroll/touch
    capture: true  // Captura en fase de bajada, no de subida
});

8. Teclado Avanzado: key vs code

Detectar teclas parece fácil, pero hay una diferencia vital para teclados internacionales.

document.addEventListener("keydown", (e) => {
    // Detectar teclas especiales (Modificadores)
    if (e.ctrlKey && e.key === "s") {
        e.preventDefault();
        console.log("Guardado rápido activado");
    }
});

9. Eventos Personalizados (Custom Events)

Puedes crear tus propios eventos con nombres que tengan sentido para tu aplicación (ej: `login`, `compraFinalizada`).

// 1. Crear el evento con datos extras (detail)
const miEvento = new CustomEvent("usuarioRegistrado", {
    detail: { nombre: "Javi", id: 45 }
});

// 2. Escucharlo como cualquier otro
document.addEventListener("usuarioRegistrado", (e) => {
    console.log("Bienvenido:", e.detail.nombre);
});

// 3. Lanzarlo cuando quieras
document.dispatchEvent(miEvento);

10. Rendimiento: Debounce y Throttle

Eventos como scroll o mousemove se disparan cientos de veces por segundo. Esto puede colgar el navegador.

Debounce (El que espera)

Espera a que el usuario "pare" de hacer la acción antes de ejecutar (ej: buscar en un buscador mientras escribes).

Throttle (El que limita)

Ejecuta la acción máximo una vez cada X milisegundos (ej: actualizar posición en un scroll).

// Idea básica de un Debounce:
let timer;
window.addEventListener("scroll", () => {
    clearTimeout(timer);
    timer = setTimeout(() => {
        console.log("Acción pesada ejecutada tras el scroll");
    }, 200);
});
Volver a JS