Indicateur de chargement non obstructif, avec prototype.js

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

Nous avons vu au cours d'un précédent article comment afficher un indicateur de chargement pendant l'exécution de requêtes Ajax lorsque l'on utilise le framework prototype.js.

Mais les implémentations que j'avais proposé souffraient toutes d'un défaut : le code HTML correspondant à l'indicateur de chargement était intégré directement dans la page, ce qui peut présente quelques inconvénients :

  • Ce code doit être dupliqué sur toutes les pages, ce qui est problématique en termes de maintenance,
  • L'indicateur, au chargement de la page, est masqué en CSS ; un visiteur utilisant un navigateur ne supportant pas les CSS[1] le verra donc toujours.

Une meilleure solution serait de créer dynamiquement l'indicateur de chargement en JavaScript ; de la sorte, il n'apparaîtrait que pour les utilisateur dont le navigateur supporte les appels Ajax - que pour les utilisateurs à qui il apporte une information, donc. Et, au passage, il sa création n'aurait à être présente que dans un fichier .js : il ne serait plus dupliqué sur toutes les pages du site ; celui-ci gagnerait donc en maintenabilité.


Exemple utilisé pour cet article

Pour cet article, je reprendrais l'exemple que nous avons déjà utilisé maintes fois :

  • Une page XHTML, qui contient un lien ; lors du clic sur ce lien, une fonction JavaScript sera appelée.
  • Un fichier JavaScript, contenant la fonction appelée lors du clic sur le lien contenu dans la page XHTML, donc le rôle est :
    • de déclencher la requête Ajax
    • et de gérer les données renvoyées
  • Et un programme côté serveur (ici encore, en PHP), qui sera appelé via la requête Ajax.

Page XHTML cliente :

Pour nos tests, la page HTML utilisée sera des plus simples :

<?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.1_rc2.js"></script>
    <script type="text/javascript" src="test4.js"></script>
</head>
<body>
    
    <p>
        <a href="" onclick="gestionClic(); return false;">
            Cliquez ici !
        </a>
    </p>
    
    <div id="resultat">&nbsp;</div>
    
