Eventos y scripting

Por: Artiko
htmxhtmx4eventoshx-onjavascriptapi

Eventos y scripting

htmx emite eventos en cada fase de una petición. Escucharlos es la manera de añadir comportamiento sin abandonar el modelo hipermedia.

El nuevo formato de nombres

htmx 4 estandarizó los nombres de los eventos:

htmx:<fase>:<sistema>[:<subacción>]
htmx 2htmx 4
htmx:beforeRequesthtmx:before:request
htmx:afterRequesthtmx:after:request
htmx:beforeSwaphtmx:before:swap
htmx:afterSwaphtmx:after:swap
htmx:afterSettlehtmx:after:settle
htmx:afterOnLoadhtmx:after:init
htmx:configRequesthtmx:config:request
htmx:responseErrorhtmx:response:error
xhr:*eliminados (ya no hay XHR)
htmx:validation:*eliminados (validación HTML5 nativa)

Catálogo de eventos

Ciclo de vida de la petición

EventoCuándoUso típico
htmx:config:requestAntes de armar la peticiónAñadir/quitar params y headers
htmx:confirmAntes de confirmar la acciónDiálogo de confirmación personalizado
htmx:before:requestJusto antes de enviarCancelar la petición
htmx:before:responseAl recibir la respuesta, antes de procesarlaInspeccionar status y headers
htmx:after:requestTras la respuestaReset de formularios, logging
htmx:response:errorRespuesta con código de errorManejo de errores
htmx:errorError de red o de htmxNotificar al usuario
htmx:abortPetición abortadaLimpiar indicadores
htmx:finally:requestSiempre, al finalLimpieza garantizada

Ciclo de vida del swap

EventoCuándo
htmx:before:swapAntes de insertar el HTML
htmx:after:swapDespués de insertarlo
htmx:before:settleAntes de la fase de settle
htmx:after:settleDespués del settle

Ciclo de vida de los elementos

EventoCuándo
htmx:before:init / htmx:after:inithtmx inicializa un elemento
htmx:before:process / htmx:after:processhtmx procesa un subárbol
htmx:process:{tipo}Procesamiento de un tipo concreto
htmx:before:cleanup / htmx:after:cleanupUn elemento sale del DOM
htmx:after:implicitInheritanceSe aplicó herencia implícita (auditoría de migración)

Historial y transiciones

htmx:before:history:update, htmx:after:history:update, htmx:after:history:push, htmx:after:history:replace, htmx:before:history:restore, htmx:before:viewTransition, htmx:after:viewTransition.

sequenceDiagram
    participant E as Elemento
    participant H as htmx
    participant S as Servidor
    E->>H: evento del trigger
    H->>H: htmx:config:request
    H->>H: htmx:confirm
    H->>H: htmx:before:request
    H->>S: fetch()
    S-->>H: respuesta
    H->>H: htmx:before:response
    H->>H: htmx:before:swap
    H->>E: inserta HTML
    H->>H: htmx:after:swap
    H->>H: htmx:before:settle
    H->>H: htmx:after:settle
    H->>H: htmx:after:request
    H->>H: htmx:finally:request

hx-on: escuchar desde el HTML

hx-on permite responder a cualquier evento sin salir del elemento. Es lo que en htmx llaman Locality of Behaviour: leyendo el elemento sabes todo lo que hace.

Eventos del DOM

<button hx-on:click="alert('¡Clic!')">Púlsame</button>
<input hx-on:focus="this.select()">
<div hx-on:mouseenter="this.classList.add('activo')"></div>

Eventos de htmx

Se escriben con el nombre completo, incluidos sus dos puntos:

<button hx-post="/ejemplo"
        hx-on:htmx:config:request="ctx.request.body.set('extra', 'Hola')">
  Enviar
</button>
<form hx-post="/mensajes"
      hx-on:htmx:after:request="find('input').value = ''">
  <input name="texto">
</form>

El objeto ctx

Todos los eventos de htmx 4 llevan un objeto ctx con la misma forma, accesible como ctx dentro de hx-on y como event.detail.ctx desde JavaScript.

PropiedadContenido
ctx.request.bodyFormData con los parámetros (mutable)
ctx.request.headersObjeto de headers (mutable)
ctx.response.statusCódigo HTTP de la respuesta
ctx.textCuerpo de la respuesta (mutable en htmx:after:request)
ctx.targetElemento destino del swap
ctx.newContentContenido recién insertado (tras el swap)

Ejemplos:

<!-- Añadir un parámetro -->
<button hx-post="/api"
        hx-on:htmx:config:request="ctx.request.body.set('tz', Intl.DateTimeFormat().resolvedOptions().timeZone)">

<!-- Añadir un header -->
<button hx-get="/api"
        hx-on:htmx:config:request="ctx.request.headers['X-Cliente'] = 'panel'">

<!-- Reaccionar al status -->
<form hx-post="/guardar"
      hx-on:htmx:after:request="if (ctx.response.status === 200) this.reset()">

API de scripting dentro de hx-on

Dentro de un handler hx-on tienes disponibles, además de this, event y ctx:

FunciónQué hace
find(selector)Busca relativo al elemento actual
findAll(selector)Todos los que coincidan
timeout(intervalo)Promesa que resuelve tras un intervalo

Y los handlers pueden ser asíncronos, lo que es nuevo en htmx 4:

<button hx-post="/like"
        hx-on:htmx:after:swap="await timeout('3s'); ctx.newContent[0].remove()">
  Me gusta
