Datos y formularios
Datos y formularios
Los formularios son el terreno natural de htmx: es donde el modelo hipermedia se nota más simple que el de una SPA. Este capítulo cubre cómo se arma la carga de una petición y cómo se validan los datos.
hx-vals: valores adicionales
Añade pares clave/valor a la petición sin necesidad de inputs ocultos:
<!-- HCON -->
<button hx-post="/api" hx-vals="prioridad:'alta' origen:'panel'">Enviar</button>
<!-- JSON -->
<button hx-post="/api" hx-vals='{"prioridad": "alta", "tags": ["urgente"]}'>Enviar</button>
Valores dinámicos con js:
Cuando el valor hay que calcularlo en el momento:
<button hx-post="/eventos"
hx-vals='js:{ancho: window.innerWidth, hora: new Date().toISOString()}'>
Registrar
</button>
El prefijo js: hace que htmx evalúe la expresión en cada petición. En htmx 4 esto reemplaza a
hx-vars, que fue eliminado.
Si tu sitio tiene una CSP estricta sin
unsafe-eval,js:no funcionará. La alternativa es el eventohtmx:config:request, que se ve en el capítulo 10.
hx-include: incluir valores de otros elementos
Por defecto, un elemento sólo envía sus propios valores (o los de su formulario). hx-include
suma los de cualquier otro elemento del documento:
<div id="filtros">
<select name="categoria">
<option value="todas">Todas</option>
<option value="libros">Libros</option>
</select>
<input type="checkbox" name="stock" value="1"> Sólo con stock
</div>
<button hx-get="/productos" hx-include="#filtros" hx-target="#resultados">
Buscar
</button>
Acepta los mismos selectores extendidos que hx-target:
<button hx-delete="/items/3" hx-include="closest form">Eliminar</button>
<button hx-post="/guardar" hx-include="next input">Guardar</button>
<button hx-post="/enviar" hx-include="#a, #b, .campos-extra">Enviar</button>
Cuando el selector apunta a un contenedor, htmx incluye todos los campos de formulario que haya dentro.
Recuerda el cambio de htmx 4: hx-delete ya no incluye automáticamente el formulario que lo
contiene. Si lo necesitas, hx-include="closest form".
hx-headers: headers personalizados
<button hx-get="/protegido" hx-headers='{"X-API-Key": "secreto"}'>Consultar</button>
El uso más común es el token CSRF, declarado una vez en el layout con herencia:
<body hx-headers:inherited='{"X-CSRF-Token": "{{ token }}"}'>
Y si un botón concreto necesita añadir otro header sin perder el heredado:
<button hx-post="/api" hx-headers:append='{"X-Request-ID": "abc"}'>Enviar</button>
hx-encoding y subida de archivos
Los formularios con archivos necesitan multipart/form-data:
<form hx-post="/subir"
hx-encoding="multipart/form-data"
hx-target="#resultado">
<input type="file" name="archivo" required>
<button type="submit">Subir</button>
</form>
<div id="resultado"></div>
En el servidor:
if (pathname === "/subir" && req.method === "POST") {
const datos = await req.formData();
const archivo = datos.get("archivo");
await Bun.write(`./uploads/${archivo.name}`, archivo);
return html(`<p>Subido: ${archivo.name} (${archivo.size} bytes)</p>`);
}
Indicador durante la subida
htmx 4 usa fetch(), que no expone eventos de progreso de subida como lo hacía XMLHttpRequest.
Para una subida grande, lo práctico es un indicador indeterminado más un timeout generoso:
<form hx-post="/subir"
hx-encoding="multipart/form-data"
hx-config="timeout:300000"
hx-indicator="#subiendo"
hx-target="#resultado">
<input type="file" name="archivo">
<button type="submit">Subir</button>
<progress id="subiendo" class="htmx-indicator"></progress>
</form>
Si necesitas un porcentaje real, el camino en htmx 4 es la extensión hx-multipart o un stream de
progreso del servidor con hx-sse, que se ven en el
capítulo 14.
Validación
htmx 4 se apoya en la validación nativa de HTML5. Un formulario con campos inválidos no se envía, sin que tengas que hacer nada:
<form hx-post="/registro" hx-target="#resultado">
<input type="email" name="email" required>
<input type="password" name="clave" minlength="8" required>
<button type="submit">Registrarse</button>
</form>
En htmx 4 se eliminaron los eventos htmx:validation:*: la validación es la del navegador, con
sus APIs estándar (setCustomValidity, :invalid, checkValidity()).
hx-validate fuera de un formulario
Un input suelto que dispara una petición no se valida por defecto. Con hx-validate="true", sí:
<input type="email"
name="email"
required
hx-post="/verificar-email"
hx-trigger="change"
hx-validate="true"
hx-target="#estado-email">
Validación en el servidor: el patrón completo
La validación real vive en el servidor. El patrón idiomático de htmx 4 usa el ciclo “devolver el formulario con los errores dentro”, combinado con morphing para no perder el foco:
<form id="registro"
hx-post="/registro"
hx-target="this"
hx-swap="outerMorph">
<div class="campo">
<label>Email</label>
<input name="email" value="">
</div>
<div class="campo">
<label>RUT</label>
<input name="rut" value="">
</div>
<button type="submit">Registrarse</button>
</form>
function renderFormulario(valores = {}, errores = {}) {
const campo = (nombre, etiqueta) => `
<div class="campo ${errores[nombre] ? "con-error" : ""}">
<label>${etiqueta}</label>
<input name="${nombre}" value="${escapar(valores[nombre] ?? "")}">
${errores[nombre] ? `<span class="error">${errores[nombre]}</span>` : ""}
</div>`;
return `
<form id="registro" hx-post="/registro" hx-target="this" hx-swap="outerMorph">
${campo("email", "Email")}
${campo("rut", "RUT")}
<button type="submit">Registrarse</button>
</form>`;
}
// POST /registro
const datos = Object.fromEntries(await req.formData());
const errores = validar(datos);
if (Object.keys(errores).length > 0) {
return new Response(renderFormulario(datos, errores), {
status: 422,
headers: { "Content-Type": "text/html" },
});
}
crearUsuario(datos);
return html(`<p class="ok">Cuenta creada ✓</p>`);
Dos detalles importantes:
- El
422se inserta. En htmx 2 había que interceptarhtmx:beforeSwappara permitirlo; en htmx 4 todos los códigos hacen swap salvo204y304. outerMorphen lugar deouterHTML: el usuario no pierde el cursor ni el scroll cuando vuelve el formulario con los errores.
sequenceDiagram
participant U as Usuario
participant F as Formulario
participant S as Servidor
U->>F: submit
F->>S: POST /registro
alt datos inválidos
S-->>F: 422 + formulario con errores
F->>F: outerMorph (foco intacto)
else datos válidos
S-->>F: 200 + confirmación
end
Validación en vivo por campo
<input name="rut"
hx-post="/validar/rut"
hx-trigger="change, blur"
hx-target="next .error"
hx-swap="innerHTML">
<span class="error"></span>
El servidor devuelve el mensaje o una cadena vacía. Recuerda swapEmpty:true si quieres que la
respuesta vacía limpie el mensaje anterior:
hx-swap="innerHTML swapEmpty:true"
Filtrar parámetros
hx-params fue eliminado en htmx 4. Para excluir parámetros de la petición se usa el evento
htmx:config:request:
<form hx-post="/guardar"
hx-on:htmx:config:request="ctx.request.body.delete('campo_interno')">
…
</form>
O globalmente:
htmx.on("htmx:config:request", (e) => {
e.detail.ctx.request.body.delete("_debug");
});
El cuerpo es un FormData estándar, así que tienes get, set, append, delete y entries.
Reset del formulario tras enviar
Un caso clásico: enviar un mensaje y limpiar el campo.
<form hx-post="/mensajes"
hx-target="#chat"
hx-swap="beforeend"
hx-on:htmx:after:request="this.reset()">
<input name="texto" autocomplete="off">
<button type="submit">Enviar</button>
</form>
Si quieres limpiarlo sólo cuando la petición fue exitosa:
hx-on:htmx:after:request="if (ctx.response.status < 400) this.reset()"
Errores comunes
| Síntoma | Causa | Solución |
|---|---|---|
| El archivo llega vacío | Falta hx-encoding | hx-encoding="multipart/form-data" |
El hx-delete no manda datos | Cambio de htmx 4 | hx-include="closest form" |
| El 422 no se ve | Se está usando config de htmx 2 (noSwap) | Revisar htmx.config.noSwap |
| Se pierde el cursor al validar | outerHTML recrea el input | outerMorph |
hx-vals con js: no evalúa | CSP sin unsafe-eval | Usar htmx:config:request |
hx-vars no funciona | Fue eliminado en htmx 4 | hx-vals con js: |
hx-params no funciona | Fue eliminado en htmx 4 | htmx:config:request + body.delete() |
Resumen
hx-valsañade valores; conjs:los calcula en el momento (reemplaza ahx-vars).hx-includesuma los campos de cualquier otro elemento, con selectores extendidos.hx-headerscon:inheritedes la forma limpia de propagar un token CSRF.hx-encoding="multipart/form-data"para subir archivos.- La validación de cliente es la nativa de HTML5;
hx-validatela activa fuera de formularios. - El patrón de validación de servidor es:
422+ formulario con errores +outerMorph. hx-paramsfue eliminado; se sustituye porhtmx:config:request.
Siguiente: Eventos y scripting →.