HTMX : Recherche en temps réel
Comment j’ai implémenté une recherche instantanée sur 1000 contacts sans écrire de JavaScript.
Ce que je voulais
Une barre de recherche qui filtre ma liste de contacts pendant que je tape. Classique. Avec React, j’aurais fait un state pour la query, un useEffect pour debounce, un appel API, puis mise à jour du state des résultats.
Avec HTMX, c’est 3 attributs.
Le HTML
<input type="search"
name="q"
placeholder="Rechercher..."
hx-get="/contacts"
hx-target="#results"
hx-trigger="keyup delay:500ms changed, change"
hx-indicator="#spinner">
<span id="spinner" class="htmx-indicator">...</span>
<div id="results">
<!-- les résultats arrivent ici -->
</div>
Décomposons :
hx-get="/contacts": envoie une requête GET à /contacts avec la valeur du champhx-target="#results": injecte la réponse dans ce divhx-trigger="keyup delay:500ms changed": déclenche 500ms après la dernière frappehx-indicator="#spinner": affiche ce spinner pendant le chargement
Le delay:500ms est important. Sans ça, chaque frappe envoie une requête. Avec 1000 contacts et un utilisateur qui tape vite, ça peut vite saturer.
Le serveur (Go/Gin)
func getContacts(c *gin.Context) {
query := c.Query("q")
var contacts []Contact
if query != "" {
contacts = searchContacts(query) // filtre sur nom, email, tel
} else {
contacts = allContacts
}
// HTMX envoie ce header
if c.Request.Header.Get("Hx-Request") == "true" {
// Requête HTMX : on renvoie juste le fragment
c.HTML(http.StatusOK, "contacts-list.html", gin.H{
"Contacts": contacts,
"Query": query,
})
return
}
// Requête normale : page complète
c.HTML(http.StatusOK, "index.html", gin.H{
"Contacts": contacts,
"Query": query,
})
}
Le header Hx-Request permet de distinguer une requête HTMX d’une requête classique. Si quelqu’un désactive JavaScript ou accède directement à l’URL, il a quand même la page complète.
Le template du fragment
<!-- contacts-list.html -->
<table>
{{ range .Contacts }}
<tr>
<td>{{ .FirstName }} {{ .LastName }}</td>
<td>{{ .Email }}</td>
<td>{{ .Phone }}</td>
</tr>
{{ else }}
<tr>
<td colspan="3">Aucun résultat pour "{{ .Query }}"</td>
</tr>
{{ end }}
</table>
Rien de spécial. Du HTML classique avec du templating Go.
Le truc que j’ai découvert : Out-of-Band Swaps
Au début, ma recherche marchait mais le compteur “X contacts trouvés” ne se mettait pas à jour. Normal : il était en dehors du #results.
HTMX a une solution : les Out-of-Band Swaps. On peut mettre à jour plusieurs éléments avec une seule réponse.
<!-- Dans la réponse du serveur -->
<span id="count" hx-swap-oob="true">
{{ len .Contacts }} contact(s)
</span>
<table>
<!-- ... le tableau normal ... -->
</table>
L’attribut hx-swap-oob="true" dit à HTMX : “cet élément, tu le remplaces là où il y a le même ID dans la page, pas dans la target principale”.
J’utilise ça pour mettre à jour le compteur et les contrôles de pagination en même temps que les résultats.
Mise à jour de l’URL
Détail pratique : je voulais que l’URL reflète la recherche pour pouvoir partager un lien filtré.
<input ... hx-push-url="true">
Quand je tape “dupont”, l’URL devient /contacts?q=dupont. Si quelqu’un ouvre ce lien, il voit directement les résultats filtrés.
Performance
Avec 1000 contacts, la recherche est instantanée. Le serveur filtre en mémoire, génère le HTML, et l’envoie. Pas de parsing JSON côté client, pas de re-render React. Le navigateur reçoit du HTML prêt à afficher.
Sur des datasets plus gros, il faudrait paginer les résultats ou ajouter un vrai moteur de recherche. Mais pour une app interne avec quelques milliers d’entrées, ça suffit largement.
Le code complet
Tout est dans le repo : git.sr.ht/~gabrielivanes/htmx_todo
Articles liés :