TP : Un système d'auto-complétion

Après avoir absorbé autant d'informations sur le concept de l'AJAX, il est grand temps de mettre en pratique une bonne partie de vos connaissances. Le TP de cette partie sera consacré à la création d'un système d'auto-complétion qui sera capable d'aller chercher, dans un fichier, les villes de France commençant par les lettres que vous aurez commencé à écrire dans le champ de recherche. Le but est d'accélérer et de valider la saisie de vos mot-clés.

Malheureusement, ce TP n'utilisera l'AJAX que par le biais de l'objetXMLHttpRequestafin de rester dans des dimensions raisonnables. Cependant, il s'agit, et de loin, de la méthode la plus en vogue de nos jours, l'utilisation d'iframes et de DSL étant réservée à des cas bien plus particuliers.

Présentation de l'exercice

Les technologies à employer

Avant de commencer, il nous faut déterminer le type de technologie dont nous avons besoin, car ici nous ne faisons pas uniquement appel au JavaScript, nous allons devoir employer d'autres langages.

Dans un cadre général, un système d'auto-complétion fait appel à trois technologies différentes :

  • Un langage client ayant la capacité de dialoguer avec un serveur ;

  • Un langage serveur capable de fournir les données au client ;

  • Une base de données qui stocke toutes les données.

Dans notre cas, nous allons utiliser le JavaScript ainsi que le PHP (bien que n'importe quel autre langage serveur soit capable de faire son travail). Nous allons, en revanche, faire une petite entorse au troisième point en utilisant un fichier de stockage plutôt qu'une base de données, cela pour une raison bien simple : simplifier notre code, surtout que nous n'avons pas besoin d'une base de données pour le peu de données à enregistrer.

Principe de l'auto-complétion

Un système d'auto-complétion se présente de la manière suivante :

Google a mis en place un système d'auto-complétion sur son moteur de recherche
Google a mis en place un système d'auto-complétion sur son moteur de recherche

Le principe est simple mais efficace : dès qu'un utilisateur tape un caractère dans le champ, une recherche est immédiatement effectuée et retournée au navigateur. Ce dernier affiche alors les résultats dans un petit cadre généralement situé sous le champ de recherche. Les résultats affichés peuvent alors être parcourus, soit par le biais des touches fléchées du clavier (haut et bas), soit par le biais du curseur de la souris. Si on choisit un des résultats listés, celui-ci est alors automatiquement écrit dans le champ de recherche en lieu et place de ce qui avait été écrit par l'utilisateur. Il ne reste alors plus qu'à lancer la recherche.

L'avantage de ce type de script, c'est que l'on gagne un temps fou : la recherche peut être effectuée en tapant seulement quelques caractères. Cela est aussi très utile lorsque l'on ne connaît qu'une partie du terme recherché ou bien quand on fait une faute de frappe.

Conception

Nous connaissons le principe de l'auto-complétion et les technologies nécessaires. Cependant, cela n'explique pas comment tout cela doit être utilisé. Nous allons donc vous guider pour vous permettre de vous lancer sans trop d'appréhension dans ce vaste projet.

L'interface

Commençons par l'interface ! De quoi allons-nous avoir besoin ? L'auto-complétion étant affiliée, généralement, à tout ce qui est du domaine de la recherche, il va nous falloir un champ de texte pour écrire les mots-clés. Cependant, ce dernier va nous poser problème, car le navigateur enregistre généralement ce qui a été écrit dans les champs de texte afin de vous le reproposer plus tard sous forme d'auto-complétion, ce qui va donc faire doublon avec notre système… Heureusement, il est possible de désactiver cette auto-complétion en utilisant l'attributautocompletede cette manière :

<input type="text" autocomplete="off" >

À cela nous allons devoir ajouter un élément capable d'englober les suggestions de recherches. Celui-ci sera composé d'une balise<div>contenant autant de<div>que de résultats, comme ceci :

<div id="results">
<div>Résultat 1</div>
<div>Résultat 2</div>
</div>

Chaque résultat dans les suggestions devra changer d'aspect lorsque celui-ci sera survolé ou sélectionné par le biais des touches fléchées.

En ce qui concerne un éventuel bouton de typesubmit, nous allons nous en passer, car notre but n'est pas de lancer la recherche, mais seulement d'afficher une auto-complétion.

La communication client/serveur

Contrairement à ce que l'on pourrait penser, il ne s'agit pas ici d'une partie bien compliquée car, dans le fond, qu'allons-nous devoir faire ? Simplement effectuer une requête à chaque caractère écrit afin de proposer une liste de suggestions. Il nous faudra donc une fonction liée à l'événementkeyupde notre champ de recherche, qui sera chargée d'effectuer une nouvelle requête à chaque caractère tapé.

Cependant, admettons que nous tapions deux caractères dans le champ de recherche, que la première requête réponde en 100 ms et la seconde en 65 ms : nous allons alors obtenir les résultats de la première requête après ceux de la seconde et donc afficher des suggestions qui ne seront plus du tout en accord avec les caractères tapés dans le champ de recherche. La solution à cela est simple : utiliser la méthodeabort()sur la précédente requête effectuée si celle-ci n'est pas terminée. Ainsi, elle ne risque pas de renvoyer des informations dépassées par la suite.

Le traitement et le renvoi des données

Côté serveur, nous allons faire un script de recherche basique sur lequel nous ne nous attarderons pas trop, le PHP n'étant pas notre priorité. Le principe consiste à rechercher les villes qui correspondent aux lettres entrées dans le champ de recherche. Nous vous avons conçu une petite archive ZIP dans laquelle vous trouverez un tableau PHP linéarisé contenant les plus grandes villes de France, il ne vous restera plus qu'à l'analyser.

Télécharger l'archive !

Le PHP n'étant pas forcément votre point fort (après tout, vous êtes là pour apprendre le JavaScript), nous allons tâcher de bien détailler ce que vous devez faire pour réussir à faire votre recherche.

Tout d'abord, il vous faut récupérer les données contenues dans le fichiertowns.txt(disponible dans l'archive fournie plus haut). Pour cela, il va vous falloir lire ce fichier avec la fonction file_get_contents()
, puis convertir son contenu en tant que tableau PHP grâce à la fonction unserialize()
.

