Système d’auto-complétion avec AJAX

S

L’expérience utilisateur est chaque jour rendue plus fluide et plus conviviale, particulièrement grâce à l’apport de JavaScript. De nombreux sites proposent par exemple un système d’auto-complétion sur leurs champs de recherche. C’est ce procédé d’auto-complétion que je vous propose de décortiquer pas à pas en développant un champ de recherche des grandes villes du monde.

Ce tutoriel s’adresse aux développeurs débutants qui souhaitent appréhender le JavaScript à travers des exemples d’utilisation concrets.

Les technologies utilisées sont HTML 5, CSS 3, JavaScript, JSON, PHP 5.3 et MySQL 5.5. Pour l’exercice, j’ai choisi d’utiliser une base de données de développement contenant une seule table cities comportant les colonnes ID, Name, CountryCode, District et Population. Cette base de données est distribuée en libre téléchargement sur le site officiel de MySQL.

Enfin, aucune bibliothèque n’est utilisée pour réaliser ce système d’auto-complétion. Cependant, il est clair que l’utilisation de jQuery, voire d’Angular.js ou de Knockout.js, par exemple, facilite et optimise grandement le genre de traitements qui sont effectués ici.

Structure HTML

Le code est très simple, on remarquera simplement l’attribut autocomplete passé à off pour éviter que le navigateur ne vienne interférer avec notre script. L’ensemble est placé dans une balise div qui sera facile à placer sur une page web.

<div id="searcher" class="form--light-search">
<input type="text" name="autocomplete" id="autocomplete" class="input--search" autocomplete="off" />
<button type="button" name="search" id="search" class="button--search"></button>
</div>

Rendu visuel : le fichier CSS

Pour rendre le champ de recherche plus ou moins esthétique, quelques lignes de code CSS viennent enjoliver le champ de recherche. Puisque nous avons un module, nous utiliserons la syntaxe BEM, principalement pour pouvoir distinguer directement les parties modulaires.

Le style du champ de recherche et de la boîte de résultats est très simple :

.form--lightsearch {
  margin: 0;
  padding: 0;
}
.input--search {
  display: block;
  float: left;
  width: 200px;
  height: 30px;
  font-size: 1em;
  line-height: 30px;
  color: #333;
  padding: 1px 3px;
  border-top: 1px solid #dfdfdf;
  border-bottom: 1px solid #dfdfdf;
  border-left: 1px solid #dfdfdf;
  border-right: none;
  background: #fff;
  margin: 0 !important;
  margin: 0 -3px 0 0; /* IE6 Bugfix */
}
.button--search {
  display: block;
  width: 32px;
  height: 34px;
  background: #f2f2f2 url('../img/search.png') no-repeat center;
  border: 1px solid #dfdfdf;
  border-bottom-right-radius: 5px;
  border-top-right-radius: 5px;
  margin: 0; 
}
.button--search:hover {
  cursor: pointer;
}
.form--lightsearch__result {
  clear: both;
  position: absolute;
  margin: 0;
}

Celui des suggestion l’est tout autant :

.item--result {
  width: 194px;
  background: #fff;
  border-bottom: 1px dashed #dfdfdf;
  border-left: 1px solid #dfdfdf;
  border-right: 1px solid #dfdfdf;
  padding: 5px 5px;
  color: #333;
}
.item--result.last,
.item--result:last-child {
  border-bottom: 1px solid #dfdfdf;
}
.item--result:hover,
.item--result.focus {
  cursor: pointer;
  color: #fff;
  background: #637c8e;
  transition: all 0.2s ease-in-out;
}
.item--result .data-0 {
  float: left;
  margin: 0 0 3px 0;
}
.item--result .data-1 {
  display: block;
  float: right;
  color: #c0c0c0;
  font-weight: normal;
}
.item--result .data-2 {
  clear: both;
  display: block;
  color: #c0c0c0;
  font-weight: normal;
}

La couche de présentation est terminée, nous pouvons dès à présent nous atteler au développement de notre script d’auto-complétion, car maintenant que nos utilisateurs peuvent voir notre champ de recherche, il s’agit de le rendre fonctionnel.

Récupérer, traiter et renvoyer les données : le script PHP

