Soyez user-friendly

Dans le premier article, nous avons constaté que le fait de charger des portions de page avec Ajax posait un sérieux problème au niveau de l’historique du navigateur (entre autres). Puis, dans le second article, nous avons résolu ce problème au moyen de la manipulation du hash de l’URL et grâce à l’événement hashchange. Mais nous avons constaté certaines limites de cette technique et avons conclu qu’une API plus propre et plus robuste serait sympathique. Voyons comment l’API History de HTML5 s’en sort.

Présentation de l’API History

Comme la plupart des APIs de HTML5, cette API est vraiment simple: une nouvelle interface et un nouvel événement.

L’interface History

Le fonctionnement de cette API ne vous dépaysera pas: il s’agit simplement d’imiter le comportement dont vous avez l’habitude quand vous naviguez dans l’historique de votre navigateur:

  • Lors de l’ouverture d’un nouvel onglet, l’historique est vide.
  • Au fur et à mesure que vous naviguez, votre historique se remplit.
  • Vous pouvez naviguer en avant et en arrière dans l’historique.
  • Si, à un moment donné, vous modifiez l’URL (par un clic ou manuellement), alors que vous n’êtes pas dans l’entrée la plus récente de l’historique, les entrées suivantes sont écartées et remplacées par la nouvelle entrée.

On accède à une instance de l’interface History via window.history. Voici tout d’abord ses propriétés, qui sont toutes en lecture seule:

PROPRIÉTÉ DESCRIPTION
length Nombre d’entrées dans l’historique, page courante comprise
state Etat courant

Et voici ses méthodes, qui ne retournent aucune valeur:

MÉTHODE DESCRIPTION
go(delta) Se déplacer de delta entrées dans (positif: avant, négatif:arrière)
back() Equivalent à go(-1)
forward() Equivalent à go(1)
pushState(data, title [, url ] ) Place l’objet data en haut de la pile, avec le titre title et éventuellement modifie l’adresse pour lui ajouter url
replaceState(data, title [, url ] ) Même principe que pushState(), mais son action est de remplacer l’état courant

Il n’y a pas grand chose à dire sur les trois premières méthodes, dont le nom est assez explicite. En revanche nous allons en dire un peu plus sur pushState(), qui est davantage utilisée que sa jumelle replaceState().

Le premier paramètre, data, est l’état que l’on souhaite stocker dans la pile. En réalité, c’est un clone de l’objet passé en paramètre qui sera mis dans la pile. Pour information, le clone est construit en utilisant un algorithme dit des “clones structurés” spécifique à HTML5. Il permet de cloner une plus large palette d’objets que le clonage JSON, par exemple des dates et des expressions régulières.
Le second paramètre, title, est une chaine de caractères qui sera utilisée pour permettre à l’utilisateur d’identifier l’état dans l’historique.
Le dernier paramètre, url, est l’URL (éventuellement relative) que l’on souhaite associer à l’état. Si elle n’est pas spécifiée, l’URL de la barre d’adresse ne sera pas mise à jour (ce qui n’empêchera pas qu’une nouvelle entrée soit ajoutée à l’historique).

L’événement PopStateEvent

Evidemment, pouvoir empiler des données n’aurait pas beaucoup de sens si nous n’étions pas prévenu lorsque l’utilisateur clique sur les boutons ‘Précédent’ ou ‘Suivant’. C’est là qu’intervient le nouvel événement PopStateEventproposé par l’API History. Il est déclenché quand l’état courant est modifié et va nous permettre de réagir pour ajuster le contenu de la page.

Mise en pratique sur le site de démonstration

Si vous ne l’avez pas encore fait, il vous sera utile de lire la première partie de cette série qui explique comment le site d’exemple est construit. Par rapport à cette version, j’ai également ajouté un fragment correspondant à la page d’accueil (qui était vide jusque là). Cela nous permettra d’évoquer certains petits écueils.