Une fois cela fait, le tableau obtenu doit être parcouru à la recherche de résultats en cohérence avec les caractères tapés par l'utilisateur dans le champ de recherche. Pour cela, vous aurez besoin d'une boucle ainsi que de la fonction count()
pour obtenir le nombre d'éléments contenus dans le tableau.

Pour vérifier si un index du tableau correspond à votre recherche, il va vous falloir utiliser la fonction stripos()
, qui permet de vérifier si une chaîne de caractères contient certains caractères, et ce sans tenir compte de la casse. Si vous trouvez un résultat en cohérence avec la recherche, alors ajoutez-le à un tableau (que vous aurez préalablement créé) grâce à la fonction array_push()
.

Une fois le tableau parcouru, il ne vous reste plus qu'à trier les résultats avec la fonction sort()
, puis à renvoyer les données au client…

Oui, mais sous quelle forme ? XML ? JSON ?

Ni l'une, ni l'autre ! Tout simplement sous forme de texte brut !

Pourquoi ? Pour une raison simple : le XML et le JSON sont utiles pour renvoyer des données qui ont besoin d'être structurées. Si nous avions eu besoin de renvoyer, en plus des noms de ville, le nombre d'habitants, de commerces et d'administrations françaises, alors nous aurions pu envisager l'utilisation du XML ou du JSON afin de structurer tout ça. Mais dans notre cas cela n'est pas utile, car nous ne faisons que renvoyer le nom de chaque ville trouvée.

Alors comment renvoyer tout ça sous forme de texte brut ? Nous pourrions faire un saut de ligne entre chaque ville retournée, mais ce n'est pas spécialement pratique à analyser pour le JavaScript. Nous allons donc devoir choisir un caractère de séparation qui n'est jamais utilisé dans le nom d'une ville. Dans ce TP, nous allons donc utiliser la barre verticale |, ce qui devrait nous permettre de retourner du texte brut sous cette forme :

Paris|Perpignan|Patelin-Paumé-Sur-Toise|Etc.