La mise en place logique des différentes instructions est plutôt simple. Que devrons-nous faire une fois que nous aurons reçu la requête venant de l’objet XHR, et que devrons-nous lui renvoyer ? Quels seront les traitements à effectuer entre la réception et l’envoi des données ? Ces 3 questions guident notre raisonnement.

Voilà comment se présente le déroulement du fichier PHP :


// Le fichier de configuration existe
// On le récupère
// Le fichier JSON existe et il est périmé
// Connexion à la DB
// Requête SQL
// Ecriture du résultat dans le fichier JSON
// Sinon tentative de récupération du fichier périmé
// Le fichier JSON existe et il est récent
// Récupération des colonnes à matcher/afficher
// Décodage du fichier JSON
// Boucle sur les données
// Matching expression + insertion dans un tableau
// Tri alphabétique
// Envoi des n premiers résultats encodés en JSON
// Si pas on renvoie une erreur

Initialisation des variables locales

La première chose à faire est de préparer les paramètres de notre script : chemins d’accès des fichiers nécessaires, expression à checker, nombre de réponses à retourner et durée de vie du fichier .json

$request = strip_tags($_POST['requestExpression']); // Request expression
$filePath = '../files/completer.json';  // Path .json file
$cfgPath  = '../files/configuration.ini';  // Path configuration file
$responseSize  = 5; // Number of items to return
$expire = time() - 3600; // Validity cache duration in seconds

La variable $cfgPath contient le chemin vers le fichier de configuration. Ce fichier est indispensable au bon fonctionnement du script, car il contient les identifiants de connexion à la base de données, mais également les colonnes qu’il va falloir récupérer et, surtout, la colonne à matcher.

Récupération des données

Issues directement de la base de données ou depuis le fichier JSON, les données sont indispensables et leur récupération constitue très logiquement la première étape à réaliser.

if(file_exists($cfgPath)) {
  $cfg = parse_ini_file ($cfgPath);
  // Le fichier n\'existe pas/n'est pas actualisé, on charge les données en DB
  if(!file_exists($filePath) || filemtime($filePath) > $expire) {
    try {
      $pdo = new PDO(
        $cfg["dsn"],
        $cfg["user"],
        $cfg["pwd"],
        array(
          PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
          PDO::ATTR_PERSISTENT => TRUE
        )
      );
    } catch(PDOException $exception) {
      exit("Error Data Base connexion : " . $exception->getMessage());
  }
  $sql = ("SELECT " . $cfg["enum"] . " FROM " . $cfg["table"] . " ORDER BY ID ASC");
  $query = $pdo->query($sql);
  if($query->rowCount() > 0) {
    while($row = $query->fetch()) {
      $result[] = $row;
    }
    // Encode le résultat de la requête SQL en JSON et l'écrit dans le fichier JSON
    $content = json_encode($result);
    $handle  = fopen($filePath, 'w+');
    if (flock($handle, LOCK_EX)) {
      ftruncate($handle, 0);
      fwrite($handle, $content);
      flock($handle, LOCK_UN);
     } else {
       exit("Error Unable to lock file");
     }
     fclose($handle);
  } else {
    @mail('admin@yoursite.com', 'application@yoursite.com', 'Completer.js : the SQL request return zero result');
    // Tentative de repli
    if (file_exists($filePath)) {
      $content = file_get_contents($filePath);
    }
  }
}
// Le fichier existe et est actualisé
else if (file_exists($filePath)) {
  $content = file_get_contents($filePath);
}
// Prochaine étape : traitement des données
} else {
  @mail('admin@yoursite.com', 'application@yoursite.com', 'Completer.js : configuration.ini is unavailable');   
  exit("Impossible de charger le fichier de configuration");
}

Avant toute chose, nous vérifions que le fichier de configuration à la base de données existe bien. S’il n’existe pas, nous sommes malheureusement contraints de renvoyer une erreur contrôlée à l’utilisateur. Ceci est une obligation dans notre cas dans la mesure où le fichier de configuration contient non seulement les éléments indispensables à la chaîne de connexion vers la base de données, mais aussi les champs que nous allons devoir récupérer, vérifier et afficher. Puisque ce problème est une entrave majeure au fonctionnement du système d’auto-complétion, nous envoyons directement un email à l’administrateur pour le prévenir de ce cas de figure.

