Capitulo 8: HTMX Avanzado
En este capitulo exploramos los atributos y patrones avanzados de HTMX que permiten crear experiencias ricas e interactivas manteniendo la simplicidad del modelo servidor-centrico.
hx-trigger: personalizar cuando se dispara
Por defecto, HTMX usa el evento natural del elemento (clic para botones, submit para formularios). Con hx-trigger puedes controlar exactamente cuando se ejecuta la peticion.
Eventos comunes
<!-- Al hacer clic (por defecto en botones) -->
<button hx-get="/datos" hx-trigger="click">Cargar</button>
<!-- Al soltar una tecla en un input -->
<input hx-get="/buscar" hx-trigger="keyup" name="q" hx-target="#resultados"/>
<!-- Al cargar el elemento en la pagina -->
<div hx-get="/noticias" hx-trigger="load">Cargando noticias...</div>
<!-- Cuando el elemento se vuelve visible en el viewport -->
<div hx-get="/mas-items" hx-trigger="revealed">
Cargando mas contenido...
</div>
Modificadores de trigger
<!-- Ejecutar solo una vez -->
<div hx-get="/banner" hx-trigger="load once">Cargando...</div>
<!-- Esperar 500ms despues de que el usuario deje de escribir -->
<input hx-get="/buscar" hx-trigger="keyup changed delay:500ms"
name="q" hx-target="#resultados"/>
<!-- Polling: repetir cada 2 segundos -->
<div hx-get="/notificaciones" hx-trigger="every 2s">
Sin notificaciones
</div>
El modificador changed asegura que solo se dispare si el valor realmente cambio. Combinado con delay, crea un debounce ideal para busqueda en tiempo real.
hx-indicator: mostrar loading
hx-indicator muestra un elemento durante la peticion:
<style>
.htmx-indicator { display: none; }
.htmx-request .htmx-indicator,
.htmx-request.htmx-indicator { display: inline; }
</style>
<button hx-get="/datos-lentos" hx-indicator="#spinner">
Cargar datos
<span id="spinner" class="htmx-indicator">⏳ Cargando...</span>
</button>
Cuando la peticion esta en curso, HTMX agrega la clase htmx-request al elemento que la disparo. Esto hace visible al indicador.
Indicator con CSS personalizado
<style>
.htmx-indicator {
opacity: 0;
transition: opacity 200ms ease-in;
}
.htmx-request .htmx-indicator {
opacity: 1;
}
</style>
<div>
<button hx-get="/api/reporte" hx-target="#reporte" hx-indicator=".loader">
Generar reporte
</button>
<div class="loader htmx-indicator">
<div class="spinner"></div>
Generando...
</div>
</div>
<div id="reporte"></div>
hx-push-url: navegacion SPA-like
hx-push-url actualiza la URL del navegador sin recargar la pagina, creando una experiencia similar a una SPA:
<nav>
<a hx-get="/pagina/inicio" hx-target="#contenido" hx-push-url="true">
Inicio
</a>
<a hx-get="/pagina/productos" hx-target="#contenido" hx-push-url="true">
Productos
</a>
<a hx-get="/pagina/contacto" hx-target="#contenido" hx-push-url="true">
Contacto
</a>
</nav>
<main id="contenido">
<!-- Contenido dinamico -->
</main>
El usuario puede usar los botones atras/adelante del navegador y la URL refleja la pagina actual. El handler debe detectar si es peticion HTMX para devolver solo el fragmento o la pagina completa.
hx-confirm: confirmacion antes de ejecutar
Ideal para acciones destructivas como eliminar registros:
<button hx-delete="/api/usuario/42"
hx-confirm="Estas seguro de eliminar este usuario?"
hx-target="closest tr"
hx-swap="outerHTML">
Eliminar
</button>
HTMX muestra un confirm() nativo del navegador. Si el usuario cancela, la peticion no se ejecuta.
Tabla con eliminar
package components
import "fmt"
type User struct {
ID int
Name string
}
templ UserRow(user User) {
<tr id={ fmt.Sprintf("user-%d", user.ID) }>
<td>{ user.Name }</td>
<td>
<button
hx-delete={ fmt.Sprintf("/api/users/%d", user.ID) }
hx-confirm="Eliminar este usuario?"
hx-target="closest tr"
hx-swap="outerHTML swap:500ms"
>
Eliminar
</button>
</td>
</tr>
}
El swap:500ms agrega un delay antes del swap, util para animaciones CSS de salida.
hx-vals: enviar valores extra
hx-vals permite enviar datos adicionales con la peticion sin campos de formulario:
<!-- Valores estaticos (JSON) -->
<button hx-post="/api/accion"
hx-vals='{"tipo": "premium", "origen": "landing"}'>
Activar Premium
</button>
<!-- Valores dinamicos con JavaScript -->
<button hx-post="/api/evento"
hx-vals="js:{timestamp: Date.now(), timezone: Intl.DateTimeFormat().resolvedOptions().timeZone}">
Registrar evento
</button>
hx-headers: headers personalizados
<!-- Enviar headers personalizados -->
<div hx-get="/api/datos"
hx-headers='{"X-Custom-Header": "valor", "Accept-Language": "es"}'>
Cargar
</div>
Util para enviar tokens CSRF o identificadores de sesion:
package components
templ SecureForm(csrfToken string) {
<form hx-post="/api/datos"
hx-headers={ fmt.Sprintf(`{"X-CSRF-Token": "%s"}`, csrfToken) }>
<input type="text" name="dato"/>
<button type="submit">Enviar</button>
</form>
}
Eventos HTMX
HTMX emite eventos en cada fase del ciclo de vida de una peticion:
<script>
// Antes de enviar la peticion
document.body.addEventListener('htmx:beforeRequest', function(evt) {
console.log('Enviando peticion a:', evt.detail.pathInfo.requestPath);
});
// Despues de insertar el nuevo contenido
document.body.addEventListener('htmx:afterSwap', function(evt) {
console.log('Contenido actualizado');
});
// Si la peticion falla
document.body.addEventListener('htmx:responseError', function(evt) {
console.error('Error:', evt.detail.xhr.status);
});
</script>
Eventos mas usados
| Evento | Cuando se dispara |
|---|---|
htmx:beforeRequest | Antes de enviar la peticion |
htmx:afterRequest | Despues de recibir la respuesta |
htmx:beforeSwap | Antes de insertar el HTML |
htmx:afterSwap | Despues de insertar el HTML |
htmx:afterSettle | Despues de que el DOM se estabiliza |
htmx:responseError | Cuando el servidor responde con error |
CSS transitions con HTMX
HTMX aplica clases CSS durante el proceso de swap que puedes usar para animaciones:
<style>
/* Elemento que esta siendo reemplazado */
.htmx-swapping {
opacity: 0;
transition: opacity 300ms ease-out;
}
/* Nuevo contenido que se esta asentando */
.htmx-settling {
opacity: 0;
}
.htmx-added {
opacity: 0;
transition: opacity 300ms ease-in;
}
</style>
<div hx-get="/contenido" hx-swap="innerHTML settle:300ms">
Contenido original
</div>
Con settle:300ms le das tiempo a la transicion CSS antes de que HTMX considere el swap completo.
hx-boost: convertir enlaces en HTMX
hx-boost transforma enlaces y formularios normales en peticiones HTMX automaticamente:
<body hx-boost="true">
<!-- Todos los enlaces dentro del body ahora usan HTMX -->
<nav>
<a href="/inicio">Inicio</a>
<a href="/productos">Productos</a>
</nav>
<main id="content">
<!-- El contenido se actualiza via HTMX -->
</main>
</body>
Con hx-boost, los enlaces hacen peticiones AJAX y reemplazan el <body> con la respuesta, manteniendo la URL actualizada. Es la forma mas rapida de hacer que un sitio multi-pagina se sienta como una SPA.
Out-of-band swaps (hx-swap-oob)
A veces una accion necesita actualizar multiples partes de la pagina. Con hx-swap-oob puedes incluir elementos extra en la respuesta que se insertan en sus posiciones correspondientes:
func handleAddToCart(w http.ResponseWriter, r *http.Request) {
// Logica para agregar al carrito...
cartCount := getCartCount()
// Respuesta principal: confirmacion
fmt.Fprint(w, `<div class="alert">Producto agregado!</div>`)
// Out-of-band: actualizar el contador del carrito en el header
fmt.Fprintf(w,
`<span id="cart-count" hx-swap-oob="true">%d</span>`,
cartCount,
)
}
El HTML principal se inserta donde indica hx-target. Los elementos con hx-swap-oob="true" se insertan donde encuentren un elemento con el mismo id, sin importar donde esten en la pagina.
Ejemplo con Templ
package components
import "fmt"
templ CartNotification(message string, count int) {
<div class="alert-success">{ message }</div>
<span id="cart-count" hx-swap-oob="true">
{ fmt.Sprintf("%d", count) }
</span>
}
Detectar peticiones HTMX desde Go
El header HX-Request permite diferenciar peticiones normales de HTMX:
func isHTMX(r *http.Request) bool {
return r.Header.Get("HX-Request") == "true"
}
func handleProducts(w http.ResponseWriter, r *http.Request) {
products := getProducts()
if isHTMX(r) {
// Solo el fragmento HTML
components.ProductList(products).Render(r.Context(), w)
return
}
// Pagina completa con layout
pages.ProductsPage(products).Render(r.Context(), w)
}
Headers de respuesta HTMX
El servidor tambien puede enviar headers que HTMX interpreta:
func handleAction(w http.ResponseWriter, r *http.Request) {
// Redirigir via HTMX (sin reload completo)
w.Header().Set("HX-Redirect", "/dashboard")
// O recargar la pagina completa
w.Header().Set("HX-Refresh", "true")
// O disparar un evento en el cliente
w.Header().Set("HX-Trigger", "showNotification")
}
| Header | Efecto |
|---|---|
HX-Redirect | Redirige el navegador a otra URL |
HX-Refresh | Recarga la pagina completa |
HX-Trigger | Dispara un evento JS en el cliente |
HX-Retarget | Cambia el target del swap |
HX-Reswap | Cambia la estrategia de swap |
HX-Push-Url | Actualiza la URL del navegador |
Anterior: Introduccion a HTMX | Siguiente: Integrando Go + Templ + HTMX