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'objetXMLHttpRequest
afin
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.
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.
Un système d'auto-complétion se présente de la manière suivante :
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.
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.
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'attributautocomplete
de cette manière :
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 :
id="results"
Résultat 1
Résultat 2
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.
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énementkeyup
de 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.
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.
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 vieilecho
et à analyser cela côté JavaScript.
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.
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.
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>
charset="utf-8"
TP : Un système d'auto-complétion
id="search" type="text" autocomplete="off"
id="results"
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
}
});
})();
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.
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
bouclefor
possède une condition un peu particulière qui stipule
qu'elle doit continuer à tourner tant qu'elle n'a pas lu tout le
tableau$data
et 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()
retournefalse
en
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.
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 :
id="search" type="text" autocomplete="off"
id="results"
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
variablepreviousValue
vous semble douteuse, ne vous en faites pas,
vous allez vite comprendre à quoi elle sert !
L'événement utilisé
estkeyup
et 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
variableselectedResult
entre 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
deselectedResult
est 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 plusselectedResult
mais
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énementkeyup
se 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
variablepreviousValue
entre 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
variablepreviousRequest
est 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.
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éreturn
en fin de fonction retourne l'objet XHR initialisé afin
qu'il puisse être stocké dans la variablepreviousRequest
pour
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.
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 :
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 :
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.