Auto-complétion d’adresse avec la base api-adresse.data.gouv.fr, bootstrap & jquery

Des formulaires intelligents 🙂

Quand on saisit une adresse dans un formulaire en ligne, c’est bien pratique lorsque le formulaire propose tout seul comme un grand le bon code postal avec la commune, ou l’inverse, ou complète un nom de rue interminable.

Pour les adresses en France, une API gratuite est mise à disposition par le site data.gouv.fr. il y a quelques exemples d’intégration sur le web, mais je n’en ai trouvé aucun qui correspondait à ce que je cherchais : une auto-complétion propre, utilisable au clavier et à la souris, avec Bootstrap 4.6 et jQuery.

Il y a sans doute moyen de l’adapter pour faire sans jQuery ou sans Bootstrap, peut-être que j’y viendrais dans un second temps.

Pour construire ce script, je me suis appuyé sur des exemples trouvés sur le net, et quelques chouettes articles tels que :

Dis, chérie, tu sais où j’ai mis les URLs que j’avais sur le bureau hier ? Il y en avait une de stackOverflow, et… bon, tant pis, j’en aurai retrouvé au moins une !

Allez, trève de plaisanteries, on se lance !

La structure HTML (avec Bootstrap)

Tout d’abord, partons d’un formulaire simple. Ca tourne au bootstrap 4.6, mais je réfléchis sérieusement à passer au bio-éthanol.

<div class="container">
  <form id="modal_form" action="process_form.php" method="post">
    <div class="form-row">
      <div class="form-group col">
        <label for="adresse" class="control-label">Adresse</label>
        <input type="text" class="form-control" id="adresse" name="adresse" autocomplete="off" data-toggle="tooltip" data-placement="top" title="Ce champ est intelligent... essaie d'y taper à peu près n'importequoi, par exemple : barry 65150 ;)" />
        <div class="address-feedback position-absolute list-group" style="z-index:1100;">
        </div>
      </div>
    </div>
    <div class="form-row">
      <div class="form-group col-sm-3">
        <label for="cp" class="control-label">Code Postal</label>
        <input type="text" class="form-control" id="cp" name="cp" />
      </div>
      <div class="form-group col-sm-6">
        <label for="ville">Ville</label>
        <input type="text" class="form-control" id="ville" name="ville" required />
      </div>
      <div class="form-group col-sm-3">
        <label for="pays">Pays</label>
        <input type="text" class="form-control" id="pays" name="pays" />
      </div>

    </div>
  </form>
</div>

Quelques passages notables : le champ adresse, qui servira pour la recherche. J’y ai mis un tooltip, mais c’est facultatif.

En-dessous, un bloc <div> vide, qui viendra accueillir la liste d’auto-complétion. parmi les classes, il y a :

class="address-feedback"

ça, c’est juste pour l’identifier dans le script js plus loin. Indispensable donc.

class="position-absolute"

ça, c’est pour que la liste d’adresses suggérées se mette par-dessus sans bousculer la mise en page.

class="list-group"

ça, c’est tout simplement pour que Bootstrap nous prépare la présentation d’une liste toute jolie.

style="z-index: 1100;"

Enfin, ça c’est pour s’assurer que la liste des suggestions s’affiche bien par-dessus tout le reste, même dans une fenêtre modale.

Passons ensuite au gros du travail : la partie javaScript !

"Je suis pas gros ! je suis bas de poitrine !" - Obélix

C’est parti ! voilà le pâté, mais vous inquiétez pas, on va détailler tout ça.

/*
 * Script de gestion de recherche avec l'API adresses.data.gouv.fr
 * 
 */

var currentFocus = -1;
var fetchTrigger = 0;

function setActive() {
  var nbVal = $("div.address-feedback a").length;
  /*a function to classify an item as "active":*/
  if (!nbVal)
    return false;
  /*start by removing the "active" class on all items:*/
  $('div.address-feedback a').removeClass("active");

  // Bidouille mathématique pour contraindre le focus dans la plage du nombre de résultats
  currentFocus = ((currentFocus + nbVal - 1) % nbVal) + 1;

  $('div.address-feedback a:nth-child(' + currentFocus + ')').addClass("active");
}

// Au clic sur une adresse suggérée, on ventile l'adresse dans les champs appropriés. On espionne mousedown plutôt que click pour l'attraper avant la perte de focus du champ adresse.
$('div.address-feedback').on("mousedown", "a", function(event) {
  // Stop la propagation par défaut
  event.preventDefault();
  event.stopPropagation();

  $("#adresse").val($(this).attr("data-name"));
  $("#cp").val($(this).attr("data-postcode"));
  $("#ville").val($(this).attr("data-city"));

  $('.address-feedback').empty();
});