</button>

Un toast que se autodestruye:

<div class="toast" hx-on:load="await timeout('4s'); this.remove()">
  Guardado correctamente
</div>

Resetear el formulario que contiene al botón:

<button hx-post="/ejemplo"
        hx-on:htmx:after:request="find('closest form').reset()">
  Enviar
</button>

La API JavaScript

htmx 4 redujo su API: todo lo que el DOM ya hace bien fue eliminado.

Métodos disponibles

MétodoDescripción
htmx.ajax(verbo, url, opciones)Lanza una petición; devuelve promesa
htmx.find(selector)Primer elemento que coincide
htmx.findAll(selector)Todos los que coinciden
htmx.on(evento, handler)Registra un listener; devuelve el handler
htmx.onLoad(handler)Se ejecuta con cada contenido nuevo procesado
htmx.trigger(elemento, evento, detalle)Dispara un evento
htmx.process(elemento)Procesa un subárbol añadido manualmente
htmx.swap(target, contenido, opciones)Swap manual
htmx.parseInterval("500ms")Parsea intervalos a milisegundos
htmx.timeout("1s")Promesa que resuelve tras el intervalo
htmx.registerExtension(nombre, def)Registra una extensión
htmx.configObjeto de configuración
htmx.versionVersión actual

Eliminados en htmx 4

EliminadoReemplazo
htmx.addClass/removeClass/toggleClasselement.classList.*
htmx.closestelement.closest()
htmx.removeelement.remove()
htmx.offremoveEventListener()
htmx.valuesnew FormData(form)

htmx.onLoad: inicializar bibliotecas de terceros

El problema clásico: inicializas un datepicker al cargar la página, htmx inserta HTML nuevo y esos inputs no tienen datepicker. La solución:

htmx.onLoad((contenido) => {
  contenido.querySelectorAll(".datepicker").forEach(inicializarDatepicker);
});

onLoad se ejecuta al cargar la página y con cada fragmento insertado.

htmx.process: HTML creado a mano

Si insertas HTML con innerHTML desde tu propio JavaScript, htmx no lo conoce:

contenedor.innerHTML = '<button hx-get="/datos">Cargar</button>';
htmx.process(contenedor);   // ahora sí funciona

Cancelar y modificar

Cancelar una petición

htmx.on("htmx:before:request", (e) => {
  if (!usuarioAutenticado()) {
    e.preventDefault();
    mostrarLogin();
  }
});

Confirmación personalizada

hx-confirm usa el confirm() del navegador. Para un modal propio:

htmx.on("htmx:confirm", async (e) => {
  if (!e.detail.question) return;      // no hay hx-confirm, seguir normal
  e.preventDefault();
  if (await miModal(e.detail.question)) {
    e.detail.issueRequest();
  }
});

Modificar la respuesta antes del swap

htmx.on("htmx:after:request", (e) => {
  e.detail.ctx.text = e.detail.ctx.text.replace(/lorem/gi, "…");
});

Manejo global de errores

htmx.on("htmx:response:error", (e) => {
  const status = e.detail.ctx.response.status;
  if (status === 401) {
    window.location = "/login";
  } else if (status >= 500) {
    mostrarToast("Error del servidor. Intenta de nuevo.");
  }
});

htmx.on("htmx:error", () => {
  mostrarToast("Sin conexión con el servidor.");
});

El capítulo 12 cubre el manejo declarativo con hx-status, que suele ser preferible a este manejo imperativo.

Depuración

htmx.config.logAll = true;

Con eso, cada evento aparece en la consola con su detail. Es la forma más rápida de responder “¿por qué no hizo el swap?” o “¿qué parámetros mandó realmente?”.

Para observar un elemento concreto:

htmx.on(htmx.find("#mi-boton"), "htmx:before:request", console.log);

Convivencia con otras bibliotecas

htmx no es excluyente. Los patrones que funcionan:

NecesidadHerramienta
Estado reactivo local (contadores, toggles)hx-live (cap. 15) o Alpine.js
Comportamiento puntualhx-on
Inicializar widgets de terceroshtmx.onLoad
Lógica compleja de clienteUn módulo JS aparte + htmx.ajax()

Regla práctica: si estás escribiendo más de tres líneas dentro de un hx-on, muévelas a una función y llámala desde el atributo.

Errores comunes

SíntomaCausaSolución
El listener de htmx:afterRequest no disparaEl nombre cambió en htmx 4htmx:after:request
El widget JS no se inicializa en contenido nuevoSe inicializó una sola vezhtmx.onLoad
htmx.closest no existeEliminado en htmx 4element.closest()
El HTML insertado a mano no reaccionahtmx no lo procesóhtmx.process(contenedor)
await no funciona en hx-onVersión antiguahtmx 4 soporta handlers async

Resumen

  • Los eventos siguen el formato htmx:<fase>:<sistema>; los nombres camelCase de htmx 2 cambiaron.
  • hx-on:<evento> permite escuchar cualquier evento desde el propio elemento, con soporte async.
  • El objeto ctx da acceso mutable a request.body, request.headers, response y text.
  • Dentro de hx-on tienes find(), findAll() y timeout().
  • La API JS se redujo: lo que hace el DOM nativo fue eliminado.
  • htmx.onLoad es la pieza clave para integrar bibliotecas de terceros.
  • htmx.config.logAll = true es la herramienta de depuración principal.

Siguiente: UX: indicadores, sync y preservación →.