Tout commejoin()en JavaScript, il existe une fonction PHP qui vous permet de concaténer toutes les valeurs d'un tableau dans une chaîne de caractères avec un ou plusieurs caractères de séparation : il s'agit de la fonction implode()
. Une fois la fonction utilisée, il ne vous reste plus qu'à retourner le tout au client avec un bon vieilechoet à analyser cela côté JavaScript.

C'est à vous !

Maintenant que vous avez toutes les cartes en main, à vous de jouer ! N'hésitez pas à regarder la correction du fichier PHP si besoin, nous pouvons comprendre que vous puissiez ne pas le coder vous-mêmes sachant que ce n'est pas le sujet de ce cours.

Mais je commence par où ? Le serveur ou le client ?

Il est préférable que vous commenciez par le code PHP afin de vous assurer que celui-ci fonctionne bien, cela vous évitera bien des ennuis de débogage.

En cas de dysfonctionnements dans votre code, pensez bien à regarder la console d'erreurs et aussi à vérifier ce que vous a renvoyé le serveur, car l'erreur peut provenir de ce dernier et non pas forcément du client. ;)

Correction

Votre système d'auto-complétion est terminé ? Bien ! Fonctionnel ou pas, l'important est d'essayer et de comprendre d'où proviennent vos erreurs, donc ne vous en faites pas si vous n'avez pas réussi à aller jusqu'au bout.

Le corrigé complet

Vous trouverez ici la correction des différentes parties nécessaires à l'auto-complétion. Commençons tout d'abord par le code PHP du serveur, car nous vous avions conseillé de commencer par celui-ci :

<?php
$data = unserialize(file_get_contents('towns.txt')); // Récupération de la liste complète des villes
$dataLen = count($data);
sort($data); // On trie les villes dans l'ordre alphabétique
$results = array(); // Le tableau où seront stockés les résultats de la recherche
// La boucle ci-dessous parcourt tout le tableau $data, jusqu'à un maximum de 10 résultats
for ($i = 0 ; $i < $dataLen && count($results) < 10 ; $i++) {
if (stripos($data[$i], $_GET['s']) === 0) { // Si la valeur commence par les mêmes caractères que la recherche
array_push($results, $data[$i]); // On ajoute alors le résultat à la liste à retourner
}
}
echo implode('|', $results); // Et on affiche les résultats séparés par une barre verticale |
?>

Vient ensuite la structure HTML, qui est on ne peut plus simple :

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<title>TP : Un système d'auto-complétion</title>
</head>
<body>
<input id="search" type="text" autocomplete="off" />
<div id="results"></div>
</body>
</html>

Et pour finir, voici le code JavaScript :

(function() {
var searchElement = document.getElementById('search'),
results = document.getElementById('results'),
selectedResult = -1, // Permet de savoir quel résultat est sélectionné : -1 signifie "aucune sélection"
previousRequest, // On stocke notre précédente requête dans cette variable
previousValue = searchElement.value; // On fait de même avec la précédente valeur
function getResults(keywords) { // Effectue une requête et récupère les résultats
var xhr = new XMLHttpRequest();
xhr.open('GET', './search.php?s='+ encodeURIComponent(keywords));
xhr.addEventListener('readystatechange', function() {
if (xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) {
displayResults(xhr.responseText);
}
});
xhr.send(null);
return xhr;
}
function displayResults(response) { // Affiche les résultats d'une requête
results.style.display = response.length ? 'block' : 'none'; // On cache le conteneur si on n'a pas de résultats
if (response.length) { // On ne modifie les résultats que si on en a obtenu
response = response.split('|');
var responseLen = response.length;
results.innerHTML = ''; // On vide les résultats
for (var i = 0, div ; i < responseLen ; i++) {
div = results.appendChild(document.createElement('div'));
div.innerHTML = response[i];
div.addEventListener('click', function(e) {
chooseResult(e.target);
});
}
}
}
function chooseResult(result) { // Choisi un des résultats d'une requête et gère tout ce qui y est attaché
searchElement.value = previousValue = result.innerHTML; // On change le contenu du champ de recherche et on enregistre en tant que précédente valeur
results.style.display = 'none'; // On cache les résultats
result.className = ''; // On supprime l'effet de focus
selectedResult = -1; // On remet la sélection à "zéro"
searchElement.focus(); // Si le résultat a été choisi par le biais d'un clique alors le focus est perdu, donc on le réattribue
}
searchElement.addEventListener('keyup', function(e) {
var divs = results.getElementsByTagName('div');
if (e.keyCode == 38 && selectedResult > -1) { // Si la touche pressée est la flèche "haut"
divs[selectedResult--].className = '';
if (selectedResult > -1) { // Cette condition évite une modification de childNodes[-1], qui n'existe pas, bien entendu
divs[selectedResult].className = 'result_focus';
}
}
else if (e.keyCode == 40 && selectedResult < divs.length - 1) { // Si la touche pressée est la flèche "bas"
results.style.display = 'block'; // On affiche les résultats
if (selectedResult > -1) { // Cette condition évite une modification de childNodes[-1], qui n'existe pas, bien entendu
divs[selectedResult].className = '';
}
divs[++selectedResult].className = 'result_focus';
}
else if (e.keyCode == 13 && selectedResult > -1) { // Si la touche pressée est "Entrée"
chooseResult(divs[selectedResult]);
}
else if (searchElement.value != previousValue) { // Si le contenu du champ de recherche a changé
previousValue = searchElement.value;
if (previousRequest && previousRequest.readyState < XMLHttpRequest.DONE) {
previousRequest.abort(); // Si on a toujours une requête en cours, on l'arrête
}
previousRequest = getResults(previousValue); // On stocke la nouvelle requête
selectedResult = -1; // On remet la sélection à "zéro" à chaque caractère écrit
}
});
})();

