Catálogo de extensiones

Por: Artiko
htmxhtmx4extensioneshx-livehx-optimistichx-preload

Catálogo de extensiones

htmx 4 mantiene el core pequeño y mueve todo lo demás a extensiones. La API de extensiones también creció: ahora pueden intervenir en todo el ciclo de petición, respuesta y swap, e incluso reemplazar la implementación de fetch().

Cómo se cargan (cambió en htmx 4)

hx-ext fue eliminado. Ahora basta con incluir el script de la extensión:

<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/htmx.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/ext/hx-preload.js"></script>

Y opcionalmente declarar cuáles se permiten, como medida de seguridad:

<meta name="htmx-config" content='extensions:"hx-preload, hx-sse"'>

Con ese allowlist, un script inyectado no puede registrar una extensión maliciosa.

flowchart LR
    A["htmx 2: hx-ext='sse' en el HTML"] --> B["htmx 4: incluir el script<br/>+ allowlist en la meta"]

Red

ExtensiónQué hace
hx-sseStream de HTML con text/event-stream
hx-wsHTML bidireccional sobre WebSockets
hx-multipartRespuestas en partes con multipart/mixed

Cubiertas en el capítulo 14.

Experiencia de usuario

ExtensiónQué hace
hx-liveBindings reactivos sobre atributos, clases, texto y estilos
hx-optimisticMuestra el resultado esperado antes de la respuesta
hx-browser-indicatorIndica actividad en el título/favicon de la pestaña
hx-promptPide un valor al usuario antes de la petición

Rendimiento

ExtensiónQué hace
hx-preloadPrecarga al pasar el mouse
hx-ptagEvita polling innecesario
hx-history-cacheRestaura páginas desde sessionStorage

Swaps

ExtensiónQué hace
hx-headFusiona etiquetas del <head> de la respuesta
hx-upsertActualiza o inserta según exista el elemento
hx-targetsSelecciona múltiples destinos
hx-downloadDescarga archivos desde una petición htmx

Compatibilidad y seguridad

ExtensiónQué hace
htmx-2-compatRestaura comportamientos de htmx 2
hx-alpine-compatConvivencia con Alpine.js
hx-cspFuncionamiento bajo CSP estricta

hx-live: reactividad declarativa

La más ambiciosa. Añade bindings reactivos al HTML, cubriendo el hueco que muchos proyectos llenaban con Alpine.js.

<script src="/js/ext/hx-live.min.js"></script>

Bindings de atributo

<input id="nombre">
<button :disabled="!q('#nombre').value">Enviar</button>

El botón se habilita solo cuando el input tiene contenido. Sin listeners, sin estado.

Clases

<!-- Forma de objeto -->
<div :class="{ alerta: edad < 18, ok: edad >= 18 }"></div>

<!-- Una clase concreta -->
<p :.advertencia="saldo < 0">Saldo negativo</p>

Texto y HTML

<input id="cantidad" type="number" value="1">
<p :text="q('#cantidad').valueAsNumber * 4990"></p>
<div :html="`<b>${valor}</b>`"></div>

Estilos

<input id="pct" type="range" value="50">
<div :style="{ width: q('#pct').value + '%', backgroundColor: '#ec4899' }"></div>

Escape hatch: hx-live

Para lógica que no cabe en una expresión:

<div hx-live="
  let termino = q('input').value;
  if (!termino) return;
  await debounce(250);
  this.textContent = await fetch('/buscar?q=' + termino).then(r => r.text());
"></div>

API pública

FunciónQué hace
htmx.live.q(selector)Selecciona elementos con lectura/escritura reactiva
htmx.live.attr(selector, nombre, valor?)Lee o escribe atributos
htmx.live.take(clase, ámbito?)Mueve una clase de un elemento a otro (tabs, selección)
htmx.live.forEvent(...)Vincula a un evento
htmx.live.nextFrame()Espera al siguiente frame
htmx.live.debounce(ms)Debounce dentro de expresiones
htmx.live.refresh()Fuerza el recálculo

Cuando cambias estado desde fuera del DOM, avisa manualmente:

window.estadoApp = "cargando";
htmx.live.refresh();

Combinado con htmx

<form :aria-busy="matches('.htmx-request')" hx-post="/guardar">
  <input name="email">
  <button type="submit">Guardar</button>
</form>

El formulario expone su estado de carga a los lectores de pantalla sin JavaScript propio.

htmx.live.take resuelve el patrón de pestañas de manera elegante:

<nav>
  <button class="activa" hx-on:click="htmx.live.take('activa')" hx-get="/tab/1" hx-target="#panel">Uno</button>
  <button hx-on:click="htmx.live.take('activa')" hx-get="/tab/2" hx-target="#panel">Dos</button>
</nav>
<div id="panel"></div>

hx-optimistic: UI optimista

Muestra el resultado esperado inmediatamente y lo reemplaza cuando llega la respuesta real.

<script src="/js/ext/hx-optimistic.js"></script>
<meta name="htmx-config" content='extensions:"hx-optimistic"'>

