HTMX proporciona una serie de eventos clave para controlar el ciclo de vida completo de las solicitudes y manipulaciones del DOM. A continuación, exploraremos los eventos más comunes de HTMX, cómo funcionan y cuándo usarlos para mejorar la interactividad de tus aplicaciones web sin necesidad de escribir JavaScript adicional.
htmx:beforeRequestSe dispara ANTES de que HTMX envíe cualquier solicitud al servidor. Es el primer evento en la cadena.
Ejemplo básico:
htmx.on("htmx:beforeRequest", (event) => {
console.log("Antes de enviar la solicitud");
}); Ejemplo práctico:
<button hx-get="/api/data" hx-target="#content">
Cargar Datos
</button>
<div id="content"></div>
<script>
htmx.on("htmx:beforeRequest", (event) => {
// Mostrar spinner de carga
const spinner = document.createElement('div');
spinner.className = 'loading-spinner';
spinner.innerHTML = 'Cargando...';
event.detail.elt.appendChild(spinner);
// Deshabilitar el botón
event.detail.elt.disabled = true;
});
</script> htmx:beforeSendSe dispara justo ANTES de enviar la solicitud HTTP, pero después de htmx:beforeRequest.
Ejemplo básico:
htmx.on("htmx:beforeSend", (event) => {
console.log("Preparando request");
}); Ejemplo práctico:
<form hx-post="/api/submit" hx-target="#result">
<input name="data" value="test">
<button type="submit">Enviar</button>
</form>
<script>
htmx.on("htmx:beforeSend", (event) => {
// Agregar token de autorización
event.detail.xhr.setRequestHeader('Authorization', 'Bearer ' + localStorage.getItem('token'));
console.log('Enviando solicitud a:', event.detail.requestConfig.path);
});
</script> htmx:beforeSwapSe dispara DESPUÉS de recibir la respuesta del servidor pero ANTES de insertar el contenido en el DOM.
Ejemplo básico:
htmx.on("htmx:beforeSwap", (event) => {
console.log("Antes de insertar el contenido en el DOM");
}); Ejemplo práctico:
<div hx-get="/api/content" hx-target="#result">
Cargar Contenido
</div>
<div id="result"></div>
<script>
htmx.on("htmx:beforeSwap", (event) => {
// Validar la respuesta
if (event.detail.xhr.status === 404) {
event.detail.shouldSwap = false;
alert('Contenido no encontrado');
return;
}
// Limpiar clases anteriores
const target = event.detail.target;
target.classList.remove('error', 'loading');
});
</script> htmx:afterSwapSe dispara DESPUÉS de que el nuevo contenido ha sido insertado en el DOM.
Ejemplo básico:
htmx.on("htmx:afterSwap", (event) => {
console.log("Contenido insertado en el DOM");
}); Ejemplo práctico:
<button hx-get="/api/table" hx-target="#table-container">
Cargar Tabla
</button>
<div id="table-container"></div>
<script>
htmx.on("htmx:afterSwap", (event) => {
// Inicializar DataTables en las nuevas tablas
const tables = event.detail.target.querySelectorAll('table.data-table');
tables.forEach(table => {
$(table).DataTable({
responsive: true,
pageLength: 10
});
});
// Aplicar animación de entrada
event.detail.target.classList.add('fade-in');
});
</script> htmx:afterSettleSe dispara DESPUÉS de que todo (incluyendo animaciones y transiciones) ha terminado.
Ejemplo básico:
htmx.on("htmx:afterSettle", (event) => {
console.log("Todo ha terminado (incluyendo animaciones)");
}); Ejemplo práctico:
<form hx-post="/api/login" hx-target="#login-result">
<input name="username" required>
<input name="password" type="password" required>
<button type="submit">Login</button>
</form>
<div id="login-result"></div>
<script>
htmx.on("htmx:afterSettle", (event) => {
// Si es un login exitoso, enfocar en el primer input
const successMessage = event.detail.target.querySelector('.success');
if (successMessage) {
const nextForm = document.querySelector('#next-step-form input');
if (nextForm) nextForm.focus();
}
// Enviar métricas
analytics.track('htmx_request_completed', {
url: event.detail.pathInfo.requestPath,
duration: Date.now() - event.detail.requestConfig.timestamp
});
});
</script> htmx:afterRequestSe dispara finalmente, DESPUÉS de que la solicitud ha terminado completamente (exitosa o fallida).
htmx.on("htmx:afterRequest", (event) => {
console.log("Solicitud finalizada completamente");
}); Ejemplo práctico:
<button hx-get="/api/data" hx-target="#content" class="btn">
Cargar
</button>
<script>
htmx.on("htmx:afterRequest", (event) => {
// Reactivar botón y quitar spinner (independientemente del resultado)
const button = event.detail.elt;
button.disabled = false;
const spinner = button.querySelector('.loading-spinner');
if (spinner) {
spinner.remove();
}
// Log del resultado
console.log('Solicitud finalizada:', {
success: event.detail.successful,
status: event.detail.xhr.status,
url: event.detail.pathInfo.requestPath
});
});
</script> Estos eventos permiten modificar la configuración de las solicitudes y respuestas de HTMX.
htmx:configRequestPermite modificar la configuración de la solicitud antes de enviarla.
Ejemplo básico:
htmx.on("htmx:configRequest", (event) => {
event.detail.headers['Authorization'] = 'Bearer token';
}); Ejemplo práctico:
<form hx-post="/api/search" hx-target="#results">
<input name="query" placeholder="Buscar...">
<select name="category">
<option value="all">Todas</option>
<option value="products">Productos</option>
</select>
<button type="submit">Buscar</button>
</form>
<script>
htmx.on("htmx:configRequest", (event) => {
// Agregar timestamp a todas las solicitudes
event.detail.parameters['timestamp'] = Date.now();
// Cambiar método si es búsqueda vacía
const query = event.detail.parameters['query'];
if (!query || query.trim() === '') {
event.detail.verb = 'get';
event.detail.path = '/api/trending';
}
});
</script> Estos eventos permiten validar y confirmar acciones antes de proceder con solicitudes.
htmx:confirmSe dispara cuando HTMX necesita confirmación antes de proceder (por ejemplo, con hx-confirm).
Ejemplo básico:
htmx.on("htmx:confirm", (event) => {
event.preventDefault();
if (confirm("¿Estás seguro?")) {
event.detail.issueRequest();
}
}); Ejemplo práctico:
<button hx-delete="/api/user/123"
hx-confirm="¿Estás seguro?"
hx-target="#result">
Eliminar Usuario
</button>
<script>
htmx.on("htmx:confirm", (event) => {
// Prevenir la confirmación por defecto
event.preventDefault();
// Crear modal de confirmación personalizado
const modal = document.createElement('div');
modal.className = 'custom-confirm-modal';
modal.innerHTML = `
<div class="modal-content">
<p>${event.detail.question}</p>
<button id="confirm-yes">Sí, eliminar</button>
<button id="confirm-no">Cancelar</button>
</div>
`;
document.body.appendChild(modal);
document.getElementById('confirm-yes').onclick = () => {
modal.remove();
event.detail.issueRequest(); // Proceder con la solicitud
};
document.getElementById('confirm-no').onclick = () => {
modal.remove(); // Cancelar
};
});
</script> htmx:validateUrlPermite validar URLs antes de que HTMX las use.
Ejemplo práctico:
htmx.on("htmx:validateUrl", (event) => {
const allowedDomains = ['api.example.com', 'cdn.example.com'];
const url = new URL(event.detail.sameHost ? event.detail.url : 'https://' + event.detail.url);
if (!allowedDomains.includes(url.hostname)) {
event.preventDefault();
console.warn('URL bloqueada:', event.detail.url);
}
}); Estos eventos permiten limpiar recursos y manejar el ciclo de vida de los elementos HTMX.
htmx:beforeCleanupElementSe dispara ANTES de que HTMX limpie un elemento del DOM.
Ejemplo práctico:
<div hx-get="/api/content" hx-target="this">
<div class="chart-container">
<!-- Chart.js canvas -->
<canvas id="myChart"></canvas>
</div>
</div>
<script>
htmx.on("htmx:beforeCleanupElement", (event) => {
// Limpiar Chart.js antes de remover el elemento
const charts = event.detail.elt.querySelectorAll('canvas');
charts.forEach(canvas => {
const chartInstance = Chart.getChart(canvas);
if (chartInstance) {
chartInstance.destroy();
}
});
// Limpiar event listeners personalizados
const customElements = event.detail.elt.querySelectorAll('[data-custom-listener]');
customElements.forEach(el => {
// Remover listeners específicos
el.removeEventListener('custom-event', el.customHandler);
});
});
</script> Estos eventos son menos comunes pero útiles para casos específicos.
htmx:loadSe dispara cuando HTMX se carga y está listo para procesar elementos.
Ejemplo básico:
htmx.on("htmx:load", () => {
console.log("Nuevo contenido cargado");
}); Ejemplo práctico:
htmx.on("htmx:load", (event) => {
console.log('HTMX está listo');
// Configuración global
htmx.config.globalViewTransitions = true;
htmx.config.scrollBehavior = 'smooth';
// Setup de headers globales
document.body.addEventListener('htmx:beforeRequest', (e) => {
e.detail.xhr.setRequestHeader('X-App-Version', '1.0.0');
});
}); htmx:oobAfterSwapSe dispara DESPUÉS de un swap “out-of-band” (fuera de banda).
Ejemplo práctico:
<!-- Respuesta del servidor incluye hx-swap-oob -->
<!--
<div id="sidebar" hx-swap-oob="true">Nuevo contenido sidebar</div>
<div id="main-content">Contenido principal</div>
-->
<script>
htmx.on("htmx:oobAfterSwap", (event) => {
console.log('Elemento actualizado fuera de banda:', event.detail.elt.id);
// Si se actualizó el sidebar, actualizar navegación
if (event.detail.elt.id === 'sidebar') {
updateNavigationState();
}
});
</script> htmx:timeoutSe dispara cuando una solicitud HTMX excede el tiempo límite.
Ejemplo básico:
htmx.on("htmx:timeout", () => {
alert("Se agotó el tiempo de la solicitud");
}); Ejemplo práctico:
<button hx-get="/api/slow-endpoint"
hx-timeout="5000"
hx-target="#result">
Cargar (puede ser lento)
</button>
<script>
htmx.on("htmx:timeout", (event) => {
// Mostrar mensaje de timeout personalizado
const target = event.detail.target || event.detail.elt;
target.innerHTML = `
<div class="error-message">
<p>La solicitud está tomando más tiempo del esperado.</p>
<button onclick="this.parentElement.parentElement.click()">
Reintentar
</button>
</div>
`;
console.warn('Timeout en solicitud:', event.detail.pathInfo.requestPath);
});
</script> htmx:xhr:progressSe dispara DURANTE la carga de una solicitud para mostrar el progreso.
Ejemplo práctico:
<form hx-post="/api/upload"
hx-encoding="multipart/form-data"
hx-target="#upload-result">
<input type="file" name="file" required>
<button type="submit">Subir Archivo</button>
<div id="progress-bar" style="display:none;">
<div class="progress-fill"></div>
<span class="progress-text">0%</span>
</div>
</form>
<script>
htmx.on("htmx:xhr:progress", (event) => {
const progressBar = document.getElementById('progress-bar');
const progressFill = progressBar.querySelector('.progress-fill');
const progressText = progressBar.querySelector('.progress-text');
if (event.detail.lengthComputable) {
const percent = Math.round((event.detail.loaded / event.detail.total) * 100);
progressBar.style.display = 'block';
progressFill.style.width = percent + '%';
progressText.textContent = percent + '%';
}
});
htmx.on("htmx:afterRequest", (event) => {
// Ocultar barra de progreso después de completar
const progressBar = document.getElementById('progress-bar');
progressBar.style.display = 'none';
});
</script> htmx:triggerSe dispara cuando un elemento con HTMX es activado (triggered).
Ejemplo práctico:
<input hx-get="/api/search"
hx-trigger="keyup changed delay:300ms"
hx-target="#search-results"
placeholder="Buscar...">
<script>
htmx.on("htmx:trigger", (event) => {
console.log('Elemento activado:', {
element: event.detail.elt.tagName,
trigger: event.type,
value: event.detail.elt.value
});
// Analytics para búsquedas
if (event.detail.elt.type === 'text' && event.detail.elt.value.length > 2) {
analytics.track('search_query', {
query: event.detail.elt.value,
trigger_type: 'keyup'
});
}
});
</script> <!DOCTYPE html>
<html>
<head>
<title>Sistema de Comentarios con HTMX Events</title>
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
<style>
.loading { opacity: 0.5; }
.error { border: 2px solid red; }
.success { border: 2px solid green; }
.fade-in { animation: fadeIn 0.3s; }
@keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
</style>
</head>
<body>
<!-- Formulario de comentarios -->
<form hx-post="/api/comments"
hx-target="#comments-list"
hx-swap="afterbegin"
id="comment-form">
<textarea name="content" required placeholder="Escribe tu comentario..."></textarea>
<button type="submit">Publicar Comentario</button>
</form>
<!-- Lista de comentarios -->
<div id="comments-list" hx-get="/api/comments" hx-trigger="load">
Cargando comentarios...
</div>
<script>
// Antes de enviar cualquier solicitud
htmx.on("htmx:beforeRequest", (event) => {
event.detail.elt.classList.add('loading');
if (event.detail.elt.tagName === 'BUTTON') {
event.detail.elt.disabled = true;
event.detail.elt.textContent = 'Enviando...';
}
});
// Configurar solicitudes
htmx.on("htmx:configRequest", (event) => {
event.detail.headers['X-CSRF-Token'] = document.querySelector('meta[name="csrf-token"]')?.content;
});
// Antes de intercambiar contenido
htmx.on("htmx:beforeSwap", (event) => {
if (event.detail.xhr.status >= 400) {
event.detail.shouldSwap = false;
event.detail.target.classList.add('error');
const errorMsg = document.createElement('div');
errorMsg.textContent = 'Error al procesar la solicitud';
errorMsg.className = 'error-message';
event.detail.target.appendChild(errorMsg);
}
});
// Después de intercambiar contenido
htmx.on("htmx:afterSwap", (event) => {
event.detail.target.classList.add('fade-in');
// Si es un nuevo comentario, limpiar el formulario
if (event.detail.elt.id === 'comment-form') {
event.detail.elt.reset();
}
// Reinicializar elementos interactivos
const newButtons = event.detail.target.querySelectorAll('button[data-action]');
newButtons.forEach(btn => {
btn.addEventListener('click', handleCommentAction);
});
});
// Después de completar todo
htmx.on("htmx:afterRequest", (event) => {
event.detail.elt.classList.remove('loading', 'error');
if (event.detail.elt.tagName === 'BUTTON') {
event.detail.elt.disabled = false;
event.detail.elt.textContent = 'Publicar Comentario';
}
// Remover mensajes de error anteriores
const errorMessages = event.detail.elt.querySelectorAll('.error-message');
errorMessages.forEach(msg => msg.remove());
});
// Manejar confirmaciones
htmx.on("htmx:confirm", (event) => {
if (event.detail.question.includes('eliminar')) {
event.preventDefault();
if (confirm('¿Estás seguro de que quieres eliminar este comentario?')) {
event.detail.issueRequest();
}
}
});
// Función auxiliar para manejar acciones de comentarios
function handleCommentAction(e) {
const action = e.target.dataset.action;
console.log('Acción de comentario:', action);
}
</script>
</body>
</html> Recuerda el orden de los eventos principales:
htmx:beforeRequest: antes de enviar la solicitudhtmx:configRequest: para modificar la solicitudhtmx:beforeSend: justo antes de enviar la solicitudhtmx:beforeSwap: antes de insertar el contenido en el DOMhtmx:afterSwap: después de insertar el contenido en el DOMhtmx:afterSettle: después de que todo ha terminado (incluyendo animaciones)htmx:afterRequest: después de que la solicitud ha terminado completamenteevent.detail.elt para targetear el elemento específicoUtiliza htmx.on para registrar eventos de debugging:
// Modo debug: loggear todos los eventos
['beforeRequest', 'beforeSwap', 'afterSwap', 'afterSettle', 'afterRequest'].forEach(eventName => {
htmx.on(`htmx:${eventName}`, (event) => {
console.log(`🔥 ${eventName}:`, event.detail);
});
}); Siempre maneja los casos de error en htmx:beforeSwap:
htmx.on("htmx:beforeSwap", (event) => {
if (event.detail.xhr.status >= 400) {
event.detail.shouldSwap = false;
// Manejar error personalizado
}
}); Usa htmx:beforeCleanupElement para limpiar recursos:
htmx.on("htmx:beforeCleanupElement", (event) => {
// Limpiar timers, listeners, plugins, etc.
const timers = event.detail.elt.dataset.timers?.split(',') || [];
timers.forEach(id => clearTimeout(parseInt(id)));
});