Essayer le code complet

Les explications

Ce TP n'est pas compliqué en soi mais aborde de nouveaux concepts, il se peut donc que vous soyez quelque peu perdus à la lecture des codes fournis. Laissez-nous vous expliquer comment tout cela fonctionne.

Le serveur : analyser et retourner les données

Comme indiqué plus tôt, il est préférable de commencer par coder notre script serveur, cela évite bien des désagréments par la suite, car il est possible d'analyser manuellement les données retournées par le serveur, et ce sans avoir déjà codé le script client. On peut donc s'assurer du bon fonctionnement du serveur avant de s'attaquer au client.

Tout d'abord, il nous faut définir comment le serveur va recevoir les mots-clés de la recherche. Nous avons choisi la méthode GET et un nom de champ « s », ce qui nous donne la variable PHP$_GET['s'].

Avant de commencer notre analyse de données, il nous faut précharger le fichier, convertir son contenu en tableau PHP et enfin trier ce dernier. À cela s'ajoutent le calcul de la taille du tableau généré ainsi que la création d'un tableau pour sauvegarder les résultats en cohérence avec la recherche :

<?php
$data = unserialize(file_get_contents('towns.txt')); // Récupération de la liste complète des villes
$dataLen = count($data);
sort($data); // On trie les villes dans l'ordre alphabétique
$results = array(); // Le tableau où seront stockés les résultats de la recherche
?>

Maintenant que toutes les données sont accessibles, il va nous falloir les analyser. Basiquement, il s'agit de la même opération qu'en JavaScript : une boucle pour parcourir le tableau et une condition pour déterminer si le contenu est valide. Voici ce que cela donne en PHP :

<?php
// La boucle ci-dessous parcourt tout le tableau $data, jusqu'à un maximum de 10 résultats
for ($i = 0; $i < $dataLen && count($results) < 10; $i++) {
if (stripos($data[$i], $_GET['s']) === 0) { // Si la valeur commence par les mêmes caractères que la recherche
// Du code…
}
}
?>

La boucleforpossède une condition un peu particulière qui stipule qu'elle doit continuer à tourner tant qu'elle n'a pas lu tout le tableau$dataet qu'elle n'a pas atteint le nombre maximum de résultats à retourner. Cette limite de résultats est nécessaire, car une auto-complétion ne doit pas afficher tous les résultats sous peine de provoquer des ralentissements dus au nombre élevé de données, sans compter qu'un trop grand nombre de résultats serait difficile à parcourir (et à analyser) pour l'utilisateur.

La fonctionstripos()retourne la première occurrence de la recherche détectée dans la valeur actuellement analysée. Il est nécessaire de vérifier que la valeur retournée est bien égale à 0, car nous ne souhaitons obtenir que les résultats qui commencent par notre recherche. La triple équivalence (===) s'explique par le fait que la fonctionstripos()retournefalseen cas d'échec de la recherche, ce que la double équivalence (==) aurait confondu avec un 0.

