Triggers: cuándo se dispara la petición

Por: Artiko
htmxhtmx4hx-triggereventospolling

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.

ElementoTrigger por defecto
<input>, <textarea>, <select>change
<form>submit
Cualquier otroclick
<!-- 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.

ModificadorQué 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
changedSólo si el valor del elemento cambió respecto a la última vez
onceSó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
consumeDetiene 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
  • delay es lo que quieres para búsquedas mientras se escribe: espera a que el usuario pare.
  • throttle es lo que quieres para eventos continuos como scroll, mousemove o resize: 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 &amp;&amp; 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">
ValorComportamiento
firstGuarda el primer evento en cola, descarta el resto
lastGuarda el último (por defecto)
allEncola todos y los ejecuta en orden
noneDescarta 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íntomaCausaSolución
La búsqueda dispara en cada teclaFalta delayhx-trigger="input changed delay:400ms"
El polling nunca se detieneEl fragmento devuelto conserva el triggerDevolver el fragmento final sin hx-trigger
El trigger personalizado no llegaSe emitió en otro elementoAñadir from:body y disparar sobre body
revealed dispara al cargar la páginaEl elemento ya está visibleEs el comportamiento correcto; usa intersect con threshold
El filtro [...] da errorComillas o && mal escapadosUsar comillas simples y &amp;&amp; en plantillas

Resumen

  • El trigger por defecto es change en inputs, submit en formularios y click en todo lo demás.
  • delay es debounce (búsquedas); throttle es frecuencia máxima (scroll).
  • changed, once, from, target, consume y queue afinan 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-Trigger desde el servidor permite refrescar otros fragmentos sin acoplarlos.

Siguiente: Targets y selectores extendidos →.