Triggers: cuándo se dispara la petición
Triggers: cuándo se dispara la petición
hx-trigger responde a la pregunta cuándo. Si no lo escribes, htmx elige un valor por defecto
según el tipo de elemento.
| Elemento | Trigger por defecto |
|---|---|
<input>, <textarea>, <select> | change |
<form> | submit |
| Cualquier otro | click |
<!-- Estos dos son equivalentes -->
<button hx-get="/datos">Cargar</button>
<button hx-get="/datos" hx-trigger="click">Cargar</button>
Sintaxis básica
<div hx-post="/entrada" hx-trigger="mouseenter">Pasa el mouse</div>
Cualquier evento del DOM sirve: click, dblclick, focus, blur, input, change, keyup,
submit, mouseenter, mouseleave, touchstart, y también eventos personalizados que dispares
tú con htmx.trigger() o que llegue desde el servidor con el header HX-Trigger.
Varios triggers
Separados por coma:
<input hx-get="/buscar"
hx-trigger="input delay:500ms, keyup[key=='Enter'], search">
Cada uno con sus propios modificadores.
Modificadores
Los modificadores se escriben en HCON después del nombre del evento, separados por espacios.
| Modificador | Qué hace |
|---|---|
delay:<tiempo> | Espera antes de emitir; si el evento se repite, reinicia la cuenta (debounce) |
throttle:<tiempo> | Emite y descarta los eventos siguientes durante ese intervalo |
changed | Sólo si el valor del elemento cambió respecto a la última vez |
once | Sólo la primera vez |
from:<selector> | Escucha el evento en otro elemento |
target:<selector> | Sólo si el evento se originó en un descendiente que coincide |
consume | Detiene la propagación del evento |
queue:<modo> | Qué hacer si llega un evento con una petición en vuelo |
[expresión] | Filtro JavaScript que debe ser verdadero |
Los tiempos aceptan ms y s: delay:500ms, throttle:1s, delay:2s.
delay vs throttle
flowchart TD
subgraph D["delay:500ms (debounce)"]
D1[tecla] --> D2[tecla] --> D3[tecla] --> D4[500ms de silencio] --> D5[1 petición]
end
subgraph T["throttle:500ms"]
T1[tecla → petición] --> T2[tecla ignorada] --> T3[tecla ignorada] --> T4[500ms] --> T5[siguiente tecla → petición]
end
delayes lo que quieres para búsquedas mientras se escribe: espera a que el usuario pare.throttlees lo que quieres para eventos continuos comoscroll,mousemoveoresize: garantiza una frecuencia máxima.
La búsqueda incremental canónica
<input type="search"
name="q"
placeholder="Buscar..."
hx-get="/buscar"
hx-trigger="input changed delay:400ms, search"
hx-target="#resultados">
<div id="resultados"></div>
changed evita que se dispare cuando el usuario pulsa una flecha o borra y reescribe lo mismo.
El evento search es el que emite un <input type="search"> cuando el usuario pulsa la X de
limpiar.
from: escuchar en otro elemento
<!-- El div se recarga cuando cambia un input que está en otra parte del DOM -->
<div hx-get="/resumen"
hx-trigger="change from:#filtros"
hx-include="#filtros">
</div>
from acepta también selectores especiales:
<!-- Un atajo de teclado global -->
<div hx-get="/ayuda"
hx-trigger="keyup[key=='?'] from:body"
hx-target="#modal">
</div>
<!-- Escuchar en window -->
<div hx-get="/estado" hx-trigger="focus from:window"></div>
Filtros con expresiones JavaScript
Entre corchetes, después del nombre del evento. La expresión se evalúa con this apuntando al
elemento y event disponible:
<!-- Sólo con Enter -->
<input hx-post="/enviar" hx-trigger="keyup[key=='Enter']">
<!-- Sólo con Ctrl+Enter -->
<textarea hx-post="/guardar" hx-trigger="keyup[ctrlKey&&key=='Enter']"></textarea>
<!-- Sólo si una variable global lo permite -->
<button hx-get="/premium" hx-trigger="click[window.usuarioEsPremium]">Ver</button>
Cuidado con las comillas: usa comillas simples dentro del atributo delimitado por dobles, y
recuerda escapar && como && si tu motor de plantillas lo exige.
queue: peticiones concurrentes
Si el trigger se dispara mientras hay una petición en vuelo:
<div hx-get="/datos" hx-trigger="click queue:last">
| Valor | Comportamiento |
|---|---|
first | Guarda el primer evento en cola, descarta el resto |
last | Guarda el último (por defecto) |
all | Encola todos y los ejecuta en orden |
none | Descarta cualquier evento durante la petición |
Para casos más complejos, el capítulo 11 cubre hx-sync, que coordina peticiones entre
elementos distintos.
Triggers especiales
htmx define cuatro triggers que no son eventos del DOM.
load
Se dispara cuando el elemento entra al DOM y htmx lo procesa. Es la base del lazy loading:
<div hx-get="/panel/ventas" hx-trigger="load">
<span class="skeleton">Cargando ventas…</span>
</div>
Funciona también con contenido insertado por un swap anterior: htmx procesa lo nuevo y dispara
load en lo que corresponda. Eso permite encadenar cargas.
Combinado con delay, sirve para escalonar:
<div hx-get="/widget/1" hx-trigger="load"></div>
<div hx-get="/widget/2" hx-trigger="load delay:200ms"></div>
<div hx-get="/widget/3" hx-trigger="load delay:400ms"></div>
revealed
Se dispara cuando el elemento entra en el viewport por scroll. Es el scroll infinito:
<tr hx-get="/productos?pagina=3"
hx-trigger="revealed"
hx-swap="afterend">
<td colspan="3">Cargando más…</td>
</tr>
El servidor devuelve las filas siguientes más un nuevo <tr> centinela con pagina=4. Cuando no
quedan más resultados, devuelve las filas sin centinela y el scroll infinito termina solo.
sequenceDiagram
participant U as Usuario
participant C as Centinela (pág. N)
participant S as Servidor
U->>C: hace scroll y lo revela
C->>S: GET /productos?pagina=N
S-->>C: filas + centinela (pág. N+1)
Note over C: el centinela viejo fue reemplazado
U->>C: sigue scrolleando…
intersect
revealed con control fino, sobre IntersectionObserver:
<div hx-get="/noticias"
hx-trigger="intersect root:#contenedor threshold:0.5">
</div>
root:<selector>: el elemento contenedor respecto al que se mide (por defecto, el viewport).threshold:<0..1>: qué proporción del elemento debe ser visible.
every: polling
<div hx-get="/notificaciones" hx-trigger="every 2s"></div>
El polling se detiene solo cuando el elemento sale del DOM. Ese es el truco para el polling que
se apaga: cuando el servidor decide que ya no hace falta seguir consultando, devuelve un
fragmento sin el hx-trigger="every ...".
// El trabajo sigue en curso: devolvemos un fragmento que seguirá haciendo polling
if (trabajo.estado === "procesando") {
return html(`
<div id="estado" hx-get="/trabajos/${id}" hx-trigger="every 2s" hx-swap="outerHTML">
Procesando… ${trabajo.progreso}%
</div>`);
}
// Terminó: el fragmento ya no tiene trigger, el polling muere
return html(`<div id="estado" class="ok">Listo ✓</div>`);
También se puede condicionar el polling con un filtro:
<div hx-get="/estado" hx-trigger="every 5s [document.visibilityState==='visible']"></div>
Así no consultas mientras la pestaña está en segundo plano.
Disparar eventos desde el código o el servidor
Desde JavaScript:
htmx.trigger("#lista", "recargar");
<ul id="lista" hx-get="/items" hx-trigger="recargar from:body"></ul>
Desde el servidor, con el header de respuesta HX-Trigger:
return new Response(html, {
headers: {
"Content-Type": "text/html",
"HX-Trigger": "carritoActualizado",
},
});
Y en el HTML, cualquier elemento puede reaccionar:
<span id="contador"
hx-get="/carrito/contador"
hx-trigger="carritoActualizado from:body">0</span>
Este patrón —el servidor notifica, otros fragmentos se refrescan— es la alternativa idiomática al “event bus” de las SPA, y evita acoplar cada acción con todas las zonas que debe actualizar.
Errores comunes
| Síntoma | Causa | Solución |
|---|---|---|
| La búsqueda dispara en cada tecla | Falta delay | hx-trigger="input changed delay:400ms" |
| El polling nunca se detiene | El fragmento devuelto conserva el trigger | Devolver el fragmento final sin hx-trigger |
| El trigger personalizado no llega | Se emitió en otro elemento | Añadir from:body y disparar sobre body |
revealed dispara al cargar la página | El elemento ya está visible | Es el comportamiento correcto; usa intersect con threshold |
El filtro [...] da error | Comillas o && mal escapados | Usar comillas simples y && en plantillas |
Resumen
- El trigger por defecto es
changeen inputs,submiten formularios yclicken todo lo demás. delayes debounce (búsquedas);throttlees frecuencia máxima (scroll).changed,once,from,target,consumeyqueueafinan el comportamiento.- Los filtros
[expresión]permiten condicionar con JavaScript. - Triggers especiales:
load(lazy),revealed(scroll infinito),intersect(control fino),every Ns(polling). - El polling se apaga devolviendo un fragmento sin el trigger.
HX-Triggerdesde el servidor permite refrescar otros fragmentos sin acoplarlos.
Siguiente: Targets y selectores extendidos →.