Une fois qu'un résultat cohérent a été trouvé, il ne reste plus qu'à l'ajouter à notre tableau$results:

<?php
for ($i = 0; $i < $dataLen && count($results) < 10; $i++) {
if (stripos($data[$i], $_GET['s']) === 0) { // Si la valeur commence par les mêmes caractères que la recherche
array_push($results, $data[$i]); // On ajoute alors le résultat à la liste à retourner
}
}
?>

Une fois que la boucle a terminé son exécution, il ne reste plus qu'à retourner le contenu de notre tableau de résultats sous forme de chaîne de caractères. Lors de la présentation de ce TP, nous avons évoqué le fait de retourner les résultats séparés par une barre verticale, c'est donc ce que nous appliquons dans le code suivant :

<?php
echo implode('|', $results); // On affiche les résultats séparés par une barre verticale |
?>

Ainsi, notre script côté client n'aura plus qu'à faire un bon vieuxsplit('|')sur la chaîne de caractères obtenue grâce au serveur pour avoir un tableau listant les résultats obtenus.

Le client : préparer le terrain

Une fois le code du serveur écrit et testé, il ne nous reste « plus que » le code client à écrire. Cela commence par le code HTML, qui se veut extrêmement simple avec un champ de texte sur lequel nous avons désactivé l'auto-complétion ainsi qu'une balise<div>destinée à accueillir la liste des résultats obtenus :

<input id="search" type="text" autocomplete="off" />
<div id="results"></div>

Voilà tout pour la partie HTML ! En ce qui concerne le JavaScript, il nous faut tout d'abord, avant de créer les événements et autres choses fastidieuses, déclarer les variables dont nous allons avoir besoin. Plutôt que de les laisser traîner dans la nature, nous allons les déclarer dans une IIFE (pour les trous de mémoire sur ce terme, c'est par ici ) :

(function() {
var searchElement = document.getElementById('search'),
results = document.getElementById('results'),
selectedResult = -1, // Permet de savoir quel résultat est sélectionné : -1 signifie « aucune sélection »
previousRequest, // On stocke notre précédente requête dans cette variable
previousValue = searchElement.value; // On fait de même avec la précédente valeur
})();

Si l'utilité de la variablepreviousValuevous semble douteuse, ne vous en faites pas, vous allez vite comprendre à quoi elle sert !

Le client : gestion des événements

L'événement utilisé estkeyupet va se charger de gérer les interactions entre l'utilisateur et la liste de suggestions. Il doit permettre, par exemple, de naviguer dans la liste de résultats et d'en choisir un avec la touche Entrée, mais il doit aussi détecter quand le contenu du champ de recherche change et alors faire appel au serveur pour obtenir une nouvelle liste de résultats.

Commençons tout d'abord par initialiser l'événement en question ainsi que les variables nécessaires :

searchElement.addEventListener('keyup', function(e) {
var divs = results.getElementsByTagName('div'); // On récupère la liste des résultats
});

Commençons tout d'abord par gérer les touches fléchées Haut et Bas. C'est là que notre variableselectedResultentre en action, car elle stocke la position actuelle de la sélection des résultats. Avec -1, il n'y a aucune sélection et le curseur se trouve donc sur le champ de recherche ; avec 0, le curseur est positionné sur le premier résultat, 1 désigne le deuxième résultat, etc.

Pour chaque déplacement de la sélection, il vous faut appliquer un style sur le résultat sélectionné afin que l'on puisse le distinguer des autres. Il existe plusieurs solutions pour cela, cependant nous avons retenu celle qui utilise les classes CSS. Autrement dit, lorsqu'un résultat est sélectionné, vous n'avez qu'à lui attribuer une classe CSS qui va modifier son style. Cette classe doit bien sûr être retirée dès qu'un autre résultat est sélectionné. Concrètement, cette solution donne ceci pour la gestion de la flèche Haut :

if (e.keyCode == 38 && selectedResult > -1) { // Si la touche pressée est la flèche « haut »
divs[selectedResult--].className = ''; // On retire la classe de l'élément inférieur et on décrémente la variable « selectedResult »
if (selectedResult > -1) { // Cette condition évite une modification de childNodes[-1], qui n'existe pas, bien entendu
divs[selectedResult].className = 'result_focus'; // On applique une classe à l'élément actuellement sélectionné
}
}

