Herencia explícita y HCON
Herencia explícita y HCON
Este es el cambio conceptual más grande de htmx 4. Si vienes de htmx 2, es lo primero que te va a romper el código; si empiezas de cero, es lo que hace que htmx 4 sea predecible.
El problema de la herencia implícita
htmx 2 copió el modelo de CSS: un atributo declarado en un ancestro afectaba a todos sus descendientes.
<!-- htmx 2 -->
<div hx-target="#resultado">
<button hx-get="/a">A</button> <!-- apunta a #resultado -->
<button hx-get="/b">B</button> <!-- apunta a #resultado -->
</div>
Cómodo en ejemplos pequeños. En una aplicación real, el problema es que no puedes saber qué hace
un elemento leyéndolo: para entender un <button hx-get="/a"> había que subir por todo el árbol
buscando ancestros con atributos hx-*. Y como el HTML se compone desde plantillas parciales, un
fragmento reutilizable heredaba cosas distintas según dónde lo incrustaras.
htmx 2 intentó parchearlo con hx-disinherit y hx-inherit, atributos que existían sólo para
apagar una herencia que nadie había pedido explícitamente.
La solución de htmx 4: :inherited
En htmx 4 nada se hereda salvo que lo pidas, con el modificador :inherited:
<!-- htmx 4 -->
<div hx-target:inherited="#resultado">
<button hx-get="/a">A</button> <!-- apunta a #resultado -->
<button hx-get="/b">B</button> <!-- apunta a #resultado -->
<button hx-get="/c" hx-target="#otro">C</button> <!-- gana el local -->
</div>
Reglas:
- Sin
:inherited, el atributo aplica sólo al elemento donde está escrito. - Con
:inherited, aplica al elemento y a todos sus descendientes. - Un valor local en el descendiente gana sobre el heredado.
- El ancestro más cercano gana sobre el más lejano.
flowchart TD
A["<body hx-target:inherited='#main'>"] --> B["<section hx-target:inherited='#panel'>"]
B --> C["<button hx-get='/x'><br/>→ #panel (ancestro más cercano)"]
B --> D["<button hx-get='/y' hx-target='#modal'><br/>→ #modal (local gana)"]
A --> E["<button hx-get='/z'><br/>→ #main"]
Qué atributos se pueden heredar
Cualquiera que tenga sentido aplicar a un subárbol:
<div hx-confirm:inherited="¿Estás seguro?">…</div>
<div hx-target:inherited="#panel">…</div>
<div hx-swap:inherited="outerHTML">…</div>
<div hx-boost:inherited="true">…</div>
<div hx-indicator:inherited="#spinner">…</div>
<div hx-headers:inherited='{"X-Tenant":"acme"}'>…</div>
<div hx-include:inherited="#filtros">…</div>
<div hx-sync:inherited="closest form">…</div>
Los que no se heredan son los que definen la petición en sí: hx-get, hx-post, hx-put,
hx-patch, hx-delete. Sería absurdo que todos los hijos de un div hicieran la misma petición.
:append: fusionar en vez de sustituir
A veces no quieres reemplazar el valor heredado sino sumarle el tuyo. Para eso está :append:
<div hx-headers:inherited='{"X-Tenant": "acme"}'>
<button hx-get="/api"
hx-headers:append='{"X-Request-ID": "123"}'>
Enviar
</button>
</div>
Esa petición manda los dos headers. Sin :append, el valor local habría reemplazado por
completo al heredado y X-Tenant se habría perdido.
Se puede combinar con :inherited para que la fusión también se propague:
<div hx-include:inherited="#campos-globales">
<form hx-include:inherited:append=".campos-extra">
<!-- Los descendientes incluyen ambos selectores -->
</form>
</div>
:append tiene sentido en atributos acumulativos: hx-headers, hx-vals, hx-include,
hx-trigger. En un hx-target no lo tiene: un destino es uno solo.
El patrón de layout
La forma idiomática de trabajar en htmx 4 es declarar la herencia en el layout, una sola vez:
<body hx-boost:inherited="true"
hx-target:inherited="#contenido"
hx-indicator:inherited="#barra-carga"
hx-headers:inherited='{"X-CSRF-Token": "abc123"}'>
<div id="barra-carga" class="htmx-indicator"></div>
<nav>
<a href="/inicio">Inicio</a>
<a href="/tareas">Tareas</a>
</nav>
<main id="contenido">…</main>
</body>
Con eso: toda la navegación es AJAX, todo cae en #contenido, todo muestra la misma barra de carga
y toda petición lleva el token CSRF. Los fragmentos que el servidor devuelva heredan esa
configuración al insertarse, porque la herencia se resuelve por posición en el DOM, no por el
momento en que llegó el HTML.
hx-config: configuración por subárbol
Además de la configuración global, htmx 4 permite configurar un subárbol concreto:
<div hx-config="timeout:5000 credentials:'include'">
<button hx-get="/api-lenta">Consultar</button>
</div>
Y por elemento:
<button hx-post="/subida-grande" hx-config="timeout:120000">Subir</button>
La precedencia es la esperable: elemento > subárbol > <meta> global > valores por defecto.
HCON en detalle
HCON (htmx Configuration Object Notation) es la sintaxis de pares clave:valor que aparece en
todo htmx 4. Existe porque escribir JSON dentro de un atributo HTML es incómodo: comillas
escapadas, llaves y comas por todos lados.
<!-- HCON -->
<div hx-swap="innerHTML swap:200ms settle:100ms scroll:top">
<!-- El mismo valor en JSON sería impracticable en un atributo -->
Reglas de sintaxis
<!-- Pares separados por espacio o coma -->
content="defaultSwap:outerHTML transitions:true"
content="defaultSwap:outerHTML, transitions:true"
<!-- Clave sola = true -->
hx-config="validate" <!-- {validate: true} -->
<!-- Booleanos y números sin comillas -->
hx-config="validate:false timeout:5000"
<!-- Strings con comillas -->
hx-config='credentials:"include"'
<!-- Anidamiento con punto -->
content="ws.reconnect:true ws.reconnectDelay:1s"
<!-- {ws: {reconnect: true, reconnectDelay: "1s"}} -->
<!-- Tiempos con unidad -->
hx-trigger="click delay:500ms throttle:1s"
JSON sigue funcionando
En cualquier atributo que acepte HCON puedes escribir JSON:
<button hx-vals='{"id": 42, "tags": ["a", "b"]}'>Enviar</button>
Usa JSON cuando necesites arrays u objetos anidados; usa HCON para todo lo demás.
Dónde aparece HCON
| Lugar | Ejemplo |
|---|---|
hx-swap | innerHTML swap:200ms settle:100ms |
hx-trigger | click delay:500ms throttle:1s from:body |
hx-config | timeout:5000 credentials:"include" |
hx-vals | token:"abc" reintentos:3 |
hx-headers | X-Tenant:"acme" |
hx-status | swap:innerHTML target:#errores |
<meta name="htmx-config"> | defaultSwap:outerHTML transitions:true |
Header HX-Location | path:"/tareas" target:"#main" |
Volver al comportamiento de htmx 2
Si estás migrando y no quieres reescribir todo de golpe:
<script>
htmx.config.implicitInheritance = true;
</script>
<script src="/js/htmx.min.js"></script>
O cargando la extensión de compatibilidad:
<script src="/js/htmx.min.js"></script>
<script src="/js/ext/htmx-2-compat.js"></script>
Es una muleta de transición, no un destino: con la herencia implícita activada pierdes justamente
la propiedad que hace mantenible el código en htmx 4. Existe además el evento
htmx:after:implicitInheritance, útil para auditar dónde se está aplicando herencia implícita
mientras migras.
Errores comunes
| Síntoma | Causa | Solución |
|---|---|---|
| Los botones apuntan a sí mismos tras migrar | El hx-target del contenedor ya no se hereda | Añadir :inherited |
| El header global desaparece en un botón concreto | El hx-headers local reemplazó al heredado | Usar hx-headers:append |
hx-get en un div no afecta a sus hijos | Los verbos no son heredables | Escribirlo en cada elemento |
| El fragmento nuevo no hereda nada | Está fuera del subárbol con :inherited | Revisar dónde se inserta |
| HCON no parsea un valor | Falta comillas en un string con caracteres especiales | clave:"valor con espacios" |
Resumen
- En htmx 4 no hay herencia salvo que la pidas con
:inherited. - El valor local siempre gana sobre el heredado; el ancestro más cercano gana sobre el lejano.
:appendfusiona el valor local con el heredado (headers, vals, include, trigger).- Los verbos de petición nunca se heredan.
- El patrón idiomático es declarar la herencia una vez en el layout.
hx-configconfigura un subárbol; la precedencia es elemento > subárbol > meta > defaults.- HCON es la sintaxis
clave:valorcompacta; JSON sigue disponible para estructuras complejas. implicitInheritanceyhtmx-2-compatson muletas de migración, no soluciones permanentes.
Siguiente: Partials, OOB y selección →.