prototype.js : Ajax.Request : Afficher un indicateur de chargement pendant l'exécution des requêtes Ajax

13 mars 2007Ajax, Javascript, prototype.js
 Cet article a été rédigé il y a plusieurs années et peut ne plus être tout à fait à jour…

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">&nbsp;</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é :

  1. En premier lieu, nous déclarons une variable permettant de garder trace du nombre de requêtes Ajax en cours d'exécution.
  2. 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.
  3. 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.activeRequestCount est automatiquement gérée par prototype.js : elle garde trace du nombre de requêtes Ajax actuellement actives - exactement comme le faisait notre variable compteurRequetesEnCours utilisé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.


Notes

[1] On peut penser au "Chargement en cours" de gmail.com, par exemple, ou aux icônes animés qu'on trouve sur nombre sites récents/modernes se souciant un minimum de notions d'ergonomie

[2] On rencontre plus fréquemment le terme anglophone que sa pseudo-traduction - notez le