prototype.js : Ajax.Request : Afficher un indicateur de chargement pendant l'exécution des requêtes Ajax
Par Pascal MARTIN le mardi 13 mars 2007, 08:00 - Développement Web - Lien permanent
Depuis les débuts du Web accessible au "grand public", les visiteurs de nos sites sont habitués à ce que leur navigateur leur donne une indication lorsqu'une page est en train de charger : barre de chargement, icône animée, ...
Les technologies Ajax permettant de charger des informations destinées à re-construire dynamiquement certaines portions de la page, l'indicateur de chargement du navigateur n'indique pas que "quelque chose se passe".
Un utilisateur cliquant sur un lien déclenchant une requête Ajax ne sait donc pas si sa demande a été prise en compte, s'il a mal cliqué, si le site est lent à répondre ou hors-service, ...
Ajax contournant le fonctionnement traditionnel du navigateur, si vous ne souhaitez pas laisser vos utilisateurs se demander si une action est en cours, c'est à votre application - à vous, donc - de gérer un indicateur de chargement[1].
Utilitaires de test
Pour les exemples que je donnerai au sein de cet article, je me baserai sur deux fichiers de test :
- Une page PHP côté serveur, qui sera appelée via une requête Ajax,
- et une page cliente en (X)HTML, qui déclenchera l'appel Ajax, et sera responsable de l'utilisation de sa valeur de retour.
Page PHP "longue"
Tout d'abord, côté serveur, nous avons besoin d'un programme qui dure "longtemps" - suffisamment longtemps pour que l'utilisateur se demande s'il se passe quelque chose, si sa demande a été prise en compte.
Et, avec la généralisation des connexions haut-débit, nos visiteurs s'attendent à ce que les réponses à leurs actions arrivent vite : quelques secondes sans indiquer à l'utilisateur que sa demande a été prise en compte, c'est déjà trop !.
Plutôt que d'écrire un gros script effectuant un traitement lourd, faisons simple pour cet exemple : en quelques lignes, voici un script PHP qui attend trois secondes, et renvoi un message sur la sortie standard :
<?php header('Content-Type: text/html; charset=UTF-8'); sleep(3); echo 'Hello World!'; ?>
(Au besoin, vous ajusterez les "3 secondes" à vos besoins, pour bien percevoir le résultat)
Page cliente
Côté client, nous utiliserons toujours le même genre de page :
- un lien, qui, une fois cliqué, provoque l'appel d'une fonction JavaScript nommée "
gestionClic", - et une zone permettant d'afficher un résultat :
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html> <head> <title>AJAX : Exemple de client</title> <script type="text/javascript" src="prototype-1.5.0.js"></script> <script type="text/javascript" src="test2.js"></script> </head> <body> <div id="chargement" style="display: none; position: absolute; right: 0px; top: 0px; color: red;"> ? Chargement... </div> <p> <a href="" onclick="gestionClic(); return false;"> Cliquez ici ! </a> </p> <div id="resultat"> </div> </body> </html>
La nouveauté ici est la présence du bloc suivant :
<div id="chargement" style="display: none; position: absolute; right: 0px; top: 0px; color: red;"> ? Chargement... </div>
Ce bloc, situé dans le coin supérieur droit de l'écran, et invisible par défaut, est celui que nous souhaiterons afficher comme "indicateur d'activité" lorsque des requêtes Ajax sont en cours.
Solution naïve :
Avant de commencer, je vous conseille de jeter un coup d'oeil aux deux articles suivants :
La solution "naïve" que nous allons mettre en place ici est la suivante : pour chaque requête Ajax, nous affichons notre indicateur de chargement dans un gestionnaire d'événement branché sur un événement levé en début de requête (onLoading, par exemple), et nous le cachons au sein d'un gestionnaire toujours appelé en fin de requête : onComplete.
Par exemple, la fonction gestionClic appelée lors d'un clic sur le lien proposé plus haut pourrait être écrite de la manière suivante :
var compteurRequetesEnCours = 0; function gestionClic() { var url = './test2.php'; var myAjax = new Ajax.Request( url, { method: 'post', onLoading: function (xhr) { // Après appel méthode open // (début de la requête Ajax) Element.show('chargement'); compteurRequetesEnCours++; }, onSuccess: function (xhr) { $('resultat').innerHTML = xhr.responseText; }, onComplete: function() { // Toujours appelé en fin de requête if (!--compteurRequetesEnCours) { Element.hide('chargement'); } } }); } // gestionClic()
En quelques mots, voici le principe utilisé :
- En premier lieu, nous déclarons une variable permettant de garder trace du nombre de requêtes Ajax en cours d'exécution.
- Puis, à chaque fois qu'une nouvelle requête est lancée, nous affichons le bloc contenant notre indicateur de chargement, et incrémentons la variable gardant trace du nombre de requêtes en cours.
- Et enfin, à la fin de chaque requête Ajax, nous décrémentons ce compteur, et, s'il a atteint 0, nous masquons l'indicateur de chargement.
Cette méthode fonctionne - si vous avez testé, vous l'aurez constaté, mais elle présente un inconvénient majeur : il vous faut la mettre en place à la main pour chaque requête !
Avec une seule requête Ajax sur la page, c'est faisable... Mais si vous en avez des dizaines, ça devient beaucoup plus lourd à développer... Et à maintenir !
prototype.js : Ajax.Responders
prototype.js permet de définir des gestionnaires d'évènements "globaux", qui seront pris en compte pour toutes les requêtes Ajax effectuées en utilisant les classes Ajax de la bibliothèque : Ajax.Request, Ajax.Updater, et Ajax.PeriodicalUpdater.
Définir un gestionnaire d'évènements global se fait en deux étapes :
- Définir un objet associant à chaque évènement la méthode gestionnaire correspondante,
- Et enregistrer ce gestionnaire, via la classe
Ajax.Responders.
Définition d'un gestionnaire d'évènements
Par exemple, nous pourrions définir un gestionnaire d'évènements global de la manière qui suit :
var myGlobalHandlers = { onCreate: function() { Element.show('chargement'); }, onComplete: function() { if(Ajax.activeRequestCount == 0){ Element.hide('chargement'); } } };
Nous définissons une méthode qui affichera notre indicateur de chargement lors de l'évènement onCreate de chaque requête Ajax, et une méthode qui le masquera à la fin de la requête, s'il n'y a plus aucune requête en cours.
Nous pouvons définir des gestionnaires d'événements globaux sur tous les événements que gère la classe Ajax.Request, que j'ai présenté au cours de l'article Gestionnaires d'événements pour Ajax.Request, ainsi que pour l'événement onCreate, déclenché en tout début de requête, lors de la création de l'instance d'objet XHR qui sera utilisée pour effectuer celle-ci.
Note : La variable
Ajax.activeRequestCountest automatiquement gérée par prototype.js : elle garde trace du nombre de requêtes Ajax actuellement actives - exactement comme le faisait notre variablecompteurRequetesEnCoursutilisée au cours de l'exemple précédent.
Enregistrement du gestionnaire d'évènements
Ensuite, pour enregistrer le gestionnaire d'événements, il suffit d'appeler la méthode register de la classe Ajax.Responders, de la manière suivante :
Ajax.Responders.register(myGlobalHandlers);
Ainsi, à chaque fois que nous lancerons une requête Ajax, notre indicateur d'activité s'affichera... Et sera automatiquement masque lorsque plus aucune requête ne sera en cours.
Et, si nous souhaitons modifier son comportement, nous n'avons à le faire qu'en un seul endroit... Ce qui est nettement meilleur en terme de facilité de maintenance de notre code !
Exemple
Pour finir, voici la version finale de la méthode gestionClic :
function gestionClic() { var url = './test2.php'; var myAjax = new Ajax.Request( url, { method: 'post', onSuccess: function (xhr) { $('resultat').innerHTML = xhr.responseText; } }); } // gestionClic()
Le traitement de gestion de l'indicateur d'activité ayant entièrement été exporté dans un gestionnaire d'événements global, notre méthode de gestion du clic sur lien n'a plus à gérer quoi que ce soit d'autre que... le clic sur le-dit lien !
Idées d'améliorations
Le mécanisme utilisé ici présente un inconvénient : dans le cas où le visiteur de votre site n'a d'actif ni JavaScript, ni CSS - un visiteur utilisant un logiciel de synthèse vocale ou une tablette Braille, par exemple - il aura toujours l'indicateur de chargement en haut de page.
Il aurait été plus judicieux de ne pas l'inclure dans le code XHTML de la page, et de le rajouter dynamiquement en JavaScript :
- Soit au chargement de la page
- Soit la première fois que nous souhaitons l'afficher.
On peut d'ailleurs se faire exactement la même remarque au sujet de l'événement onclick de notre lien : si on sépare correctement le contenu et la présentation, il n'a pas grand chose à faire dans notre code XHTML - même si, pour les exemples que j'utilise, c'est nettement plus simple et que cela nous permet de nous concentrer sur les éléments qui nous intéressent ici.
Je n'entrerai pas dans les détails d'implémentation ici : j'aurai l'occasion de parler de "Javacript non obstructif" ("Unobstrusive JavaScript"[2]) dans de futurs articles.
prototype.js : implémentation de Ajax.activeRequestCount
Pour finir, un petit détail technique : nous avons vu comment utiliser Ajax.activeRequestCount pour déterminer combien de requêtes Ajax étaient en cours de traitement... Et nous l'avons utilisé dans un Ajax.Responders.
Mais, si vous avez la curiosité de jeter un coup d'oeil aux sources de prototype.js, vous trouverez un passage qui ressemble à ceci :
Ajax.Responders.register({ onCreate: function() { Ajax.activeRequestCount++; }, onComplete: function() { Ajax.activeRequestCount--; } });
(ligne 937 de prototype version 1.5.1_rc1)
Et oui : c'est le mécanisme de gestionnaire d'événements global que nous avons vu plus haut qui est utilisé pour maintenir à jour la variable... que nous avons nous-même utilisé dans un gestionnaire d'événements global !
Il est donc tout à fait possible - nous en avons la preuve - de définir plusieurs gestionnaire d'événements globaux, au besoin.
Pour être averti lors de la publication de nouvelles entrées, n'hésitez pas à vous abonner au flux RSS ou ATOM des articles de mon blog !
Commentaires
Très intéressant cet article, mais ce qui serait super ce serait de faire une archive contenant tous tes fichiers.
(ou pour le jour où je pourrais être amené à en mettre un déjà existant à jour ! )
très intéressant;
dans l'exemple "naïf", je pense qu'il faut changer "onLoading" par "onCreate"
(Cf l'implémentation de
Ajax.activeRequestCountdans le corps de l'article)Et ce serait aussi plus proche de l'exemple donné, pour l'utilisation de
Ajax.Responders.Très bonne remarque, donc !
Merci
Mais... prototype.js en version 1.5.0 ne permet pas la surcharge de l'événement
onCreatelorsque l'on passe par l'objetAjax.Request(Et c'est cette version qui a été utilisée pour l'écriture de cet article - heureusement que j'avais le numéro de version dans l'inclusion du script depuis la page HTML, sinon, je serais encore en train de chercher pourquoi je m'étais branché sur
onLoading!)Par contre, avec une version de prototype.js plus récente, on pourrait effectivement se brancher sur
onCreate, ce qui serait plus logique, par rapport aux autres exemples...... Et l'indicateur de chargement s'afficherait un peu plus tôt.
... Et ce serait plus sûr, l'appel de la fonction branchée sur
onLoadingn'étant pas garanti - dixit la doc (testé sous IE6, FF2, et Konqueror 3.5, l'indicateur de chargement s'affichait à chaque fois)Pour information, le callback
onCreatea été rajouté en version 1.5.1(Cf liste sur http://prototypejs.org/api/ajax/options )
Note à moi-même : il faudra que j'y pense si je met un jour cet article à jour ^^
Très bon article qui fait aussi office de tutorial à prototype.js qui m'avait rebuté au premier abord, du coup je me paluche le tout à la main alors qu'utiliser ce framework facilité bien des choses...
Merci
constater aussi peu der commentaires à votre billlet me surprend sérieusement
Mais merci pour le votre, en tous cas !
ne vous inquiétez pas, vu la qualité et la clarté des articles, je pense que ce site va vite intéresser beaucoup de monde, d'autant que les infos sur prototype.js en french sont plutôt rares et obscures, on attend la suite avec impatience ! merci
Bravo à l'auteur du site, c'est une bonne initiative, continu à nous pondre des articles comme ça
La première chose que j'ai retenue en arrivant sur ce blog, est la clareté du site. Bravo ! De plus, les articles sont vraiment intéressants et donnent envie d'être lus jusqu'au bout. Merci, et continuez comme ça.
Ce site plutôt bien fait me rend bien des services!
Il y a pourtant certaines choses que je verrais améliorables :
- bien expliquer le principe de l'utilisation d'ajax (je cherche pas mal sans trouver vraiment), c'est à dire (si je ne me trompe pas et c'est un peu ca que je voudrais vérifier) : créer un objet pour effectuer la requete auprès du serveur sur une page php donnée en url (obligé? on ne peut pas faire autrement : désigner une fonction dans un fichier?). On traite cet objet en retour ou un autre je ne sais pas trop dans une fonction JS prévue spécifiquement pour traiter le retour. Bien expliquer le coté client/serveur.
- je verrais bien aussi au début de chaque bout de code un petit commentaire indiquant le fichier auquel il appartient au début j'avais un peu du mal à m'y retrouver
Un grand bravo pour ce très bon site! Ces petites critiques sont peu de choses a côté du très bon travail fait sur ce site! Je les ai tout de même écrites pour le rendre (éventuellement! ^^) encore meilleur.
En tous cas, merci pour tout et bonne continuation!
Bonsoir,
Merci
Je note vos remarques. Considérant que cet article date d'il y a deux ans, je ne pense pas que je le mettrai à jour, mais je tâcherai de m'en souvenir pour les prochains posts que je publierai -- en particulier pour ce qui est des noms de fichiers !
Bonne continuation,
Bonjour,
Je me demandais s'il était possible de créer un gestionnaire d'évenements, mais au lieu de "l'affecter" à toutes les requetes, choisir celles que l'on veut.
C'est a dire, au lieu de faire
Ajax.Responders.register(myGlobalHandlers);faire quelque chose du genre
maFonction.register(monGestionnairedEvenement);Bonsoir,
A part en affectant "manuellement" les fonctions qui vont bien aux différentes instances d'
Ajax.Request, je ne pense pas...Finalement, cela revient à faire ce qui est proposé dans la partie "Solution naïve", mais en externalisant le traitement dans une fonction, utilisée pour chacune des instances d'
Ajax.Requestpour lesquelles vous voulez avoir un indicateur de chargement.Sinon, une solution peut-être envisageable (mais pas forcément "élégante" ? ) serait la suivante :
Ajax.Responders.registeravec un gestionnaire globalPar contre, quelques commentaire dans le code indiquant à quoi sert ce paramètre faciliteront la maintenance, que ce soit pour vous dans quelques mois, ou pour un autre développeur prenant votre suite ^^
Bon courage !
Merci beaucoup pour le temps que vous m'accordez.
Bonne continuation!
j'ai un formulaire et j'ai mis le onclick sur le bouton :
ensuite dans le test2.js, je reenvoi ma page index.php
mais il me l'a recharge sans les élements post, comment faire svp?
voila ma fonction test2.js
var myGlobalHandlers = {
onCreate: function()
{
Element.show('chargement');
},
onComplete: function()
{
if(Ajax.activeRequestCount == 0){
Element.hide('chargement');
}
}
};
Ajax.Responders.register(myGlobalHandlers);
function gestionClic() {
var url = './index.php';
var myAjax = new Ajax.Request(
url,
{
method: 'post',
parameters: 'param1=valeur1¶m2=valeur2¶m3=valeur3',
onSuccess: function (xhr)
{
$('resultat').innerHTML = xhr.responseText;
}
});
} // gestionClic()
merci beaucoup
Pour passer facilement tes paramètre en Post il suffit d'utiliser une fonction de se genre :
@@
function affichemodele(){
var marque = $F('marque');
var div = $('divmodele');
var file = 'listemodele.ajax.php';
var param = 'marq='+marque;
new Ajax.Updater(div, file, {
method : 'post',
parameters : param
});
}
@@
le "var div" et la div a mettre à jour.
le "var file" et le fichier ou requête à exécuter
le "var param" et la liste des paramètre à passé via l'Ajax.
on pourrait aussi les rentré directement dans la fonction plutôt que de passé par des variable, mais je trouve ça plus simple à lire, et surtout plus efficace quand il faut débuggué à coup de Alert().
Après, il reste plus qu'a greffer un gestionnaire d'évènements là dessus, mais comme c'est bien expliquer dans ce tutoriel ça devrait être assez simple.
Voici une page fonctionnant sur IE8 et pas sur FireFox:
<html>
<head>
<title> Page de Test </title>
<script type='text/javascript' src="prototype.js"></script>
<script type='text/javascript'>
function test1()
{
var url = 'http://localhost:1180/';
var parametres = 'file=/119536.xml';
var myAjax = new Ajax.Updater(
'systemeAttente',
url,
{
method: 'get',
asynchronous: false,
parameters: parametres,
onComplete: afficheReponse,
onFailure: rapporteErreur
}
);
}
function afficheReponse (requete)
{
//affiche le XML dans le textarea
$('resultat').value = requete.responseText;}
function rapporteErreur() {
$('resultat').value = "Erreur !!!";
}
function effacer() {
Element.show('systemeAttente');
$('resultat').value = "";
}
</script>
</head>
<body>
<hr/>
<textarea id='resultat' cols=60 rows=10 ></textarea><hr/>
<div id='systemeAttente'>
</div>
<input type="button" value="Effacer" onclick="effacer();"><br>
<input type="button" value="Envoi" onclick="test1();"><br>
</body>
</html>
''Les serveur renvoit le contenu d'un fichier texte.
La version prototype.js est 1.6.0.3''
Une idée de ce qui faire la différence ?