Par contre, si le fichier est accessible, alors nous le parsons avec la fonction parse_ini_file() afin d’en extraire les données.

Nous vérifions ensuite si le fichier .json existe, et nous vérifions s’il est encore valide au regard de la durée de mise en cache que nous avons paramétrée. Si le fichier est périmé, nous tentons une connexion à la base de données pour interroger notre table et regénérer le fichier. Si le fichier est toujours valide, alors nous ne nous connectons pas inutilement afin de ne pas surcharger le serveur de bases de données avec des requêtes répétitives.

L’accès aux données est fait en PHP via une instance PDO, qui est une couche d’abstraction permettant de gérer indifférement plusieurs systèmes de gestion de bases de données. Ainsi, si nous décidons plus tard de passer à MariaDB par exemple, nous ne devrons rien modifier dans notre code applicatif.

Traitement des données et recherche d’occurences

Il ne nous reste plus qu’à récupérer et parser le fichier .json généré et/ou simplement lu via json_decode() pour vérifier la correspondance avec la requête envoyée par l’utilisateur, grâce à la fonction strpos(). A chaque occurence trouvée débutant l’expression, nous plaçons les résultats dans un tableau que nous trions ensuite par ordre alphabétique, que nous limitons à n résultats, que nous réencodons en JSON avec json_encode() pour permettre la transmission des données sérialisées sous formes d’objets, et enfin que nous retournons avec un simple echo.

$enum = explode(',', trim($cfg['enum']));
// On décode le fichier JSON
$complete = json_decode($content);
$length = count($complete);
for($i = 0 ; $i < $length ; $i++) {
  // On matche l'expression sans tenir compte de la casse
  $pos = strpos(strtolower($complete[$i]->$enum[0]), strtolower($request));
  // Une occurence est trouvée
  if($pos !== FALSE) {
    //La chaîne commence par l'expression
    if($pos == 0) {
      $response[] = $complete[$i];
    }
  }
}
// Tri alphabétique
sort($response);

Envoi de la réponse

Il ne nous reste plus qu’à renvoyer la réponse au script JavaScript qui a effectué la requête :

 // Envoie une réponse encodée en JSON, limitée à 5 résultats
echo json_encode(array_slice($response, 0, $responseSize));

A présent, tout est prêt pour recevoir le résultat de la requête asynchrone.

Code client avec JavaScript

A ce stade, rien ne se passe entre l’entrée des données et la recherche d’occurences, et pour cause, rien ne relie l’interface à notre source de données. Puisque celle-ci nous retourne à présent un résultat dans un format utilisable en JavaScript, établissons le lien qui l’unit à notre formulaire.

Avant cela, assurons du bon fonctionnement de notre script sur tous les navigateurs. Plusieurs implémenations sont en effet rendues difficiles principalement sur les versions d’Internet Explorer inférieures à la 8. Pour palier à ces problèmes de portabilités, plusieurs polyfills sont utilisés. Ils ne sont pas explicités ici, vous pouvez les retrouver dans le fichier completer.js.

Afin d’assurer une meilleure lisibilité, les noms de méthodes et de propriétés commencent par une majuscule. C’est une habitude héritée du développement en .Net, que j’ai décidé de conserver en JavaScript pour cette fois-ci, car les chaînes d’extensions sont beaucoup plus faciles à lire si les propriétés et les méthodes commencent par une majuscule. C’est une syntaxe que j’ai voulu tester, cependant la meilleure façon de procéder serait de stocker les chaînes d’extension dans des variables, ce qui assurerait par ailleurs de meilleures performances.

Modèle de conception

Afin d’assurer la perméabilité du scope et la réutilisation sur d’autres projets, nous commençons par utiliser un pseudo-système de namespace.

if(typeof Completer === 'undefined') {
  var Completer = {};
} else {
  console.log('Ce namespace existe déjà');
}

Tout comme pour le script serveur, avant de développer les méthodes, nous définissons l’architecture générale au sein de notre namespace. Et puisque nous savons que nous allons fonctionner selon un pseudo modèle de classe statique – la comparaison s’arrêtant au principe de fonctionnement, nous nommons déjà les méthodes que nous allons utiliser.