// On espionne le clavier dans le champ adresse pour déclencher les actions qui vont bien
$("#adresse").keyup(function(event) {
  // Stop la propagation par défaut
  event.preventDefault();
  event.stopPropagation();

  if (event.keyCode === 38) { // Flèche HAUT
    currentFocus--;
    setActive();
    return false;
  } else if (event.keyCode === 40) { // Flèche BAS
    currentFocus++;
    setActive();
    return false;
  } else if (event.keyCode === 13) { // Touche ENTREE
    if (currentFocus > 0) {
      // On simule un clic sur l'élément actif
      $("div.address-feedback a:nth-child(" + currentFocus + ")").mousedown();
    }
    return false;
  }

  // Si on arrive ici c'est que l'user a avancé dans la saisie : on réinitialise le curseur de sélection.
  $('div.address-feedback a').removeClass("active");
  currentFocus = 0;

  // On annule une éventuelle précédente requête en attente
  clearTimeout(fetchTrigger);

  // Si le champ adresse est vide, on nettoie la liste des suggestions et on ne lance pas de requête.
  let rue = $("#adresse").val();
  if (rue.length === 0) {
    $('.address-feedback').empty();
    return false;
  }

  // On lance une minuterie pour une requête vers l'API.
  fetchTrigger = setTimeout(function() {
    // On lance la requête sur l'API
    $.get('https://api-adresse.data.gouv.fr/search/', {
      q: rue,
      limit: 15,
      autocomplete: 1
    }, function(data, status, xhr) {
      let liste = "";
      $.each(data.features, function(i, obj) {
        // J'ajoute chaque élément dans une liste
        let cooladdress = obj.properties.name + " " + obj.properties.postcode + " <strong>" + obj.properties.city + "</strong>";
        liste += '<a class="list-group-item list-group-item-action py-1" href="#" name="' + obj.properties.label + '" data-name="' + obj.properties.name + '" data-postcode="' + obj.properties.postcode + '" data-city="' + obj.properties.city + '">' + cooladdress + '</a>';
      });
      $('.address-feedback').html(liste);
    }, 'json');
  }, 500);
});

// On cache la liste si le champ adresse perd le focus
$("#adresse").focusout(function() {
  $('.address-feedback').empty();
});

// On annule le comportement par défaut des touches entrée et flèches si une liste de suggestion d'adresses est affichée
$("#adresse").keydown(function(e) {
  if ($("div.address-feedback a").length > 0 && (e.keyCode === 38 || e.keyCode === 40 || e.keyCode === 13)) {
    e.preventDefault();
  }
});

Vous noterez qu’on a commencé par initialiser deux variables : une qui nous servira à stocker un TimeOut (un déclenchement à retardement pour la requête vers l’API), et une autre qui nous servira de curseur de sélection d’un résultat, avec les résultats étant numérotés de 1 à n.

var fetchTrigger = 0;
var currentFocus = -1;

On utilisera cette deuxième variable plus tard, mais on sait qu’elle sera pratique, car on gardera le focus sur le champ #adresse pour pouvoir continuer à taper, évidemment.

D’ailleurs, en parlant de focus, autant régler ça tout de suite : si l’utilisateur sort du champ adresse – autrement dit si le champ perd le focus – on masque les résultats proposés. Par exemple s’il n’a pas de résultat satisfaisant et qu’il veut compléter les autres champs à la main…

    // On cache la liste si le champ adresse perd le focus
    $("#adresse").focusout(function() {
      $('.address-feedback').empty();
    });

Un autre détail : comme on utilisera les flèches haut, bas et entrée pour la sélection au clavier d’un résultat, on va annuler leur comportement normal quand une proposition de résultats est affichée.

    // On annule le comportement par défaut des touches entrée et flèches si une liste de suggestion d'adresses est affichée
    $("#adresse").keydown(function(e) {
      if ($("div.address-feedback a").length > 0 && (e.keyCode === 38 || e.keyCode === 40 || e.keyCode === 13)) {
        e.preventDefault();
      }
    });