Vous constaterez que la première condition doit vérifier deux règles. La première est la touche frappée, jusque là tout va bien. Quant à la seconde règle, elle consiste à vérifier que notre sélection n'est pas déjà positionnée sur le champ de texte, afin d'éviter de sortir de notre « champ d'action », qui s'étend du champ de texte jusqu'au dernier résultat suggéré par notre auto-complétion.

Curieusement, nous retrouvons une seconde condition (ligne 5) effectuant la même vérification que la première :selectedResult > -1. Cela est en fait logique, car si l'on regarde bien la troisième ligne, la valeur deselectedResultest décrémentée, il faut alors effectuer une nouvelle vérification.

Concernant la flèche Bas, les changements sont assez peu flagrants, ajoutons donc la gestion de cette touche à notre code :

else if (e.keyCode == 40 && selectedResult < divs.length - 1) { // Si la touche pressée est la flèche « bas »
results.style.display = 'block'; // On affiche les résultats « au cas où »
if (selectedResult > -1) { // Cette condition évite une modification de childNodes[-1], qui n'existe pas, bien entendu
divs[selectedResult].className = '';
}
divs[++selectedResult].className = 'result_focus';
}

Ici, les changements portent surtout sur les valeurs à analyser ou à modifier. On ne décrémente plusselectedResultmais on l'incrémente. La première condition est modifiée afin de vérifier que l'on ne se trouve pas à la fin des résultats au lieu du début, etc.

Et, surtout, l'ajout d'une nouvelle ligne (la troisième) qui permet d'afficher les résultats dans tous les cas. Pourquoi cet ajout ? Eh bien, pour simplifier l'utilisation de notre script. Vous le constaterez plus tard, mais lorsque vous choisirez un résultat (donc un clic ou un appui sur Entrée) cela entraînera la disparition de la liste des résultats. Grâce à l'ajout de notre ligne de code, vous pourrez les réafficher très simplement en appuyant sur la flèche Bas !

Venons-en maintenant à la gestion de cette fameuse touche Entrée :

else if (e.keyCode == 13 && selectedResult > -1) { // Si la touche pressée est « Entrée »
chooseResult(divs[selectedResult]);
}

Alors oui, vous êtes en droit de vous demander quelle est cette fonctionchooseResult(). Il s'agit en fait d'une des trois fonctions que nous allons créer, mais plus tard ! Pour le moment, retenez seulement qu'elle permet de choisir un résultat (et donc de gérer tout ce qui s'ensuit) et qu'elle prend en paramètre l'élément à choisir. Nous nous intéresserons à son code un peu plus tard.

Maintenant, il ne nous reste plus qu'à détecter quand le champ de texte a été modifié.

C’est simple, à chaque fois que l'événementkeyupse déclenche, cela veut dire que le champ a été modifié, non ?

Pas tout à fait, non ! Cet événement se déclenche quelle que soit la touche relâchée, cela inclut donc les touches fléchées, les touches de fonction, etc. Tout cela nous pose problème au final, car nous souhaitons savoir quand la valeur du champ de recherche est modifiée et non pas quand une touche quelconque est relâchée. Il y aurait une solution à cela : vérifier que la touche enfoncée fournit bien un caractère, cependant il s'agit d'une vérification assez fastidieuse et pas forcément simple à mettre en place si l'on souhaite être compatible avec Internet Explorer.

C’est donc là que notre variablepreviousValueentre en piste ! Le principe est d'y enregistrer la dernière valeur du champ de recherche. Ainsi, dès que notre événement se déclenche, il suffit de comparer la variablepreviousValueà la valeur actuelle du champ de recherche ; si c'est différent, alors on enregistre la nouvelle valeur du champ dans la variable, on effectue ce qu'on a à faire et c'est reparti pour un tour. Simple, mais efficace !

Une fois que l'on sait que la valeur de notre champ de texte a été modifiée, il ne nous reste plus qu'à lancer une nouvelle requête effectuant la recherche auprès du serveur :