var Completer = {
  // Initialise le script
  Init: function() {},
  // Gestion des évènements
  EventHandlers: {
    // Navigation clavier
    KeyboardNavigation: function() {},
    // Masquage des résultats
    HideResults: function() {},
    // Insertion au clic
    ClickableSuggestion: function() {}
  },
  // Propriétés de colonnes
  SetProperties: function() {},
  // Navigation
  Navigation: function() {},
  // Affichage des suggestions
  DisplaySuggestions: function() {},
  // Insertion d'une sélection
  InsertSuggestion: function() {},
  // Set focus
  SetFocus: function() {},
  // Attribution du focus
  GetFocus: function() {},
  // Retrait du focus
  RemoveFocus: function() {}
};

Propriétés

Nous déclarons les propriétés suivantes :

ConfigPath: 'php/configurer.php', // Chemin fichier configuration PHP
Path: 'php/completer.php', // Chemin script dynamique
Properties: [], // Propriétés récupérées
Suggestions: [], // Elements HTML suggérés
Pointer: -1, // Pointeur de navigation clavier
PreviousValue: '', // Valeur précédente entrée par l'utilisateur
Focused: null, // Element HTML ayant le focus
Input: null, // Element HTML input
Result: null, // Element HTML parent des suggestions
Response: {}, // Objet littéral contenant la réponse au format JSON

Méthodes

Bien qu’on ne puisse pas réellement parler de méthodes dans ce contexte étant donné que nous ne travaillons pas avec une classe, par facilité nous parlerons de méthodes du namespace Completer.

Init()

Cette méthode constitue le point d’entrée de notre programme. C’est par elle que nous initialiserons notre script en nous appuyant sur les évènements. L’interaction avec l’utilisateur se fera grâce au clavier principalement. Quelques évènements seront écoutés au click de souris, nous y reviendrons plus tard.

Init: function() {
  Completer.SetProperties();
  var result = document.createElement('div');
  result.id = "result";
  result.className = "form--lightsearch__result";
  this.Result = result;
  var searcher = document.getElementById('searcher');
  searcher.appendChild(result);
  this.Input = document.getElementById('autocomplete');
  this.EventHandlers.KeyboardNavigation(); 
  this.EventHandlers.HideResults();
},

SetProperties()

La méthode SetProperties() se charge de faire une petite requête asynchrone pour récupérer le fichier de configuration qui sera nécessaire lors des affichages.

SetProperties: function() {
  var xhr = GetXmlHttpRequest();
  xhr.open('GET', Completer.ConfigPath, true);
  xhr.onreadystatechange = function() {
  if(xhr.readyState === 4 && xhr.status === 200) {
    var response = xhr.responseText;
    Completer.Properties = response.split(', ');
  }
};
xhr.send(null);
},

EventHandlers

EventHandler est le sous-namespace à l’intérieur duquel nous allons placer nos méthodes liées aux évènements. Nous devrons gérer les évènements liés au clavier, l’insertion d’une suggestion dans le champ input et le masquage du jeu de résultats si l’utilisateur clique en dehors du set.

Nous devons donc récupérer la valeur de notre input à chaque relâchement de touche. C’est ce que nous faisons en lui assignant un écouteur d’évènement keyup qui réagira différement en fonction de la touche pressée. Pour assigner les écouteurs d’évènements aux éléments, nous utilisons la méthode AddEvent(), qui est un polyfill permettant d’uniformiser les méthodes natives addEventListener et attachEvent.

EventHandlers.KeyboardNavigation()
KeyboardNavigation: function() {
  AddEvent(Completer.Input, 'keyup', function(e) {
    e = e || window.event;
    var keycode = e.keyCode;
    // Haut/bas dans les résultats
    if(keycode === 38 || keycode === 40) {
      Completer.Navigation(keycode);
    }
    // Insère la suggestion qui a le focus dans l'input
    else if (keycode === 13) {
      Completer.InsertSuggestion();
    } else {
      if(Completer.Input.value !== Completer.PreviousValue) {
        Completer.DisplaySuggestions();
      }
    }
  });
},