Maintenant on rentre dans le vif du sujet : on regarde les frappes clavier sur le champ #adresse

    // On espionne le clavier dans le champ adresse pour déclencher les actions qui vont bien
    $("#adresse").keyup(function(event) {
      // Stop la propagation par défaut
      event.preventDefault();
      event.stopPropagation();

Si les touches utilisées sont les flèches haut, bas, on déplace le curseur (incrément ou décrément), et on met à jour l’affichage (on reviendra ultérieurement sur la fonction setActive() ). Si c’était la touche Entrée : on simule un clic sur le résultat actif (là encore, on verra ce que ça fait plus tard).

      if (event.keyCode === 38) { // Flèche HAUT
        currentFocus--;
        setActive();
        return false;
      } else if (event.keyCode === 40) { // Flèche BAS
        currentFocus++;
        setActive();
        return false;
      } else if (event.keyCode === 13) { // Touche ENTREE
        if (currentFocus > 0) {
          // On simule un clic sur l'élément actif
          $("div.address-feedback a:nth-child(" + currentFocus + ")").mousedown();
        }
        return false;
      }

Si c’était une de ces touches on est pas allés plus loin (return false;). Mais si c’était pas le cas, c’est sans doute que l’utilisateur est en train de taper sa recherche, youpi !

On commence par (ré)initialiser le curseur de sélection de résultat. Parce que si la liste de résultats change, la position du curseur voudra plus rien dire. 0, c’est juste avant 1 (si, si, c’est vrai !) donc comme ça, l’utilisateur passera direct au premier résultat en faisant un coup sur la flèche du bas.

  // Si on arrive ici c'est que l'user a avancé dans la saisie : on réinitialise le curseur de sélection.
  $('div.address-feedback a').removeClass("active");
  currentFocus = 0;

Vous vous rappelez la variable fetchTrigger ? on avait dit qu’on l’utiliserait pour déclencher la requête vers l’API avec un léger retard. ça évite que ça lance autant de requêtes qu’on tape de lettres dans notre chaîne de recherche, alors qu’une seule requête quand on a fini de taper suffit largement 🙂

Bon en l’occurrence, à ce stade, on est en train de traiter une nouvelle chaine de recherche. avant même de lancer la requête vers l’API, on va vider le chargeur de notre canon à AJAX, afin de pas griller des cartouches inutiles (vous avez saisi la métaphore ?).

clearTimeout(fetchTrigger);

Mais là, forcément, une question vous taraude. « Sais-tu danser la Carioca ? » Ce n’est pas un fox-trot ou une polka, ce n’est vraiment pas compliqué, pour la comprendre, suis bien mes pas.

// Si le champ adresse est vide, on nettoie la liste des suggestions et on ne lance pas de requête.
let rue = $("#adresse").val();
if (rue.length === 0) {
  $('.address-feedback').empty();
  return false;
}

Allez go, on y est enfin, on dégoupille la grenade, et au bout de 500 ms, si elle a pas été désamorcée parce qu’une autre touche a été appuyée (cf-ci-dessus), elle nous explose une requête GET vers l’API du site officiel bleu-blanc-rouge. Pensez bien à donner les paramètres de la recherche en paramètres de l’appel $.get(), si on les met dans l’URL on se fait refouler par les vigiles (j’ai cherché un moment avant de comprendre que ça venait de là…)

  // On lance une minuterie pour une requête vers l'API.
  fetchTrigger = setTimeout(function() {
    // Voila enfin la fameuse requête sur l'API
    $.get('https://api-adresse.data.gouv.fr/search/', {
      q: rue,
      limit: 15,
      autocomplete: 1
    }, function(data, status, xhr) {
      let liste = "";
      $.each(data.features, function(i, obj) {
        // données phase 1 (obj.properties.label) & phase 2 : name, postcode, city
        // J'ajoute chaque élément dans une liste
        let cooladdress = obj.properties.name + " " + obj.properties.postcode + " <strong>" + obj.properties.city + "</strong>";
        liste += '<a class="list-group-item list-group-item-action py-1" href="#" name="' + obj.properties.label + '" data-name="' + obj.properties.name + '" data-postcode="' + obj.properties.postcode + '" data-city="' + obj.properties.city + '">' + cooladdress + '</a>';
      });
      $('.address-feedback').html(liste);
    }, 'json');
  }, 500);
});

Vous avez vu le 500 à la fin du code ? c’est le délai donné (en millisecondes) au timeout. A moins de faire la saisie avec les orteils, on met moins d’une demi-seconde entre chaque lettre d’un même mot.

Allez, on a fait le plus dur ! la grosse fonction est faite. Vous vous rappelez l’appel à la fonction setActive() pour colorier le résultat sélectionné ? Il est temps de définir cette fonction.

function setActive() {
  var nbVal = $("div.address-feedback a").length;
  /*a function to classify an item as "active":*/
  if (!nbVal)
    return false;
  /*start by removing the "active" class on all items:*/
  $('div.address-feedback a').removeClass("active");

  // Bidouille mathématique pour contraindre le focus dans la plage du nombre de résultats
  currentFocus = ((currentFocus + nbVal - 1) % nbVal) + 1;

  $('div.address-feedback a:nth-child(' + currentFocus +  ')').addClass("active");
}

