Créer une autocomplétion native dans les formulaires Rails avec f.datalist (sans JavaScript)
Note: f.datalist arrive dans Rails 8.2. C’est mergé dans main mais pas encore publié. Vous pouvez consulter la pull request, pointer votre Gemfile vers la branche main pour l’essayer, ou cloner l’application de démonstration et lancer bin/rails server. Si vous préférez ne pas attendre, le datalist_tag sous-jacent est disponible depuis Rails 8.0 ; nous y reviendrons plus bas. À voir aussi : les attributs JSON typés avec has_json, un autre ajout de la 8.2.
Il existe un écart agaçant entre « je veux des suggestions dans ce champ texte » et « je viens d’ajouter une dépendance JavaScript, une action de contrôleur et un contrôleur Stimulus ». Le navigateur propose une réponse native depuis des années, l’élément <datalist>, et Rails 8.2 ajoute un helper FormBuilder pour l’utiliser.
Soyons clairs, ce n’est pas une nouvelle capacité. Vous pouviez déjà rendre un datalist avec datalist_tag (Rails 8.0), ou avec du HTML brut avant cela. Ce que f.datalist apporte, c’est le câblage de l’id, pour que le champ et sa liste ne se désynchronisent pas. C’est peu de chose, mais c’est un bug que j’ai réellement mis en production, et le correctif tient en deux lignes au lieu de trois.
Nous allons construire un formulaire d’inscription avec un champ pays qui suggère des correspondances au fur et à mesure de la frappe. Pas de bibliothèque JS, pas de fetch, pas d’état côté client.
Ce que nous construisons
Un formulaire Profile avec un champ country adossé à une liste de suggestions native. Le navigateur gère le typeahead, et nous ne quittons jamais le bloc form_with.
La fonctionnalité Rails : f.datalist
f.datalist enveloppe le datalist_tag existant et dérive l’id de l’élément afin que le champ et la liste restent reliés :
# actionview/lib/action_view/helpers/form_options_helper.rb
def datalist(method, choices = nil, html_options = {})
@template.datalist_tag(field_id(method, "datalist"), choices, html_options)
end
L’élément clé est field_id(method, "datalist"). Un <datalist> ne fonctionne que lorsque l’attribut list du champ correspond à l’id du datalist. Voici le même formulaire sans le helper :
<%# L'id est une chaîne littérale que vous tapez désormais à l'identique à deux endroits %>
<%= f.text_field :country, list: "profile_country_datalist" %>
<%= datalist_tag "profile_country_datalist", Profile::COUNTRIES %>
Ça marche, mais l’id est une chaîne en dur à deux endroits. Renommez le champ ou le modèle et ils divergent, et l’autocomplétion casse silencieusement, sans aucune erreur. field_id construit le même id à partir du nom du champ des deux côtés, donc ils correspondent toujours.
datalist_tag est présent dans les versions publiées de Rails depuis la 8.0, donc la version manuelle fonctionne dès aujourd’hui sans Rails de edge. f.datalist se contente de supprimer la gestion manuelle de l’id.
La construction
Commencez par un modèle simple :
class Profile < ApplicationRecord
validates :country, presence: true
end
Puis le formulaire. Les deux lignes qui comptent sont le text_field et le datalist :
<%= form_with model: @profile do |f| %>
<div>
<%= f.label :country %>
<%= f.text_field :country, list: f.field_id(:country, :datalist) %>
<%= f.datalist :country, ["Argentina", "Brazil", "Canada", "Chile", "Japan"] %>
</div>
<%= f.submit %>
<% end %>
Le list: du champ et l’id produit par f.datalist valent tous deux field_id(:country, :datalist), donc le navigateur les associe.
Cela produit le rendu suivant :
<input list="profile_country_datalist" type="text"
name="profile[country]" id="profile_country" />
<datalist id="profile_country_datalist">
<option value="Argentina">Argentina</option>
<option value="Brazil">Brazil</option>
<option value="Canada">Canada</option>
<option value="Chile">Chile</option>
<option value="Japan">Japan</option>
</datalist>
Le voir fonctionner
Tapez dans le champ pays et le navigateur affiche une liste déroulante filtrée. « C » réduit à Canada et Chile, « Ca » réduit à Canada. Choisissez-en une pour remplir le champ, ou ignorez la liste et tapez ce que vous voulez, puisqu’un datalist suggère des valeurs au lieu de les restreindre.
Rien n’a atteint le serveur et aucun JavaScript ne s’est exécuté. Le navigateur a fait le filtrage tout seul.
Il n’y a pas de capture d’écran parce que la liste déroulante est dessinée par le système d’exploitation, pas par la page, et n’apparaît donc pas dans les captures. Lancez l’application de démonstration pour la voir pour de vrai.
Alimenter les choix depuis la base de données
Les tableaux en dur conviennent pour une démo. L’usage réel consiste à alimenter les suggestions depuis vos données. L’argument choices accepte le même format que options_for_select, donc le résultat d’une requête s’insère directement :
<%= f.text_field :tag, list: f.field_id(:tag, :datalist) %>
<%= f.datalist :tag, Tag.where.not(name: [nil, ""]).distinct.order(:name).pluck(:name) %>
Le distinct et le rejet des valeurs vides évitent les doublons et les chaînes vides dans la liste. Déplacez cette requête dans un scope ou une méthode de modèle dans du vrai code. Et comme le champ est un simple text_field, modifier un enregistrement existant ne demande rien de plus : il se pré-remplit avec la valeur enregistrée et propose toujours la liste.
La forme [label, value] fonctionne aussi, comme pour un select :
<%= f.datalist :country_code,
[["Argentina", "AR"], ["Brazil", "BR"], ["Canada", "CA"]] %>
Mais un <datalist> n’est pas un <select>. Cela rend <option value="AR">Argentina</option>, et un datalist compare ce que vous tapez à la value (« AR »), insère la value et affiche la value comme suggestion. Le texte (« Argentina ») n’est au mieux qu’un indice secondaire, et certains navigateurs l’ignorent. Le comportement « afficher le nom, envoyer le code » d’un select ne s’applique pas ici, et taper « Arg » ne trouvera pas « Argentina ». Mettez dans la value ce que vous voulez que les utilisateurs tapent, ce qui signifie généralement que la forme tableau simple est celle qu’il vous faut.
Aller plus loin
Quelques pistes à explorer :
- Plusieurs listes dans un même formulaire. Chaque id est dérivé de son propre nom de méthode, donc
f.datalist :countryetf.datalist :cityn’entrent jamais en collision. Ajoutez-en autant que vous voulez. - HTML par option. Le format accepte un hash d’options final, donc
["Chile", "CL", { disabled: true }]rend une option désactivée. - Listes partagées. Pour plusieurs champs qui partagent un même ensemble, repassez au
datalist_tag('shared_id', choices)brut et pointez lelist:de chaque champ vers"shared_id".
Points de vigilance
Un datalist se contente de proposer des suggestions ; il n’empêchera personne de taper quelque chose hors liste. Ce qui se trouve dans le champ est ce qui arrive à votre contrôleur. Continuez donc à valider côté serveur : validates :country, presence: true, plus une vérification inclusion si vous n’acceptez qu’un ensemble connu. Le datalist sert au confort de saisie ; le modèle reste garant de l’exactitude.
Voici un piège : ne mettez pas autocomplete="off" sur le champ. C’est un réflexe courant pour neutraliser l’autoremplissage du navigateur, mais Safari et les autres navigateurs WebKit l’interprètent comme « aucune suggestion » et masquent le datalist. Si la liste apparaît dans Chrome mais pas dans Safari, c’est presque toujours la raison. Laissez autocomplete en dehors du champ, ou donnez-lui un vrai token, et la liste revient.
Autre point de vigilance : chaque option voyage dans le HTML, et il n’y a pas de filtrage côté serveur. Quelques centaines d’entrées, c’est très bien. « Toutes les villes du pays » ou « les 40 000 utilisateurs », c’est un payload gonflé et une liste lente à chaque frappe. Utilisez f.datalist pour des ensembles bornés que cela ne vous dérange pas d’envoyer en entier. Au-delà, vous voudrez une vraie autocomplétion adossée à un endpoint.
La liste déroulante a aussi ses limites. Vous ne pouvez pas la styler, le support des lecteurs d’écran varie selon les navigateurs et le comportement sur mobile diffère selon l’appareil. Pour un ensemble connu de suggestions, rien de tout cela n’a vraiment d’importance, mais si vous avez besoin de style, d’une accessibilité complète ou d’un filtrage distant, c’est là qu’un combobox JavaScript justifie son existence.
Pour conclure
L’autocomplétion native dans un formulaire Rails tient désormais en deux lignes : un text_field avec un attribut list: et un f.datalist qui dérive l’id correspondant. Pour le cas courant « suggérer depuis un ensemble connu », c’est tout le travail. Voir la PR #57318 pour l’implémentation.