La valeur retournée nous permettra tantôt de naviguer au sein de notre jeu de résultats grâce aux flèches de navigation haut et bas, tantôt d’insérer l’élément sélectionné dans le champ texte grâce à la touche Enter, tantôt d’interroger notre source de données.

Dans un premier temps nous vérifions donc la valeur retournée par l’évènement keyup.

La valeur de retour 38 ou 40, qui représente respectivement la valeur retournée par l’appui sur les flèches haut et bas, appelera la méthode Navigation().

La valeur 13 représente la valeur de retour lors de l’appui sur la touche Enter. Lorsque nous serons dans ce cas de figure, nous appelerons la méthode InsertSuggestion().

S’il ne s’agit d’aucune de ces deux valeurs, nous serons alors dans le cas de figure où l’utilisateur saisit des données au clavier. Ces données seront envoyées vers le serveur afin de vérifier la concordance avec les données de notre fichier .json, pour ensuite afficher les suggestions proposées.

EventHandlers.HideResult()

Si nous anticipons un peu la suite des évènements, et c’est le cas de le dire, nous savons que nous allons devoir masquer le jeu de résultats dans le cas où l’utilisateur s’en détournerait pour on ne sait quelle raison. Nous plaçons donc un écouteur sur l’évènement click de l’élément body.

HideResults: function(elm) {
  AddEvent(document.body, 'click', function() {
    elm.style.display = 'none';
  });
},
EventHandlers.ClickableSuggestion()

Notre jeu de résultats ne nous permet actuellement pas d’insérer une suggestion dans le champ input lorsque nous cliquons sur un item de suggestion. Nous comblons cette lacune avec la méthode ClickableSuggestion(), qui prend en paramètre le nom de la classe que nous allons utiliser pour assigner l’évènement aux éléments HTML, ici item--result.

ClickableSuggestion: function(nameClass) {
  Completer.Suggestions = document.getElementsByClassName(nameClass);
  if(typeof Completer.Suggestions !== 'undefined') {
    var numberOfSuggestions = Completer.Suggestions.length;
    if(numberOfSuggestions !== 0 && numberOfSuggestions !== null) {
      for(var i = 0; i < numberOfSuggestions; i++) {
        AddEvent(Completer.Suggestions[i], 'click', (function(i) {
          return function() {
            Completer.Focused = Completer.Suggestions[i];
            Completer.InsertSuggestion();
          };
        })(i));
      }
    }
  }
}

Ce bout de code est simple à comprendre. Il présente cependant une particularité que l’on retrouve régulièrement en JavaScript : une closure. En effet, si nous observons l’appel à la fonction AddEvent(), nous pouvons voir qu’en troisème paramètre, nous passons bien une fonction de callback, comme l’oblige la signature de la fonction, mais cette fonction de callback est une fonction anonyme !

Le procédé est indispensable dans ce cas, car nous voulons assigner un évènement sur chaque item contenu dans le tableau HTMLCollection, en y accédant par sa clef numérique grâce à la valeur de i. Or, nous somme dans une boucle, nous avons donc accès au scope local à l’intérieur de cette boucle, notre appel de méthode va accéder à la valeur de i en permanence, ce qui signifie qu’au final, i vaudra toujours … numberOfSuggestions !

Dans ce cas de figure, la fonction anonyme nous permet de retourner une autre fonction qui retournera la valeur de i passé en paramètre à chaque tour de boucle. Et la boucle est bouclée, nos évènements sont bien assignés sur toutes les suggestions.

DisplaySuggestions()

La méthode DisplaySuggestion() représente le coeur de notre application. C’est elle qui récupère les données entrées par l’utilisateur, les envoie au serveur et réceptionne le résultat pour l’afficher. Tout cela grâce à l’objet XHR ! Ici également, nous utilisons le polyfill GetXmlHttpRequest().

