HTMX Avanzado

Por: Artiko
htmxgowebavanzado

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

EventoCuando se dispara
htmx:beforeRequestAntes de enviar la peticion
htmx:afterRequestDespues de recibir la respuesta
htmx:beforeSwapAntes de insertar el HTML
htmx:afterSwapDespues de insertar el HTML
htmx:afterSettleDespues de que el DOM se estabiliza
htmx:responseErrorCuando 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")
}
HeaderEfecto
HX-RedirectRedirige el navegador a otra URL
HX-RefreshRecarga la pagina completa
HX-TriggerDispara un evento JS en el cliente
HX-RetargetCambia el target del swap
HX-ReswapCambia la estrategia de swap
HX-Push-UrlActualiza la URL del navegador

Anterior: Introduccion a HTMX | Siguiente: Integrando Go + Templ + HTMX