Je vous passe les détails mathématiques, c’est pour revenir à 1 après le dernier résultat de la liste.

On y est presque ! il nous manque un seul détail : que fait-on quand on clique sur un résultat ?

// Au clic sur une adresse suggérée, on ventile l'adresse dans les champs appropriés. On espionne mousedown plutôt que click pour l'attraper avant la perte de focus du champ adresse.
$('div.address-feedback').on("mousedown", "a", function(event) {
  // Stop la propagation par défaut
  event.preventDefault();
  event.stopPropagation();

  $("#adresse").val($(this).attr("data-name"));
  $("#cp").val($(this).attr("data-postcode"));
  $("#ville").val($(this).attr("data-city"));

  $('.address-feedback').empty();
});

On ventile les données et zou ! Voilà le résultat !

Flemme de coller tout ça dans une page html ? Tonton Mika t’as préparé un joli jsFiddle pour voir le résult et jouer avec.

C’est bon, maintenant, tu peux aller manger des gencives de porc.

Cet article a 10 commentaires

  1. Alidor Rémy

    Et bien moi … je me suis bien marré !

  2. Willy willy

    Nice! Thx! « Chez Laplo, les meilleurs gencives du littoral! » 😉

  3. Maryline

    Bonjour,

    J’ai utilisé votre code, super pratique ! merci encore !
    Je bloque cependant sur un détail : j’aimerais que l’utilisateur ne puisse pas saisir d’autres caractères dans le champ adresse une fois qu’il a sélectionné une adresse de la liste.
    L’idée c’est vraiment de forcer l’utilisateur à sélectionner une adresse existante.
    Je ne sais pas comment faire, si vous avez un idée ce serait super !
    Merci à vous !

    1. mickael

      Bonjour Maryline, merci pour votre retour. Content que ce code puisse être utile 🙂
      Si je comprend bien votre besoin, peut-être que le plus simple est d’ajouter un champ pour pouvoir distinguer l’adresse de la recherche. En mettant les champs de l’adresse (adresse, code postal, ville) en readonly, et en ne conservant pas le champ de recherche (appelé adresse dans mon code, mais sans doute à renommer) dans le traitement du formulaire, ça devrait le faire.
      Bien sûr, sans contrôle des valeurs côté serveur, aucune certitude qu’un utilisateur n’a pas modifié l’adresse en tripatouillant un peu le html.
      J’espère être assez clair, et que ça marche aussi (pas testé ma proposition :p)

  4. Romy

    Bonjour, tout d’abord merci pour votre travail.

    J’ai un petit souci, j’ai copié collé votre code html et js (le js dans une balise script dans la page html) cependant, l’autocompletion ne fonctionne pas. J’écris un début d’adresse et je n’ai rien. Avez-vous une idée ? Merci par avance.

    1. Romy

      Je voulais dire »quand j’écris la ville ou le pays, cp »

      1. mickael

        Bonjour Romy, merci pour votre retour. Ce script ne fonctionne que sur le champ ‘adresse’, les autres champs ne déclenchent pas l’auto-complétion. à vous de l’améliorer selon vos besoins 🙂

  5. Patrick

    Bonjour,

    Merci pour ce partage et pour la bonne humeur.

    C’est exactement le code dont j’ai besoin, mais je n’arrive pas à le faire fonctionner.

    La console me signale une erreur sur cette ligne :

    $(‘[data-toggle= »tooltip »]’).tooltip()
    Uncaught TypeError: $(…).tooltip is not a function

    Je précise que le javascript n’est pas mon fort… et que j’essaie d’intégrer ce code dans une page wordpress.

    Merci et à bientôt,

    Patrick

    1. mickael

      Bonjour Patrick,
      Merci pour ce retour. Le script n’a pas été écrit pour être intégré à wordpress, et a été fait avec Bootstrap pour l’esthétique.
      La fonction tooltip() pointe vers une librairie utilisée par bootstrap (popper), et sert à afficher l’infobulle lorsque l’utilisateur survole le champ adresse. (voir la doc de Bootstrap là-dessus)
      Ce n’est pas indispensable au bon fonctionnement du script; tester en supprimant les 3 lignes indiquées commes facultatives dans le JSFiddle.
      Je crains que cette erreur en console ne soit pas la seule cause de dysfonctionnement dans le cas d’une intégration dans wordpress :/

      Bon courage !

Laisser un commentaire