DisplaySuggestions: function() {
  Completer.Result.style.display = 'block';
  Completer.Pointer = -1;
  var text = "", xhr = GetXmlHttpRequest();
  xhr.open('POST', Completer.PHPPath, true);
  xhr.setRequestHeader('content-type', 'application/x-www-form-urlencoded');
  xhr.onreadystatechange = function() {
    if(xhr.readyState === 4 && xhr.status === 200) {
      var response = JSON.parse(xhr.responseText);
      if(response !== null) {
        Completer.Response = response;
        var properties = Completer.Properties.length,
        responseLength = response.length;
        for(var i = 0; i < responseLength ; i++) {
          if(typeof response[i] !== 'undefined') {
            var cls;
            i + 1 === responseLength ? cls = 'last' : cls = '';
            text += '<div id="' + i + '" class="item--result">';
            for(var j = 0; j < properties; j++) {
              text += '<span class="data-' + j + '">' + response[i][j] + '</span>';
            }
            text += '</div>';
          } 
        } 
      } else {
        text = '<div class="item--result">Not found</div>';
      }
    } else if(xhr.readyState === 4 && xhr.status !== 200) {
      text = '<div class="item--result">Error</div>';
    }
    Completer.Result.innerHTML = text;
    Completer.EventHandlers.ClickableSuggestion('item--result');
};
xhr.send('requestExpression=' + Completer.Input.value);
},

Uune fois les résultats retournés en affichage, nous avons modifié le DOM, et c’est à cet instant précis que nous devons agir si nous voulons permettre l’interactivité de notre jeu de résultats avec la souris. Nous n’aurions pas pu le faire plus tôt, car les éléments à écouter n’existaient pas, et nous ne pouvons pas le faire plus tard, car la portée des variables nous en empêcherait.

Nous plaçons donc un écouteur d’évènement click sur chaque item de notre jeu de résultats, à l’aide de la méthode EventHandlers.ClickableSuggestion(), détaillée juste avant. Ainsi, nous faisons en sorte de rendre possible l’insertion de la propriété Name de l’objet sélectionné par le clic dans notre champ input.

Navigation()

A ce stade, nous ne gérons pas les interactions avec le clavier. Que se passera-t-il si l’utilisateur veut utiliser les touches de navigation haut et/ou bas pour naviguer au sein du résultat de recherche ? Rien, pour le moment.

C’est là qu’intervient la bien nommée méthode Navigation(), qui prend en paramètre le keycode retourné par l’évènement keyup. En fonction de la valeur de ce paramètre, nous naviguons au sein de notre jeu de résultats, en attribuant/retirant le focus sur nos éléments grâce à la méthode SetFocus().

Navigation: function(keycode) {
  if(Completer.Pointer >= -1 && Completer.Pointer <= Completer.Suggestions.length - 1) {
    // Pointeur en dehors du data set, avant le premier résultat
    if(Completer.Pointer === -1) {
      if(keycode === 40) {
        Completer.SetFocus(keycode);
      }
    }
    // Pointeur au dernier résultat du data set
    else if (Completer.Pointer === Completer.Suggestions.length - 1) {
      if(keycode === 38) {
        Completer.SetFocus(keycode);
      }
    }
    // Pointeur dans le data set
    else { 
      Completer.SetFocus(keycode);
    }
  }
},

InsertSuggestion()

Nous gérons notre navigation, mais pas encore l’appui sur la touche Enter ou le click sur un item survolé par la souris. Or c’est bien par une de ces deux actions que l’utilisateur voudra sélectionner un item pour compléter son champ de recherche. Nous définissons donc la méthode InsertSuggestion() qui se chargera de cela, et qui n’oubliera pas de fermer la porte en sortant, en masquant le jeu de résultats.

InsertSuggestion: function() {
  var id;
  Completer.Focused !== null ? id = Completer.Focused.id : id = 0;
  Completer.Input.value = Completer.Response[id][0];
  Completer.Pointer = -1;
  Completer.Result.style.display = 'none';
},

SetFocus()

Pour gérer visuellement la navigation, la méthode Navigation() se réfère à la méthode SetFocus().

Si on descend dans le jeu de résultats, il faut enlever le focus à l’élément quitté (sauf s’il s’agit du premier élément à la sortie de l’input) et l’attribuer au suivant. La référence de ce système est la propriété Pointer initialisée à -1 et modifée ensuite dynamiquement avec la valeur de la clef de l’élément qui a le focus dans le jeu de résultats.