else if (searchElement.value != previousValue) { // Si le contenu du champ de recherche a changé
previousValue = searchElement.value; // On change la valeur précédente par la valeur actuelle
getResults(previousValue); // On effectue une nouvelle requête
selectedResult = -1; // On remet la sélection à zéro à chaque caractère écrit
}

La fonctiongetResults()sera étudiée plus tard, elle est chargée d'effectuer une requête auprès du serveur, puis d'en afficher ses résultats. Elle prend en paramètre le contenu du champ de recherche.

Il est nécessaire de remettre la sélection des résultats à -1 (ligne 7) car la liste des résultats va être actualisée. Sans cette modification, nous pourrions être positionnés sur un résultat inexistant. La valeur -1 étant celle désignant le champ de recherche, nous sommes sûrs que cette valeur ne posera jamais de problème.

Alors, en théorie, notre code fonctionne plutôt bien, mais il manque cependant une chose : nous ne nous sommes pas encore servis de la variablepreviousRequest. Rappelez-vous, elle est supposée contenir une référence vers le dernier objet XHR créé, cela afin que sa requête puisse être annulée dans le cas où nous aurions besoin de lancer une nouvelle requête alors que la précédente n'est pas encore terminée. Mettons donc son utilisation en pratique :

else if (searchElement.value != previousValue) { // Si le contenu du champ de recherche a changé
previousValue = searchElement.value;
if (previousRequest && previousRequest.readyState < XMLHttpRequest.DONE) {
previousRequest.abort(); // Si on a toujours une requête en cours, on l'arrête
}
previousRequest = getResults(previousValue); // On stocke la nouvelle requête
selectedResult = -1; // On remet la sélection à zéro à chaque caractère écrit
}

Alors, qu'avons-nous de nouveau ? Tout d'abord, il faut savoir que la fonctiongetResults()est censée retourner l'objet XHR initialisé, nous profitons donc de cela pour stocker ce dernier dans la variablepreviousRequest(ligne 9).

Ligne 5, vous pouvez voir une condition qui vérifie si la variablepreviousRequestest bien initalisée et surtout si l'objet XHR qu'elle référence a bien terminé son travail. Si l'objet existe mais que son travail n'est pas terminé, alors on utilise la méthodeabort()sur cet objet avant de faire une nouvelle requête.

Le client : déclaration des fonctions

Une fois la mise en place des événements effectuée, il faut passer aux fonctions, car nous faisons appel à elles sans les avoir déclarées. Ces dernières sont au nombre de trois :

  • getResults(): effectue une recherche sur le serveur ;

  • displayResults(): affiche les résultats d'une recherche ;

  • chooseResult(): choisit un résultat.

Effectuons les étapes dans l'ordre et commençons par la première,getResults(). Cette fonction doit s'occuper de contacter le serveur, de lui communiquer les lettres de la recherche, puis de récupérer la réponse. C'est donc elle qui va se charger de gérer les requêtes XHR. Voici la fonction complète :

function getResults(keywords) { // Effectue une requête et récupère les résultats
var xhr = new XMLHttpRequest();
xhr.open('GET', './search.php?s='+ encodeURIComponent(keywords));
xhr.addEventListener('readystatechange', function() {
if (xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) {
// Le code une fois la requête terminée et réussie…
}
});
xhr.send(null);
return xhr;
}

Nous avons donc une requête XHR banale qui envoie les termes de la recherche à la pagesearch.php, le tout dans une variable GET nommée « s ». Comme vous pouvez le constater, pensez bien à utiliser la fonctionencodeURIComponent()afin d'éviter tout caractère indésirable dans l'URL de la requête.

Le mot-cléreturnen fin de fonction retourne l'objet XHR initialisé afin qu'il puisse être stocké dans la variablepreviousRequestpour effectuer une éventuelle annulation de la requête grâce à la méthodeabort().

Une fois la requête terminée et réussie, il ne reste plus qu'à afficher les résultats, nous allons donc passer ces derniers en paramètres à la fonctiondisplayResults():

xhr.addEventListener('readystatechange', function() {
if (xhr.readyState == XMLHttpRequest.DONE && xhr.status == 200) {
displayResults(xhr.responseText);
}
});