<ul id="mensajes">
  <li>Hola mundo</li>
</ul>

<template id="tpl-enviando">
  <li>Enviando…</li>
</template>

<form hx-post="/mensajes"
      hx-target="#mensajes"
      hx-swap="beforeend"
      hx-optimistic="#tpl-enviando">
  <input name="cuerpo" placeholder="Mensaje…">
  <button type="submit">Enviar</button>
</form>

La extensión expone los parámetros de la petición como atributos data-* en el template instanciado, así que la vista previa puede mostrar lo que el usuario escribió, no un texto genérico.

Estilo del estado provisional:

.hx-optimistic {
  opacity: .6;
  font-style: italic;
}
sequenceDiagram
    participant U as Usuario
    participant L as Lista
    participant S as Servidor
    U->>L: submit
    L->>L: inserta el template (clase hx-optimistic)
    L->>S: POST /mensajes
    S-->>L: <li> real
    L->>L: reemplaza la vista previa

Úsalo sólo cuando la operación casi nunca falla. Si falla a menudo, ver aparecer y desaparecer contenido es peor que esperar.

hx-preload

<a href="/tareas" hx-preload>Tareas</a>

Carga el destino al pasar el mouse. Para la navegación principal es una de las mejoras de percepción de velocidad más baratas que existen.

hx-head: fusionar el <head>

Cuando navegas con boost, la respuesta puede traer un <head> con estilos o metadatos distintos. hx-head los fusiona con el <head> actual en lugar de ignorarlos: útil para meta tags de Open Graph, hojas de estilo específicas de una sección o scripts por página.

hx-upsert

Da acceso a la estrategia de “actualizar si existe, insertar si no” para elementos identificados por id. Simplifica los endpoints que no saben si la operación fue creación o edición.

hx-csp: Content Security Policy estricta

Con una CSP sin unsafe-inline ni unsafe-eval, los atributos hx-on, los hx-vals con js: y las expresiones de hx-live dejan de funcionar. hx-csp proporciona la ruta compatible.

Complementa la opción de configuración inlineScriptNonce, que permite pasar el nonce de tu CSP a los scripts que htmx procesa. Se cubre en el capítulo 18.

htmx-2-compat y hx-alpine-compat

htmx-2-compat restaura de una sola vez: herencia implícita, nombres de eventos antiguos y el comportamiento previo de no hacer swap ante errores. Es la red de seguridad para migrar por partes.

hx-alpine-compat resuelve el conflicto de sintaxis entre hx-live y Alpine.js, que usan el mismo prefijo : para bindings. Si tu proyecto ya usa Alpine y quieres incorporar hx-live gradualmente, esta es la pieza.

Escribir tu propia extensión

htmx.registerExtension("mi-extension", {
  onEvent(nombre, evento) {
    if (nombre === "htmx:config:request") {
      evento.detail.ctx.request.headers["X-Traza"] = crypto.randomUUID();
    }
  },
});

En htmx 4 las extensiones tienen acceso a HCON para parsear su propia configuración, y pueden sustituir la función de red, lo que abre la puerta a caché offline, reintentos con backoff o transporte alternativo.

Elegir extensiones

flowchart TD
    A{¿Qué te falta?} --> B[Reactividad en el cliente]
    A --> C[Sensación de instantaneidad]
    A --> D[Tiempo real]
    A --> E[Compatibilidad]
    B --> B1["hx-live"]
    C --> C1["hx-preload + hx-optimistic"]
    D --> D1["hx-sse / hx-ws"]
    E --> E1["htmx-2-compat / hx-alpine-compat / hx-csp"]

Recomendación: no cargues extensiones “por si acaso”. Cada una es JavaScript que se descarga y ejecuta. Empieza sin ninguna y añade cuando tengas el problema concreto delante.

Errores comunes

SíntomaCausaSolución
hx-ext="sse" no hace nadahx-ext fue eliminado en htmx 4Incluir el script de la extensión
La extensión no se activaNo está en el allowlistAñadirla a extensions: en la meta
Conflicto de : con AlpineAmbos usan el mismo prefijohx-alpine-compat
hx-live no reacciona a un cambio externoEl estado no está en el DOMhtmx.live.refresh()
La vista optimista se queda pegadaLa petición falló sin respuestaManejar htmx:error

Resumen

  • hx-ext desapareció: cargar el script basta, y extensions: en la meta actúa como allowlist.
  • hx-live aporta reactividad declarativa: :attr, :class, :text, :html, :style y la API htmx.live.*.
  • hx-optimistic muestra el resultado antes de la respuesta usando un <template>.
  • hx-preload precarga al hover; hx-head fusiona metadatos; hx-upsert simplifica crear/editar.
  • htmx-2-compat, hx-alpine-compat y hx-csp cubren compatibilidad.
  • Las extensiones de htmx 4 pueden interceptar todo el ciclo e incluso reemplazar fetch().

Siguiente: Proyecto: CRUD completo →.