Guía de Eventos Comunes en HTMX: Control Total del Ciclo de Vida

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.

1Evento htmx:beforeRequest

Se dispara ANTES de que HTMX envíe cualquier solicitud al servidor. Es el primer evento en la cadena.

  ¿Cuándo usarlo?

  • Mostrar indicadores de carga
  • Deshabilitar botones para evitar múltiples envíos
  • Aplicar animaciones de salida
  • Preparar la UI para la nueva carga

Ejemplo básico:

js
htmx.on("htmx:beforeRequest", (event) => {
  console.log("Antes de enviar la solicitud");
});

Ejemplo práctico:

html
<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>

2. htmx:beforeSend

Se dispara justo ANTES de enviar la solicitud HTTP, pero después de htmx:beforeRequest.

  ¿Cuándo usarlo?

  • Modificar headers de la solicitud
  • Agregar tokens de autenticación
  • Logging de solicitudes

Ejemplo básico:

js
htmx.on("htmx:beforeSend", (event) => {
  console.log("Preparando request");
});

Ejemplo práctico:

html
<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>

3. htmx:beforeSwap

Se dispara DESPUÉS de recibir la respuesta del servidor pero ANTES de insertar el contenido en el DOM.

  ¿Cuándo usarlo?

  • Validar la respuesta antes de mostrarla
  • Limpiar animaciones anteriores
  • Preparar el contenedor para el nuevo contenido
  • Cancelar el swap si es necesario

Ejemplo básico:

js
htmx.on("htmx:beforeSwap", (event) => {
  console.log("Antes de insertar el contenido en el DOM");
});

Ejemplo práctico:

html
<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>

4. htmx:afterSwap

Se dispara DESPUÉS de que el nuevo contenido ha sido insertado en el DOM.

  ¿Cuándo usarlo?

  • Inicializar plugins de terceros
  • Aplicar animaciones de entrada
  • Actualizar el estado de la UI
  • Ejecutar código que necesita el nuevo contenido

Ejemplo básico:

js
htmx.on("htmx:afterSwap", (event) => {
  console.log("Contenido insertado en el DOM");
});

Ejemplo práctico:

html
<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>

5. htmx:afterSettle

Se dispara DESPUÉS de que todo (incluyendo animaciones y transiciones) ha terminado.

  ¿Cuándo usarlo?

  • Cleanup final después de animaciones
  • Establecer focus en elementos
  • Actualizar URLs o historial
  • Métricas y analytics

Ejemplo básico:

js
htmx.on("htmx:afterSettle", (event) => {
  console.log("Todo ha terminado (incluyendo animaciones)");
});

Ejemplo práctico:

html
<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>

6. htmx:afterRequest

Se dispara finalmente, DESPUÉS de que la solicitud ha terminado completamente (exitosa o fallida).

  ¿Cuándo usarlo?

  • Cleanup universal (exitoso o fallido)
  • Reactivar botones
  • Ocultar spinners de carga
  • Logging de finalización

js
htmx.on("htmx:afterRequest", (event) => {
  console.log("Solicitud finalizada completamente");
});

Ejemplo práctico:

html
<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>

Eventos de Configuración


Estos eventos permiten modificar la configuración de las solicitudes y respuestas de HTMX.

7. htmx:configRequest

Permite modificar la configuración de la solicitud antes de enviarla.

  ¿Cuándo usarlo?

  • Modificar parámetros de la solicitud
  • Cambiar el método HTTP dinámicamente
  • Agregar parámetros adicionales

Ejemplo básico:

js
htmx.on("htmx:configRequest", (event) => {
event.detail.headers['Authorization'] = 'Bearer token';
});

Ejemplo práctico:

html
<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>

Eventos de Validación y Confirmación


Estos eventos permiten validar y confirmar acciones antes de proceder con solicitudes.

8. htmx:confirm

Se dispara cuando HTMX necesita confirmación antes de proceder (por ejemplo, con hx-confirm).

  ¿Cuándo usarlo?

  • Crear diálogos de confirmación personalizados
  • Validaciones complejas antes de enviar
  • Confirmaciones asíncronas

Ejemplo básico:

js
htmx.on("htmx:confirm", (event) => {
event.preventDefault();
if (confirm("¿Estás seguro?")) {
  event.detail.issueRequest();
}
});

Ejemplo práctico:

html
<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>

9. htmx:validateUrl