Passons maintenant à la fonctiondisplayResults(). Cette dernière a pour but d'afficher à l'utilisateur les résultats de la recherche. Son but est donc de parser la réponse de la requête, puis de créer les éléments HTML nécessaires à l'affichage, et enfin de leur attribuer à chacun un des résultats de la recherche. Ce qui nous donne donc ceci :

function displayResults(response) { // Affiche les résultats d'une requête
results.style.display = response.length ? 'block' : 'none'; // On cache le conteneur si on n'a pas de résultats
if (response.length) { // On ne modifie les résultats que si on en a obtenu
response = response.split('|'); // On parse la réponse de la requête afin d'obtenir les résultats dans un tableau
var responseLen = response.length;
results.innerHTML = ''; // On vide les anciens résultats
for (var i = 0, div; i < responseLen; i++) { // On parcourt les nouveaux résultats
div = results.appendChild(document.createElement('div')); // Ajout d'un nouvel élément <div>
div.innerHTML = response[i];
div.addEventListener('click', function(e) {
chooseResult(e.target);
});
}
}
}

Rien de bien terrible, n'est-ce pas ? Il suffit juste de comprendre que cette fonction crée un nouvel élément pour chaque résultat trouvé et lui attribue un contenu et un événement, rien de plus. :)

Maintenant, il ne nous reste plus qu'à étudier la fonctionchooseResult(). Basiquement, son but est évident : choisir un résultat, ce qui veut dire qu'un résultat a été sélectionné et doit venir remplacer le contenu de notre champ de recherche. D'un point de vue utilisateur, l'opération semble simple, mais d'un point de vue développeur il faut penser à gérer pas mal de choses, comme la réinitialisation des styles des résultats par exemple. Voici la fonction :

function chooseResult(result) { // Choisit un des résultats d'une requête et gère tout ce qui y est attaché
searchElement.value = previousValue = result.innerHTML; // On change le contenu du champ de recherche et on enregistre en tant que précédente valeur
results.style.display = 'none'; // On cache les résultats
result.className = ''; // On supprime l'effet de focus
selectedResult = -1; // On remet la sélection à zéro
searchElement.focus(); // Si le résultat a été choisi par le biais d'un clic, alors le focus est perdu, donc on le réattribue
}

Vous voyez, il n'y a rien de bien compliqué pour cette fonction, mais il fallait penser à tous ces petits détails pour éviter d'éventuels bugs minimes.

La correction de ce TP est maintenant terminée, n'hésitez pas à l'améliorer selon vos envies, les possibilités sont multiples.

Idées d'améliorations

Afin de ne pas vous laisser vous reposer sur vos lauriers jusqu'au prochain chapitre, nous vous proposons deux idées d'améliorations.

La première consiste à faciliter la saisie de caractères dans le champ de texte. Le principe consiste à écrire, dans le champ, le premier résultat et à surligner la partie qui n'a pas été mentionnée par l'utilisateur. Exemple :

Le premier résultat est écrit dans le champ et une partie est grisée
Le premier résultat est écrit dans le champ et une partie est grisée

Comme vous pouvez le constater, nous avons commencé à écrire les lettres « to », les résultats se sont affichés et surtout le script nous a rajouté les derniers caractères du premier résultat tout en les surlignant afin que l'on puisse réécrire par-dessus sans être gêné dans notre saisie. Ce n'est pas très compliqué à mettre en place, mais cela vous demandera un petit approfondissement du JavaScript, notamment grâce au cours « Insertion de balises dans une zone de texte » écrit par Sébastien de la Marck sur le Openclassrooms, cela afin de savoir comment surligner seulement une partie d'un texte contenu dans un champ.

La deuxième amélioration consiste à vous faire utiliser un format de structuration afin d'afficher bien plus de données. Par exemple, vous pouvez très bien ajouter des données pour quelques villes (pas toutes quand même), tout transférer de manière structurée grâce au JSON et afficher le tout dans la liste des résultats, comme ceci par exemple :

Il est possible d'afficher des données sur les villes grâce au JSON
Il est possible d'afficher des données sur les villes grâce au JSON

Cela fait un bon petit défi, n'est-ce pas ? Sur ce, c'est à vous de choisir si vous souhaitez vous lancer dans cette aventure, nous nous retrouvons à la prochaine partie, qui traitera du HTML5.