</body>
</html>
  • Inclusion de prototype.js (au moment où j'écris ceci, la version la plus récente est la 1.5.1 RC2 ; cela dit, les fonctionnalités utilisées ici existent depuis les versions antérieures).
  • Inclusion du fichier JavaScript que nous développerons, qui contiendra en particulier notre fonction réalisant une requête Ajax.
  • Définition d'un lien ; la fonction JavaScript gestionClic sera déclenchée lors d'un clic sur celui-ci.
  • Et enfin, définition d'un bloc d'id resultat, destiné à être renseigné par le retour de la requête Ajax.


JavaScript - requête Ajax :

Le principe utilisé pour exécuter une requête Ajax en arrière-plan est exactement le même que pour d'autres articles : nous utilisons une instance de l'objet Ajax.Request que nous fournit prototype.js :

function gestionClic()
{
  var myAjax = new Ajax.Request(
      './test4-server.php', 
      {
        method: 'post', 
        postBody: '',
        onComplete: function (originalRequest, personnes)
          {
            if (personnes != null)
            {
              var str = '';
              for (var i=0 ; i < personnes.length ; i++)
              {
                var personne = personnes[i];
                str += personne.prenom + ' ' + personne.nom + '<br />';
              }
              $('resultat').innerHTML = str;
            }
            else
            {
               $('resultat').innerHTML = 'Données JSON invalides';
            }
          }
      }
    );
} // gestionClic()

La gestion des données retournées est des plus simple :

  • Notre fonction JavaScript attend que le programme serveur retourne des données au format JSON
  • Celles-ci doivent correspondre à une liste d'objets dotés chacun d'une propriété nom et d'une propriété prenom
  • Les résultats seront affichés dans le bloc prévu plus haut.

Vous noterez que nous ne nous pré-occupons pas vraiment de la gestion d'erreur : ce n'est pas le but de cet article.


Programme serveur

Côté serveur, nous repartons avec un programme tout simple : une liste au format JSON en sortie[2], et une attente de quelques secondes, pour nous permettre de voir apparaître, côté client, notre indicateur de chargement :

<?php
    header('Content-Type: text/html; charset: UTF-8');
    $str = '[{"nom": "Lerdorf", "prenom": "Rasmus"}, '
           . '{"nom": "Gutmans", "prenom": "Andi"}, '
           . '{"nom": "Suraski", "prenom": "Zeev"}]';
    sleep(3);
    header('X-JSON: ' . $str);
?>

Difficile de faire plus simple, disais-je donc...


Indicateur de chargement

Pour ce qui est du fonctionnement de l'indicateur de chargement, nous utilisons la fonctionnalité de gestionnaire d'événements global offert par prototype.js, via la classe Ajax.Responders :

var myGlobalHandlers = {
        onCreate: function()
            {
                if (!$('chargement'))
                {
                    creationIndicateur();
                }
                Element.show('chargement');
            },
        onComplete: function()
            {
                if(Ajax.activeRequestCount == 0){
                    Element.hide('chargement');
                }
            }
    };
Ajax.Responders.register(myGlobalHandlers);

Nous commençons par créer un gestionnaire d'événements global, qui :

  • A lancement d'une requête Ajax :
    • appelle la fonction creationIndicateur pour créer l'indicateur s'il n'existe pas au sein de notre page HTML
    • affiche l'indicateur
  • A la fin d'une requête Ajax, s'il n'y en n'a plus en cours :
    • masque l'indicateur

Et nous l'enregistrons, à l'aide de la méthode Ajax.Responders.register.

Pour plus de détails, n'hésitez pas à consulter l'article Afficher un message de chargement pendant l'exécution des requêtes Ajax.


L'utilisation d'un gestionnaire d'événements global nous permet de rendre l'affichage / le masquage de l'indicateur de chargement totalement indépendant de l'implémentation de tous nos appels Ajax au sein de la page, ainsi que distinct du contenu de celle-ci.

L'idéal serait maintenant de parvenir à rendre sa création même distincte du corps de la page, pour ne pas :

  • avoir à le définir sur chacune de nos pages
  • qu'il soit présent dans le code HTML si le navigateur de votre visiteur n'en n'a pas besoin (navigateur ne supportant pas JavaScript, par exemple)

Nous allons donc passer à différentes implémentations possibles pour la méthode creationIndicateur...


Première méthode : Injection d'un bloc HTML dans le document

Pour ne pas inclure la définition du bloc indicateur de chargement dans le corps de la page HTML, contrairement à ce que nous avions fait lorsque je vous ai présenté comment afficher un indicateur de chargement lors de requêtes Ajax, la première solution envisageable est, simplement, d'ajouter celui-ci au document HTML en JavaScript, lorsque nous voulons l'afficher pour la première fois :

// En incluant un bloc HTML directement dans le document :
function creationIndicateur()
{
    new Insertion.Top(document.body, '<div id="chargement" style="display: none; position: absolute; right: 0px; top: 0px; color: red;">? Chargement...</div>');
} // creationIndicateur()

Avec cette méthode, nous gagnons sur un point : notre bloc indicateur de chargement n'est ajouté à la page HTML que si c'est nécessaire.

Cela dit, on pourrait imaginer des solutions plus "jolies" que l'insertion brutale d'un bloc HTML...


Seconde méthode : via le DOM

Par exemple, nous pourrions ajouter notre bloc HTML en passant par une petit manipulation du DOM, comme ceci :

// En créant le bloc en manipulant le DOM,
// et en paramétrant directement les styles :
function creationIndicateur()
{
    var indicateur = document.createElement('div');
    Element.extend(indicateur);
    indicateur.id = 'chargement';
    indicateur.setStyle({
        display: 'none',
        position: 'absolute',
        right: '0px',
        top: '0px',
        color: 'red'
    });
    indicateur.innerHTML = '? Chargement...';
    document.body.appendChild(indicateur);
} // creationIndicateur()

En quelques mots :

  • Nous créons un nouvel élément ; une balise 'div', en l'occurrence
  • Nous nous assurons que les méthodes proposées par prototype.js pour manipuler le DOM soient appliquées à notre élément, à l'aide de la méthode Element.extend
  • Nous définissons l'id qui sera affecté à notre élément, pour pouvoir y accéder par la suite via la méthode $() de prototype.js[3]
  • Nous initialisons les styles de notre indicateur (invisible par défaut, rouge, en haut à droite, ...)
  • Nous définissons le texte qui sera affiché comme indicateur de chargement
  • Et enfin, nous ajoutons ce nouvel élément à notre document - au corps de la page HTML, plus précisément.

Au niveau des avantages, c'est plus flexible que la méthode précédente - au sens que cela nous permet plus d'opérations - même si ce n'est guère vraiment illustré ici.

Mais nous avons toujours un inconvénient : les styles sont définis au sein du code JavaScript ; nous ne respectons donc pas encore le principe de séparation contenu / manipulations / présentation...


Troisième méthode : via le DOM, en utilisant une feuille de styles externe

Enfin, voici une troisième solution : comme au-dessus, nous manipulons le DOM pour créer le bloc correspondant à notre indicateur de chargement, mais son style ne sera plus défini directement en JavaScript : nous ferons référence à une feuille de style CSS.


En se basant sur un id d'élément

En premier lieu, si nous souhaitons que le style se base sur l'id de notre élément, nous définirons dans notre CSS :

#chargement {
    position: absolute;
    right: 0px;
    top: 0px;
    color: red;
}

Et, côté JavaScript, nous n'avons qu'à créer l'élément :

// En créant le bloc en manipulant le DOM,
// en utilisant une feuille de style externe
// (en se basant sur un id d'élément)
function creationIndicateur()
{
    var indicateur = document.createElement('div');
    Element.extend(indicateur);
    indicateur.id = 'chargement';
    indicateur.innerHTML = '? Chargement...';
    document.body.appendChild(indicateur);
} // creationIndicateur()

Je ne rentrerai pas dans les détails : cela correspond à ce que j'ai présenté plus haut, sans la définition des styles "en dur".


Ou sur une classe CSS

Et si nous préférons utiliser une classe CSS plutôt que de nous baser sur l'id de l'élément :

.indicateur {
    position: absolute;
    right: 0px;
    top: 0px;
    color: red;
}

Et côté JavaScript :

// En créant le bloc en manipulant le DOM,
// en utilisant une feuille de style externe
// (en se basant sur une classe d'élément)
function creationIndicateur()
{
    var indicateur = document.createElement('div');
    Element.extend(indicateur);
    indicateur.id = 'chargement';
    indicateur.addClassName('indicateur');
    indicateur.innerHTML = '? Chargement...';
    document.body.appendChild(indicateur);
} // creationIndicateur()

La différence étant que nous avons du rajouter une instruction pour que notre élément utilise la classe CSS voulue.


Dans un cas comme dans l'autre, nous avons enfin atteint notre objectif, à savoir, disposer d'un indicateur de chargement s'affichant automatiquement lors de requêtes Ajax, en séparant :

  • les données (page HTML)
  • les manipulations (JavaScript)
  • et la présentation (CSS)


Notes

[1] Tablette braille, logiciel de synthèse vocale, robot d'indexation de moteur de recherche, ...

[2] En en-tête X-JSON, comme attendu par prototype.js

[3] Un alias de document.getElementById()