Eventos y scripting
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 2 | htmx 4 |
|---|---|
htmx:beforeRequest | htmx:before:request |
htmx:afterRequest | htmx:after:request |
htmx:beforeSwap | htmx:before:swap |
htmx:afterSwap | htmx:after:swap |
htmx:afterSettle | htmx:after:settle |
htmx:afterOnLoad | htmx:after:init |
htmx:configRequest | htmx:config:request |
htmx:responseError | htmx: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
| Evento | Cuándo | Uso típico |
|---|---|---|
htmx:config:request | Antes de armar la petición | Añadir/quitar params y headers |
htmx:confirm | Antes de confirmar la acción | Diálogo de confirmación personalizado |
htmx:before:request | Justo antes de enviar | Cancelar la petición |
htmx:before:response | Al recibir la respuesta, antes de procesarla | Inspeccionar status y headers |
htmx:after:request | Tras la respuesta | Reset de formularios, logging |
htmx:response:error | Respuesta con código de error | Manejo de errores |
htmx:error | Error de red o de htmx | Notificar al usuario |
htmx:abort | Petición abortada | Limpiar indicadores |
htmx:finally:request | Siempre, al final | Limpieza garantizada |
Ciclo de vida del swap
| Evento | Cuándo |
|---|---|
htmx:before:swap | Antes de insertar el HTML |
htmx:after:swap | Después de insertarlo |
htmx:before:settle | Antes de la fase de settle |
htmx:after:settle | Después del settle |
Ciclo de vida de los elementos
| Evento | Cuándo |
|---|---|
htmx:before:init / htmx:after:init | htmx inicializa un elemento |
htmx:before:process / htmx:after:process | htmx procesa un subárbol |
htmx:process:{tipo} | Procesamiento de un tipo concreto |
htmx:before:cleanup / htmx:after:cleanup | Un elemento sale del DOM |
htmx:after:implicitInheritance | Se 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.
| Propiedad | Contenido |
|---|---|
ctx.request.body | FormData con los parámetros (mutable) |
ctx.request.headers | Objeto de headers (mutable) |
ctx.response.status | Código HTTP de la respuesta |
ctx.text | Cuerpo de la respuesta (mutable en htmx:after:request) |
ctx.target | Elemento destino del swap |
ctx.newContent | Contenido 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ón | Qué 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étodo | Descripció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.config | Objeto de configuración |
htmx.version | Versión actual |
Eliminados en htmx 4
| Eliminado | Reemplazo |
|---|---|
htmx.addClass/removeClass/toggleClass | element.classList.* |
htmx.closest | element.closest() |
htmx.remove | element.remove() |
htmx.off | removeEventListener() |
htmx.values | new 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:
| Necesidad | Herramienta |
|---|---|
| Estado reactivo local (contadores, toggles) | hx-live (cap. 15) o Alpine.js |
| Comportamiento puntual | hx-on |
| Inicializar widgets de terceros | htmx.onLoad |
| Lógica compleja de cliente | Un 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íntoma | Causa | Solución |
|---|---|---|
El listener de htmx:afterRequest no dispara | El nombre cambió en htmx 4 | htmx:after:request |
| El widget JS no se inicializa en contenido nuevo | Se inicializó una sola vez | htmx.onLoad |
htmx.closest no existe | Eliminado en htmx 4 | element.closest() |
| El HTML insertado a mano no reacciona | htmx no lo procesó | htmx.process(contenedor) |
await no funciona en hx-on | Versión antigua | htmx 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 soporteasync.- El objeto
ctxda acceso mutable arequest.body,request.headers,responseytext. - Dentro de
hx-ontienesfind(),findAll()ytimeout(). - La API JS se redujo: lo que hace el DOM nativo fue eliminado.
htmx.onLoades la pieza clave para integrar bibliotecas de terceros.htmx.config.logAll = truees la herramienta de depuración principal.
Siguiente: UX: indicadores, sync y preservación →.