Par souci de lisibilité et dans l’éventualité où il faudrait manuellement attribuer ou retirer le focus à un élément, les instructions permettant d’attribuer ou de retirer la classe focus à un élément sont placées dans deux méthodes distinctes : GetFocus() et RemoveFocus().

SetFocus: function(keycode) {
  if(keycode === 40) {
    if(this.Pointer !== -1) {
      Completer.RemoveFocus();
    } 
    Completer.Pointer++;
    Completer.GetFocus();
  } else if(keycode === 38) {
    Completer.RemoveFocus();
    Completer.Pointer--;
    if(Completer.Pointer !== -1) {
      Completer.GetFocus();
    }
  }
},

GetFocus() et RemoveFocus()

Ces deux méthodes sont appelées par la méthode setFocus() et servent à ajouter/retirer la classe focus à un élément HTML du jeu de résultats.

GetFocus: function() {
  Completer.Focused = Completer.Suggestions[Completer.Pointer];
  Completer.Suggestions[Completer.Pointer].className += ' focus';
},
RemoveFocus: function() {
  Completer.Focused = null;
  Completer.Suggestions[Completer.Pointer].className = 'item--result';
}

Utilisation et suggestions d’améliorations

Notre code est terminé, mais il ne fonctionnera pas en l’état. Pour initialiser notre script et le rendre utilisable, il ne nous reste plus qu’à appeler notre méthode Init().

Completer.Init();

Voilà, notre système d’auto-complétion est terminé et fonctionnel !

Le tutoriel est un peu long, mais les scripts, eux, ne le sont pas, cela est surtout dû à la présentation visuelle de la page du blog, et au fait que toutes les parties ont été commentées avec moult détails.

Edit 2017: Le script a été redéveloppé en tant que plugin jQuery. Vous pouvez tester la démo, ou télécharger directement les sources. N’hésitez pas à commenter le projet si vous avez des idées d’améliorations !

A propos de l'auteur

Steve Lebleu

Cross-triathlète, amoureux de nature, de grands espaces et ... d'applications web. Curieux et touche-à-tout, je m'intéresse à tous les aspects du développement d'un projet web. Je suis développeur full stack freelance depuis 2018, principalement sur des piles Javascript.

Ajouter un commentaire

  • Super boulot, je trouve étonnant qu’il n’y ait pas plus de commentaires…

    Je suis intéressé par ce script pour un projet de tri de villes mais que ce soit sur wamp ou sur votre démo, il apparait une erreur dès qu’il n’y a pas de correspondance dans la recherche:

    SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data

    N’étant pas calé en ajax, pourriez vous m’indiquer comment résoudre cela ?

    Cordialement

    • Bonjour Henry,

      Merci pour le compliment, et merci pour votre retour, je vais corriger ça dans le tutoriel. Le blog n’a pas beaucoup de visibilité, ceci expliquant sans doute cela.

      Vous pouvez résoudre le problème que vous rencontrez en faisant une petite vérification avant de parser la chaîne, avec quelque chose comme ceci :

      if(response.length) {
      JSON.parse(response);
      }

  • Bonsoir,

    En regardant sur le net les moyens qui existent pour faire une auto complétion pour une requête donnée, je suis tombée sur votre script. que je le trouve intéressant.

    J’aimerai bien l’utiliser, mais en exécutant le code en modifiant le fichier completer.json (afin de mettre mes propres données) je trouve quelques soucis, rien ne s’affiche quand je tape une lettre! je pense que c’est un problème d’encodage, car même avec votre fichier, quand je modifie le nom d’une ville par exemple, ça pose de problème! voici l’erreur:

    SyntaxError: JSON.parse: unexpected character at line 1 column 1 of the JSON data

    Merci de me répondre par mail si possible afin de m’expliquer comment résoudre ce problème.

    Bien cordialement,

    • Bonsoir Meryem,

      Le fichier completer.json est complété dynamiquement, son utilité est de servir de cache, ce qui peut se discuter.

      Tu peux “bypasser” cela en faisant communiquer le client et le serveur en direct, sans passer par un fichier physique.

      Dans tous les cas, l’erreur que tu rencontres est une erreur classique lorsqu’on manipule des données JSON, il s’agit probablement d’un problème de données, ou d’encodage, impossible à dire sans voir les données en question, et le code JavaScript qui le traite.