Permite validar URLs antes de que HTMX las use.

  ¿Cuándo usarlo?

  • Validar URLs contra whitelist/blacklist
  • Transformar URLs dinámicamente
  • Seguridad adicional

Ejemplo práctico:

js
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);
  }
});

Eventos de Limpieza


Estos eventos permiten limpiar recursos y manejar el ciclo de vida de los elementos HTMX.

10. htmx:beforeCleanupElement

Se dispara ANTES de que HTMX limpie un elemento del DOM.

  ¿Cuándo usarlo?

  • Cleanup de event listeners
  • Guardar estado antes de la limpieza
  • Cleanup de plugins de terceros

Ejemplo práctico:

html
<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>

Eventos Especializados


Estos eventos son menos comunes pero útiles para casos específicos.

11. htmx:load

Se dispara cuando HTMX se carga y está listo para procesar elementos.

  ¿Cuándo usarlo?

  • Inicialización global de HTMX
  • Configuración inicial
  • Setup de interceptores globales

Ejemplo básico:

js
htmx.on("htmx:load", () => {
  console.log("Nuevo contenido cargado");
});

Ejemplo práctico:

js
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');
  });
});

12. htmx:oobAfterSwap

Se dispara DESPUÉS de un swap “out-of-band” (fuera de banda).

  ¿Cuándo usarlo?

  • Manejar actualizaciones en múltiples partes de la página
  • Sincronizar cambios en elementos relacionados

Ejemplo práctico:

html
<!-- 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>

13. htmx:timeout

Se dispara cuando una solicitud HTMX excede el tiempo límite.

  ¿Cuándo usarlo?

  • Manejar timeouts de manera personalizada
  • Mostrar mensajes de error específicos
  • Reintentar solicitudes

Ejemplo básico:

js
htmx.on("htmx:timeout", () => {
alert("Se agotó el tiempo de la solicitud");
});

Ejemplo práctico:

html
<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>

14. htmx:xhr:progress

Se dispara DURANTE la carga de una solicitud para mostrar el progreso.

  ¿Cuándo usarlo?

  • Mostrar barras de progreso
  • Upload de archivos grandes
  • Feedback visual durante solicitudes largas

Ejemplo práctico:

html
<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>

15. htmx:trigger

Se dispara cuando un elemento con HTMX es activado (triggered).

  ¿Cuándo usarlo?

  • Logging de interacciones
  • Modificar comportamiento basado en el trigger
  • Analytics de uso

Ejemplo práctico:

html
<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>

Ejemplo Completo: Sistema de Comentarios

html
<!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>

Consejos y Buenas Prácticas

1. Orden de Ejecución

Recuerda el orden de los eventos principales:

  1. htmx:beforeRequest: antes de enviar la solicitud
  2. htmx:configRequest: para modificar la solicitud
  3. htmx:beforeSend: justo antes de enviar la solicitud
  4. htmx:beforeSwap: antes de insertar el contenido en el DOM
  5. htmx:afterSwap: después de insertar el contenido en el DOM
  6. htmx:afterSettle: después de que todo ha terminado (incluyendo animaciones)
  7. htmx:afterRequest: después de que la solicitud ha terminado completamente

2. Performance

  • No registres listeners pesados en eventos que se disparan frecuentemente
  • Usa event.detail.elt para targetear el elemento específico
  • Considera debouncing para eventos de input

3. Debugging

Utiliza htmx.on para registrar eventos de debugging:

js
// Modo debug: loggear todos los eventos
['beforeRequest', 'beforeSwap', 'afterSwap', 'afterSettle', 'afterRequest'].forEach(eventName => {
  htmx.on(`htmx:${eventName}`, (event) => {
      console.log(`🔥 ${eventName}:`, event.detail);
  });
});

4. Manejo de Errores

Siempre maneja los casos de error en htmx:beforeSwap:

js
htmx.on("htmx:beforeSwap", (event) => {
  if (event.detail.xhr.status >= 400) {
      event.detail.shouldSwap = false;
      // Manejar error personalizado
  }
});

5. Cleanup

Usa htmx:beforeCleanupElement para limpiar recursos:

js
htmx.on("htmx:beforeCleanupElement", (event) => {
  // Limpiar timers, listeners, plugins, etc.
  const timers = event.detail.elt.dataset.timers?.split(',') || [];
  timers.forEach(id => clearTimeout(parseInt(id)));
});