Introduccion a HTMX
Capitulo 7: Introduccion a HTMX
HTMX es una libreria que permite hacer peticiones AJAX, transiciones CSS y mas directamente desde atributos HTML. La idea central: el servidor devuelve HTML, no JSON.
Que es HTMX
HTMX extiende HTML con atributos que permiten:
- Hacer peticiones HTTP (GET, POST, PUT, DELETE) desde cualquier elemento
- Reemplazar partes de la pagina con la respuesta del servidor
- Todo sin escribir JavaScript
El modelo mental es diferente al de las SPAs: en vez de consumir APIs JSON y construir el DOM en el cliente, el servidor genera HTML parcial y HTMX lo inyecta donde indiques.
Ventajas sobre el modelo SPA
- Menos complejidad: no necesitas framework frontend, bundler ni estado en el cliente
- Servidor como fuente de verdad: toda la logica vive en Go
- Progresivo: los formularios siguen funcionando sin JavaScript (graceful degradation)
- Menos codigo: un atributo HTML reemplaza decenas de lineas de fetch + DOM manipulation
Incluir HTMX
Via CDN
<script src="https://unpkg.com/[email protected]"></script>
Archivo local (recomendado para produccion)
# Descargar y colocar en tu directorio de archivos estaticos
curl -o static/htmx.min.js https://unpkg.com/[email protected]/dist/htmx.min.js
package layouts
templ Base(title string) {
<!DOCTYPE html>
<html lang="es">
<head>
<title>{ title }</title>
</head>
<body>
{ children... }
<script src="/static/htmx.min.js"></script>
</body>
</html>
}
Atributos fundamentales
hx-get: hacer GET y reemplazar contenido
hx-get hace una peticion GET cuando se interactua con el elemento:
<button hx-get="/api/saludo">Cargar saludo</button>
Al hacer clic, HTMX hace GET a /api/saludo y reemplaza el contenido interno del boton con la respuesta.
Handler en Go:
func handleSaludo(w http.ResponseWriter, r *http.Request) {
fmt.Fprint(w, "<span>Hola desde el servidor!</span>")
}
hx-post: enviar datos sin reload
hx-post envia un formulario via POST sin recargar la pagina:
<form hx-post="/api/contacto">
<input type="text" name="nombre" placeholder="Tu nombre"/>
<input type="email" name="email" placeholder="[email protected]"/>
<button type="submit">Enviar</button>
</form>
El servidor recibe los datos como un formulario normal (r.FormValue).
hx-target: donde poner la respuesta
Por defecto, HTMX reemplaza el contenido del elemento que disparo la peticion. Con hx-target controlas donde va la respuesta:
<button hx-get="/api/usuarios" hx-target="#lista-usuarios">
Cargar usuarios
</button>
<div id="lista-usuarios">
<!-- Aqui aparecera la respuesta -->
</div>
hx-swap: como reemplazar el contenido
hx-swap controla la estrategia de reemplazo:
| Valor | Comportamiento |
|---|---|
innerHTML | Reemplaza el contenido interno (por defecto) |
outerHTML | Reemplaza el elemento completo |
beforeend | Agrega al final del elemento |
afterbegin | Agrega al inicio del elemento |
beforebegin | Inserta antes del elemento |
afterend | Inserta despues del elemento |
delete | Elimina el elemento target |
none | No hace swap (util para side effects) |
<button hx-get="/api/nuevo-item" hx-target="#lista" hx-swap="beforeend">
Agregar item
</button>
<ul id="lista">
<li>Item existente</li>
<!-- Los nuevos items se agregan al final -->
</ul>
Primer ejemplo completo
Un boton que carga contenido dinamico:
package pages
import "myapp/templates/layouts"
templ Home() {
@layouts.Base("Mi App") {
<h1>HTMX Demo</h1>
<button hx-get="/clicked" hx-swap="outerHTML">
Hazme clic
</button>
}
}
package components
templ ClickedMessage() {
<p class="success">El boton fue clickeado! Este HTML vino del servidor.</p>
}
Handler en Go:
func handleClicked(w http.ResponseWriter, r *http.Request) {
components.ClickedMessage().Render(r.Context(), w)
}
Al hacer clic, el boton se reemplaza por el mensaje. Sin JavaScript, sin JSON, sin DOM manipulation.
Formulario sin recargar la pagina
package pages
import "myapp/templates/layouts"
templ ContactPage() {
@layouts.Base("Contacto") {
<h1>Contacto</h1>
<form hx-post="/contacto" hx-target="#resultado" hx-swap="innerHTML">
<input type="text" name="nombre" placeholder="Nombre" required/>
<input type="email" name="email" placeholder="Email" required/>
<textarea name="mensaje" placeholder="Mensaje"></textarea>
<button type="submit">Enviar</button>
</form>
<div id="resultado"></div>
}
}
package components
templ ContactSuccess(nombre string) {
<div class="alert-success">
<p>Gracias { nombre }, tu mensaje fue enviado.</p>
</div>
}
templ ContactError(msg string) {
<div class="alert-error">
<p>Error: { msg }</p>
</div>
}
Handler:
func handleContact(w http.ResponseWriter, r *http.Request) {
nombre := r.FormValue("nombre")
email := r.FormValue("email")
if nombre == "" || email == "" {
components.ContactError("Todos los campos son requeridos").
Render(r.Context(), w)
return
}
// Procesar el contacto...
components.ContactSuccess(nombre).Render(r.Context(), w)
}
El patron: HTML parcial, no JSON
Este es el concepto mas importante. En una API REST tradicional:
Cliente: GET /api/usuarios
Servidor: {"users": [{"name": "Ana"}, {"name": "Luis"}]}
Cliente: Parsear JSON → Crear elementos DOM → Insertar en el DOM
Con HTMX:
Cliente: GET /usuarios (via hx-get)
Servidor: <li>Ana</li><li>Luis</li>
HTMX: Inserta el HTML directamente
El servidor es responsable de generar el HTML. El cliente solo lo muestra.
Como funciona internamente
Cuando HTMX procesa una peticion:
- Intercepta el evento (clic, submit, etc.)
- Hace una peticion AJAX al URL especificado
- Agrega el header
HX-Request: trueautomaticamente - Recibe la respuesta HTML del servidor
- Inserta el HTML segun la estrategia de
hx-swap
Puedes detectar si una peticion viene de HTMX en tu handler:
func handler(w http.ResponseWriter, r *http.Request) {
isHTMX := r.Header.Get("HX-Request") == "true"
if isHTMX {
// Devolver solo el fragmento HTML
components.UserList(users).Render(r.Context(), w)
return
}
// Devolver la pagina completa con layout
pages.UsersPage(users).Render(r.Context(), w)
}
Este patron permite que la misma ruta funcione tanto para navegacion normal como para peticiones HTMX.
Comparacion con fetch/JSON
Sin HTMX (JavaScript vanilla)
<button id="load-btn">Cargar datos</button>
<div id="resultado"></div>
<script>
document.getElementById('load-btn').addEventListener('click', async () => {
const response = await fetch('/api/datos');
const data = await response.json();
const html = data.items.map(item =>
`<div class="item"><h3>${item.title}</h3></div>`
).join('');
document.getElementById('resultado').innerHTML = html;
});
</script>
Con HTMX
<button hx-get="/datos" hx-target="#resultado">Cargar datos</button>
<div id="resultado"></div>
Menos codigo, mas declarativo, la logica de presentacion vive en el servidor.