Lors du clic sur un lien nous allons, en plus de la requête Ajax pour charger le contenu, ajouter un nouvel état. De cette manière nous construisons un historique de navigation.

 $('nav a').click(
    function(event) {
        event.preventDefault();
        var url = $(this).attr('href');
        loadFragment(url);
        window.history.pushState({ link: url }, '', url);
    }
);

Comme vous le voyez, on utilise la méthode pushState(). Parlons un peu des arguments qu’on lui passe dans notre cas:

  1. Un littéral objet qui ne contient qu’une propriété: le lien cliqué. Ici c’est un peu overkill bien sûr, mais c’était pour montrer qu’on peut le faire.
  2. Une chaine de caractères vide pour le titre. C’est parce que cet argument n’est pas pris en compte par les navigateurs actuellement. En fait ils mettent systématiquement dans l’historique document.title et rien d’autre pour l’instant…
  3. Le dernier argument, optionnel, est utilisé parce que nous souhaitons mettre à jour l’adresse. Cela permettra à l’utilisateur de placer un signet sur une de nos pages. Mais on peut s’en passer si on ne veut pas donner cette possibilité.

Comme pour la méthode du hashchange, il nous faut maintenant pouvoir réagir lorsque l’utilisateur navigue dans l’historique. Nous allons donc intercepter l’événement popstate:

$(window).bind('popstate',  
    function(event) {
        loadFragment(event.originalEvent.state.link);
    });

Nous n’avons rien d’autre à faire quand nous recevons cet événement, que de lire l’état devenu l’état courant et de charger la page correspondante.

Vous avez peut-être noté que j’ai été obligé d’employer la notation event.originalEvent.state pour accéder à l’état. Quand on utilise jQuery, l’événement passé au callback n’est pas l’événement du DOM, mais un événement normalisé propre à jQuery. D’habitude on peut les utiliser indiféremment parce que les propriétés de l’objet du DOM sont copiées dans l’événement normalisé, mais visiblement ce n’est pas encore le cas pour popstate. En attendant, nous réutilisons donc l’événement du DOM.

Le code précédent pose néanmoins un problème avec Safari 5, car si on regarde la console on s’aperçoit que l’on a, au premier chargement, ce message d’erreur: "TypeError: 'null' is not an object (evaluating 'event.originalEvent.state.link')". Que se passe-t-il donc ?

Gare au popstate précoce

En fait cette spécification, comme celle de la plupart des APIs HTML5, a évolué et tous les navigateurs n’en sont pas au même degré d’implémentation de la norme actuelle. Dans notre cas, certains navigateurs (comme Safari et Chrome) déclenchent cet événement au premier chargement de la page. On va donc modifier le code pour gérer le cas où la propriété event.state est nulle:

$(window).bind('popstate',  
    function(event) {
        if (event.originalEvent.state) {
            loadFragment(event.originalEvent.state.link);
        }
    });

La touche finale

Le site fonctionne assez correctement comme cela, mais il y a quand même un problème lorsqu’on revient sur la page d’accueil du site, car elle n’est associée à aucun état (event.originalEvent.state == null) puisqu’on y a accédé via un signet, un lien externe ou bien en entrant l’adresse à la main. Donc aucun pushState() n’a encore eu lieu… Nous allons remédier à ce problème en faisant en sorte d’associer un état à la page d’arrivée comme ceci:

$(document).ready(function() {
    ...
    var landingPage = getFragmentName();
    if (!landingPage) {
        landingPage = 'home';
    }
    window.history.replaceState({ link: landingPage }, '', landingPage);
    ...
}

On peut maintenant dépiler l’historique jusqu’au bout sans problème !

Conclusion

Nous avons vu qu’avec très peu de code (et du code simple qui plus est), nous pouvons vraiment mettre en oeuvre une expérience utilisateur plus conviviale. Et si le navigateur qu’il utilise ne supporte pas cette API, la situation n’en sera pas pire pour autant. Evidemment, l’API History ne sert pas cet unique objectif: puisque l’on peut associer des objets complexes à nos états, il est possible d’imaginer des mécanismes du type Undo / Redo sur des applications complexes, des jeux, etc…

comments powered by Disqus