JavaScript
Tutorial JavaScript: Eventos del DOM
JavaScript DOM: manipulación y uso. Domina la manipulación y uso del DOM en JavaScript con ejemplos prácticos y detallados.
Aprende JavaScript y certifícateFases de propagación
Cuando interactuamos con elementos en una página web, los eventos del DOM no simplemente ocurren en el elemento que los desencadena, sino que siguen un camino predecible a través del árbol de elementos. Este recorrido se conoce como propagación de eventos y es fundamental para entender cómo JavaScript maneja las interacciones del usuario.
La propagación de eventos en el DOM ocurre en tres fases distintas y secuenciales:
- Fase de captura: El evento desciende desde el elemento
window
hasta el elemento objetivo - Fase de objetivo: El evento alcanza el elemento donde ocurrió originalmente
- Fase de burbujeo: El evento asciende desde el elemento objetivo hasta
window
Visualización del flujo de eventos
Para entender mejor estas fases, imaginemos una estructura HTML simple:
<div id="outer">
<div id="inner">
<button id="button">Click me</button>
</div>
</div>
Cuando hacemos clic en el botón, el evento sigue este recorrido:
- Fase de captura:
window
→document
→<html>
→<body>
→<div id="outer">
→<div id="inner">
→<button>
- Fase de objetivo:
<button>
- Fase de burbujeo:
<button>
→<div id="inner">
→<div id="outer">
→<body>
→<html>
→document
→window
Controlando la fase de escucha
Por defecto, los event listeners se ejecutan durante la fase de burbujeo. Sin embargo, podemos configurarlos para que se activen durante la fase de captura utilizando el tercer parámetro del método addEventListener()
:
element.addEventListener(eventType, handler, useCapture);
Donde useCapture
es un booleano:
false
(valor predeterminado): El listener se ejecuta en la fase de burbujeotrue
: El listener se ejecuta en la fase de captura
Ejemplo práctico de las tres fases
Veamos un ejemplo que demuestra las tres fases de propagación:
const outer = document.getElementById('outer');
const inner = document.getElementById('inner');
const button = document.getElementById('button');
// Fase de captura (tercer parámetro: true)
outer.addEventListener('click', () => {
console.log('Outer - Fase de captura');
}, true);
inner.addEventListener('click', () => {
console.log('Inner - Fase de captura');
}, true);
// Fase de objetivo y burbujeo (tercer parámetro: false)
button.addEventListener('click', () => {
console.log('Button - Fase de objetivo');
});
inner.addEventListener('click', () => {
console.log('Inner - Fase de burbujeo');
});
outer.addEventListener('click', () => {
console.log('Outer - Fase de burbujeo');
});
Al hacer clic en el botón, la consola mostrará:
Outer - Fase de captura
Inner - Fase de captura
Button - Fase de objetivo
Inner - Fase de burbujeo
Outer - Fase de burbujeo
Deteniendo la propagación
En ocasiones, necesitamos detener la propagación de un evento para evitar que se ejecuten listeners en elementos padres. Para esto tenemos dos métodos principales:
event.stopPropagation()
: Detiene la propagación del evento, pero permite que otros listeners del mismo elemento se ejecutenevent.stopImmediatePropagation()
: Detiene la propagación y evita que se ejecuten otros listeners del mismo elemento
button.addEventListener('click', (event) => {
console.log('Button clicked');
event.stopPropagation();
// Los listeners de los elementos padres no se ejecutarán
});
Fase de captura en la práctica
La fase de captura es menos utilizada que la de burbujeo, pero puede ser útil en escenarios específicos:
// Interceptar eventos antes de que lleguen al objetivo
document.addEventListener('click', (event) => {
// Verificar si el usuario está autorizado
if (!userIsAuthorized && event.target.classList.contains('restricted')) {
event.stopPropagation();
showLoginPrompt();
}
}, true); // Usar fase de captura
Event.currentTarget vs Event.target
Al trabajar con propagación de eventos, es importante distinguir entre:
event.target
: El elemento donde originalmente ocurrió el eventoevent.currentTarget
: El elemento cuyo listener está ejecutándose actualmente
outer.addEventListener('click', (event) => {
console.log('Target:', event.target.id); // 'button' (si se hizo clic en el botón)
console.log('Current Target:', event.currentTarget.id); // 'outer'
});
Configuración moderna de addEventListener
En versiones modernas de JavaScript, el tercer parámetro de addEventListener
puede ser un objeto con opciones avanzadas:
element.addEventListener('click', handler, {
capture: false, // Equivalente a useCapture
once: true, // El listener se ejecuta solo una vez
passive: true // Mejora el rendimiento en eventos táctiles
});
Entender las fases de propagación de eventos es esencial para crear interfaces interactivas eficientes y para implementar patrones como la delegación de eventos, que veremos en la siguiente sección.
Delegación de eventos: Implementación eficiente para elementos dinámicos
La delegación de eventos es un patrón de diseño en JavaScript que aprovecha la fase de burbujeo del DOM para manejar eventos de manera más eficiente, especialmente cuando trabajamos con elementos que se crean o eliminan dinámicamente. En lugar de asignar event listeners a cada elemento individual, este patrón consiste en asignar un único listener a un elemento ancestro que procesará los eventos de todos sus descendientes.
Problema que resuelve la delegación
Imaginemos una lista de tareas donde constantemente añadimos o eliminamos elementos:
<ul id="taskList">
<li class="task">Tarea 1 <button class="delete">×</button></li>
<li class="task">Tarea 2 <button class="delete">×</button></li>
<!-- Más tareas se añadirán dinámicamente -->
</ul>
Sin delegación, tendríamos que hacer algo así:
// Enfoque problemático: añadir listeners a cada botón
document.querySelectorAll('.delete').forEach(button => {
button.addEventListener('click', handleDelete);
});
// Problema: los nuevos botones añadidos no tendrán el listener
Este enfoque presenta dos problemas principales:
- Cada elemento necesita su propio listener, consumiendo memoria
- Los elementos creados dinámicamente después de asignar los listeners no responderán a eventos
Implementación básica de delegación
La solución es utilizar la delegación de eventos:
// Un único listener en el contenedor padre
const taskList = document.getElementById('taskList');
taskList.addEventListener('click', (event) => {
if (event.target.classList.contains('delete')) {
const taskItem = event.target.closest('.task');
taskItem.remove();
}
});
Con este enfoque:
- Asignamos un único listener al elemento contenedor
- Verificamos si el elemento que originó el evento (
event.target
) es el que nos interesa - Aplicamos la lógica correspondiente solo cuando es necesario
Identificación precisa del elemento objetivo
Para identificar correctamente el elemento que desencadenó el evento, podemos usar diferentes técnicas:
document.getElementById('container').addEventListener('click', (event) => {
// Por clase CSS
if (event.target.classList.contains('button')) {
// Acción para botones
}
// Por atributo data
if (event.target.dataset.action === 'edit') {
// Acción para elementos con data-action="edit"
}
// Por selector de coincidencia
if (event.target.matches('.item > .icon')) {
// Acción para iconos dentro de items
}
});
Método closest() para elementos anidados
Cuando trabajamos con elementos anidados, el método closest()
es extremadamente útil para encontrar el ancestro más cercano que coincida con un selector:
document.getElementById('shoppingList').addEventListener('click', (event) => {
// Si hacemos clic en cualquier elemento dentro de un item
const listItem = event.target.closest('li');
if (listItem) {
// Verificamos si el clic fue en el botón de eliminar
if (event.target.classList.contains('delete')) {
listItem.remove();
}
// Verificamos si el clic fue en el botón de editar
else if (event.target.classList.contains('edit')) {
editItem(listItem);
}
}
});
Delegación con múltiples tipos de eventos
La delegación funciona con cualquier tipo de evento que se propague:
const form = document.getElementById('userForm');
// Delegación para diferentes tipos de eventos
form.addEventListener('input', handleFormChanges);
form.addEventListener('change', handleFormChanges);
form.addEventListener('submit', handleFormSubmit);
function handleFormChanges(event) {
const target = event.target;
if (target.matches('input[type="text"]')) {
validateTextField(target);
} else if (target.matches('select')) {
updateDependentFields(target);
}
}
Ventajas de la delegación de eventos
- Rendimiento mejorado: Menos listeners significa menos sobrecarga de memoria
- Manejo dinámico: Funciona automáticamente con elementos añadidos dinámicamente
- Código más limpio: Centraliza la lógica de manejo de eventos
- Menos fugas de memoria: Evita problemas con listeners huérfanos cuando se eliminan elementos
Ejemplo práctico: Tabla de datos interactiva
Veamos un ejemplo más completo de una tabla con múltiples acciones:
const dataTable = document.getElementById('dataTable');
dataTable.addEventListener('click', (event) => {
const target = event.target;
const row = target.closest('tr');
// No procesar si no hay fila o es el encabezado
if (!row || row.parentElement.tagName === 'THEAD') return;
// Determinar la acción según el elemento clicado
if (target.classList.contains('btn-edit')) {
openEditModal(row.dataset.id);
} else if (target.classList.contains('btn-delete')) {
confirmDelete(row.dataset.id);
} else if (target.tagName === 'TD' && !target.classList.contains('no-select')) {
toggleRowSelection(row);
}
});
Limitaciones de la delegación
Aunque la delegación es poderosa, tiene algunas limitaciones:
- No funciona con eventos que no se propagan (como
focus
yblur
en algunos navegadores) - Para estos casos, podemos usar los eventos
focusin
yfocusout
que sí se propagan:
// Delegación para eventos de foco
document.getElementById('formContainer').addEventListener('focusin', (event) => {
if (event.target.tagName === 'INPUT') {
event.target.classList.add('active');
}
});
document.getElementById('formContainer').addEventListener('focusout', (event) => {
if (event.target.tagName === 'INPUT') {
event.target.classList.remove('active');
}
});
Delegación con elementos deshabilitados
Los elementos con el atributo disabled
no disparan eventos, lo que puede complicar la delegación:
// Solución: usar data-attributes en lugar de disabled
document.querySelectorAll('[data-disabled="true"]').forEach(el => {
el.setAttribute('aria-disabled', 'true');
// No usar el atributo disabled, sino simular su comportamiento
});
container.addEventListener('click', (event) => {
if (event.target.getAttribute('aria-disabled') === 'true') {
event.preventDefault();
return false;
}
// Continuar con el manejo normal
});
La delegación de eventos es una técnica fundamental para crear interfaces web eficientes y mantenibles, especialmente cuando trabajamos con contenido dinámico o listas extensas de elementos interactivos.
Eventos personalizados: Creación, dispatch y comunicación entre componentes
Los eventos personalizados en JavaScript permiten extender el sistema de eventos del DOM para crear una comunicación flexible entre componentes de una aplicación. A diferencia de los eventos nativos como click
o submit
, los eventos personalizados son definidos por el desarrollador para representar acciones específicas de la aplicación.
Fundamentos de eventos personalizados
Los eventos personalizados se crean utilizando la interfaz CustomEvent
, que extiende la interfaz básica Event
añadiendo la capacidad de transportar datos personalizados:
// Creación básica de un evento personalizado
const event = new CustomEvent('userLogin', {
bubbles: true,
cancelable: true,
detail: { userId: 42, username: 'john_doe' }
});
El constructor CustomEvent
acepta dos parámetros:
- El nombre del evento (string)
- Un objeto de opciones con las siguientes propiedades:
bubbles
: Determina si el evento se propaga en el árbol DOM (default:false
)cancelable
: Indica si el evento puede ser cancelado (default:false
)detail
: Objeto que contiene los datos personalizados que queremos transmitir
Disparando eventos personalizados
Una vez creado el evento, podemos dispararlo (dispatch) en cualquier elemento del DOM:
// Crear el evento
const productAddedEvent = new CustomEvent('productAdded', {
bubbles: true,
detail: {
productId: 'abc123',
quantity: 2,
price: 29.99
}
});
// Disparar el evento desde un elemento
document.getElementById('addToCartButton').addEventListener('click', () => {
// Lógica para añadir al carrito
// Notificar a otros componentes mediante el evento personalizado
document.dispatchEvent(productAddedEvent);
});
Escuchando eventos personalizados
Para escuchar eventos personalizados, utilizamos addEventListener
exactamente igual que con eventos nativos:
// Escuchar el evento personalizado
document.addEventListener('productAdded', (event) => {
const { productId, quantity, price } = event.detail;
// Actualizar el contador del carrito
updateCartCounter(quantity);
// Mostrar notificación
showNotification(`Producto añadido: $${price}`);
});
Comunicación entre componentes
Los eventos personalizados son ideales para implementar una arquitectura basada en eventos donde los componentes se comunican sin acoplarse directamente:
// Componente emisor (ShoppingCart)
class ShoppingCart {
addItem(product) {
// Lógica para añadir el producto
this.items.push(product);
// Notificar a otros componentes
const event = new CustomEvent('cart:changed', {
bubbles: true,
detail: {
items: this.items,
totalItems: this.items.length,
totalPrice: this.calculateTotal()
}
});
document.dispatchEvent(event);
}
}
// Componente receptor (CartIndicator)
class CartIndicator {
constructor() {
this.element = document.querySelector('.cart-indicator');
// Escuchar cambios en el carrito
document.addEventListener('cart:changed', this.update.bind(this));
}
update(event) {
const { totalItems } = event.detail;
this.element.textContent = totalItems;
if (totalItems > 0) {
this.element.classList.add('active');
}
}
}
Eventos con namespace
Una buena práctica es utilizar namespaces (espacios de nombres) para organizar los eventos personalizados y evitar colisiones:
// Eventos con namespace usando el patrón 'dominio:acción'
const events = {
cart: {
ITEM_ADDED: 'cart:itemAdded',
ITEM_REMOVED: 'cart:itemRemoved',
CLEARED: 'cart:cleared'
},
auth: {
LOGIN: 'auth:login',
LOGOUT: 'auth:logout',
SESSION_EXPIRED: 'auth:sessionExpired'
}
};
// Uso
document.dispatchEvent(new CustomEvent(events.auth.LOGIN, {
bubbles: true,
detail: { user: currentUser }
}));
Cancelación de eventos personalizados
Los eventos personalizados pueden ser cancelables, lo que permite a los receptores detener el flujo normal:
// Crear un evento cancelable
const beforeSubmitEvent = new CustomEvent('form:beforeSubmit', {
bubbles: true,
cancelable: true, // Importante para permitir cancelación
detail: { formData: formData }
});
// Disparar y verificar si fue cancelado
document.getElementById('userForm').addEventListener('submit', function(e) {
e.preventDefault();
// Disparar evento personalizado antes de enviar
const canContinue = this.dispatchEvent(beforeSubmitEvent);
if (canContinue) {
// Nadie canceló el evento, continuar con el envío
submitFormData();
} else {
console.log('Envío cancelado por un listener');
}
});
// Listener que puede cancelar el evento
document.addEventListener('form:beforeSubmit', (event) => {
const { formData } = event.detail;
// Validación personalizada
if (!validateCustomRules(formData)) {
// Cancelar el evento
event.preventDefault();
showValidationErrors();
}
});
Eventos de un solo uso
Para situaciones donde solo necesitamos escuchar un evento una vez, podemos usar la opción once
:
document.addEventListener('app:initialized', (event) => {
// Configuración que solo debe ejecutarse una vez
setupInitialState(event.detail.config);
}, { once: true });
Patrón de bus de eventos
Para aplicaciones más complejas, podemos implementar un bus de eventos centralizado:
// Implementación simple de un bus de eventos
class EventBus {
constructor() {
this.eventTarget = new EventTarget();
}
on(eventName, listener) {
this.eventTarget.addEventListener(eventName, listener);
}
off(eventName, listener) {
this.eventTarget.removeEventListener(eventName, listener);
}
once(eventName, listener) {
this.eventTarget.addEventListener(eventName, listener, { once: true });
}
emit(eventName, detail = {}) {
const event = new CustomEvent(eventName, { detail });
this.eventTarget.dispatchEvent(event);
}
}
// Uso del bus de eventos
const bus = new EventBus();
// Componente A
bus.on('userUpdated', (event) => {
updateUserInterface(event.detail);
});
// Componente B
function saveUserData(userData) {
// Guardar datos
api.updateUser(userData).then(() => {
bus.emit('userUpdated', userData);
});
}
Eventos personalizados y Web Components
Los eventos personalizados son especialmente útiles cuando trabajamos con Web Components, permitiendo una comunicación desacoplada:
// Componente personalizado que emite eventos
class ProductCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// Configuración del componente...
}
connectedCallback() {
this.shadowRoot.innerHTML = `
<div class="product">
<h3>${this.getAttribute('name')}</h3>
<p>$${this.getAttribute('price')}</p>
<button class="add-to-cart">Añadir al carrito</button>
</div>
`;
this.shadowRoot.querySelector('.add-to-cart').addEventListener('click', () => {
// Emitir evento que atraviesa el Shadow DOM
this.dispatchEvent(new CustomEvent('product:added', {
bubbles: true,
composed: true, // Permite que el evento atraviese el límite del Shadow DOM
detail: {
id: this.getAttribute('product-id'),
name: this.getAttribute('name'),
price: parseFloat(this.getAttribute('price'))
}
}));
});
}
}
customElements.define('product-card', ProductCard);
Los eventos personalizados proporcionan un mecanismo poderoso para crear sistemas de comunicación flexibles y desacoplados entre los diferentes componentes de una aplicación web moderna, facilitando la creación de arquitecturas escalables y mantenibles.
Otras lecciones de JavaScript
Accede a todas las lecciones de JavaScript y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.
Introducción A Javascript
Introducción Y Entorno
Tipos De Datos
Sintaxis
Variables
Sintaxis
Operadores
Sintaxis
Estructuras De Control
Sintaxis
Funciones
Sintaxis
Funciones Cierre (Closure)
Sintaxis
Métodos De Strings
Sintaxis
Funciones Cierre (Closure)
Sintaxis
Operadores Avanzados
Sintaxis
Funciones
Sintaxis
Expresiones Regulares
Sintaxis
Estructuras De Control
Sintaxis
Arrays Y Métodos
Estructuras De Datos
Conjuntos Con Set
Estructuras De Datos
Mapas Con Map
Estructuras De Datos
Conjuntos Con Set
Estructuras De Datos
Funciones Flecha
Programación Funcional
Filtrado Con Filter() Y Find()
Programación Funcional
Transformación Con Map()
Programación Funcional
Reducción Con Reduce()
Programación Funcional
Funciones Flecha
Programación Funcional
Transformación Con Map()
Programación Funcional
Inmutabilidad Y Programación Funcional Pura
Programación Funcional
Clases Y Objetos
Programación Orientada A Objetos
Excepciones
Programación Orientada A Objetos
Encapsulación
Programación Orientada A Objetos
Herencia
Programación Orientada A Objetos
Polimorfismo
Programación Orientada A Objetos
This Y Contexto
Programación Orientada A Objetos
Patrón De Módulos Y Namespace
Programación Orientada A Objetos
Prototipos Y Cadena De Prototipos
Programación Orientada A Objetos
Destructuring De Objetos Y Arrays
Programación Orientada A Objetos
Manipulación Dom
Dom
Selección De Elementos Dom
Dom
Modificación De Elementos Dom
Dom
Eventos Del Dom
Dom
Localstorage Y Sessionstorage
Dom
Bom (Browser Object Model)
Dom
Callbacks
Programación Asíncrona
Promises
Programación Asíncrona
Async / Await
Programación Asíncrona
Api Fetch
Programación Asíncrona
Naturaleza De Js Y Event Loop
Programación Asíncrona
Websockets
Programación Asíncrona
Módulos En Es6
Construcción
Configuración De Bundlers Como Vite
Construcción
Eslint Y Calidad De Código
Construcción
Npm Y Dependencias
Construcción
Introducción A Pruebas En Js
Testing
Pruebas Unitarias
Testing
Ejercicios de programación de JavaScript
Evalúa tus conocimientos de esta lección Eventos del DOM con nuestros retos de programación de tipo Test, Puzzle, Código y Proyecto con VSCode, guiados por IA.
Excepciones
Transformación con map()
Arrays y Métodos
Reto Métodos de Strings
Transformación con map()
Funciones flecha
Selección de elementos DOM
API Fetch
Encapsulación
Mapas con Map
Creación y uso de variables
Polimorfismo
Reto Funciones flecha
Tipos de datos
Reto Operadores avanzados
Reto Estructuras de control
Estructuras de control
Pruebas unitarias
Inmutabilidad y programación funcional pura
Funciones flecha
Polimorfismo
Reto Polimorfismo
Array
Transformación con map()
Reto Variables
Gestor de tareas con JavaScript
Proyecto Modificación de elementos DOM
Manipulación DOM
Funciones
Conjuntos con Set
Reto Prototipos y cadena de prototipos
Reto Encapsulación
Funciones flecha
Async / Await
Reto Excepciones
Reto Filtrado con filter() y find()
Reto Promises
Creación y uso de variables
Excepciones
Promises
Funciones cierre (closure)
Reto Herencia
Herencia
Reto Async / Await
Proyecto Eventos del DOM
Herencia
Selección de elementos DOM
Modificación de elementos DOM
Reto Clases y objetos
Filtrado con filter() y find()
Funciones cierre (closure)
Reto Destructuring de objetos y arrays
Callbacks
Funciones
Mapas con Map
Reducción con reduce()
Callbacks
Manipulación DOM
Introducción al DOM
Reto Funciones
Reto Funciones cierre (closure)
Promises
Reto Reducción con reduce()
Async / Await
Reto Estructuras de control
Eventos del DOM
Introducción a JavaScript
Async / Await
Promises
Selección de elementos DOM
Filtrado con filter() y find()
Callbacks
Creación de clases y objetos Restaurante
Reducción con reduce()
Filtrado con filter() y find()
Reducción con reduce()
Conjuntos con Set
Herencia de clases
Eventos del DOM
Clases y objetos
Modificación de elementos DOM
Mapas con Map
Proyecto carrito compra agoodshop
Introducción a JavaScript
Reto Mapas con Map
Funciones
Proyecto administrador de contactos
Reto Expresiones regulares
Tipos de datos
Clases y objetos
Array
Conjuntos con Set
Array
Encapsulación
Clases y objetos
Uso de operadores
Uso de operadores
Estructuras de control
Proyecto Manipulación DOM
En esta lección
Objetivos de aprendizaje de esta lección
- Comprender la importancia de los eventos del DOM en el desarrollo web.
- Aprender a utilizar los escuchadores de eventos para detectar y responder a eventos específicos.
- Conocer algunos de los eventos del DOM más comúnmente utilizados, como 'click', 'dblclick', 'mouseenter', 'mouseleave', 'keydown', 'keyup', 'load', 'submit', y 'change'.
- Entender cómo pasar funciones de callback como argumentos a los escuchadores de eventos y cómo trabajar con el objeto de evento para obtener información sobre el evento que ocurrió.
- Aprender cómo crear interactividad en una página web mediante la detección y respuesta a eventos del DOM.