Accueil Nos publications Blog HTML 5 : Introduction aux web components

HTML 5 : Introduction aux web components

HTML5Depuis longtemps, nous cherchons et nous avons des moyens de développer nos applications web sous forme de modules.

En effet, en Java avec SiteMesh, en PHP avec include(), en .Net WebForms avec les user controls, en .Net MVC avec les vues partielles, etc., il est possible de définir dans nos pages HTML des zones qui peuvent tout aussi bien être du code HTML que des composants/contrôles réutilisables. 

Le point commun entre toutes ces méthodes est que c’est côté serveur que l’on s’occupe du rendu de ces zones.

Le HTML 5 nous offre aussi cette possibilité grâce aux web components. Cette fois, c’est côté client que l’on s’occupe de l’insertion du HTML dans nos pages, sans y perdre en performance, voire l’inverse et je vais vous expliquer comment.

Qu’est-ce qu’un Web Component ?

Avec le HTML 5, plusieurs nouveautés sont apparues pour rendre le web visuellement plus dynamique et plus complexe, mais aussi pour qu’il soit plus facile à développer. Parmi elles, plusieurs principes comme :

  • les animations et les transitions en CSS
  • la mise en cache coté client pour gérer les coupures réseau
  • la possibilité d’utiliser nos propres balises dans notre code HTML en respectant les normes
  • la possibilité d’importer et réutiliser du HTML dans d’autres documents HTML

Et plein d’autres.

Un web component, c’est la fusion de 3 grands principes/composants du HTML 5 qui sont les 2 derniers cités dans le paragraphe précédent : les importsles custom elements mais aussi ce que l’on appelle le shadow DOM, quelque chose qui permet de customiser un élément en isolant son contenu sans toucher au DOM de la page. Je décrirai plus en détails ces 3 principes dans cet article.

En fusionnant les 3, on obtient donc un web component. Pour ceux qui ne l’ont pas encore deviné, c’est un custom element (donc avec une balise que l’on a inventée), dont on importe le contenu HTML grâce aux imports. Son contenu n’est pas inclu dans le DOM mais dans le shadow DOM de cet élément.

1) Le shadow DOM

Avec le principe de shadow DOM, il est possible de créer un DOM propre à un élément. Ce DOM n’est directement accessible ni par le DOM principal ni en CSS, ni en javascript.

Pour vous imaginer la chose, prenez l’exemple de la balise <video>. Dans notre DOM, ce n’est qu’une balise comme les autres et pourtant, quand on l’utilise, on peut voir des boutons, une progress bar, des images, etc. En javascript ou en CSS, nous n’avons pas accès à ces boutons directement. On doit passer par des fonctions propres à l’élément. Si video était notre élément et qu’il n’était pas natif en HTML, c’est dans son shadow DOM que l’on aurait défini son contenu et ses fonctions.

Si vous essayez ce code sur Google Chrome, vous comprendrez mieux le principe de shadow DOM.

Nous avons créé un shadow DOM à un élément div pour qu’il contienne un h1 contenant le texte “Hello world!”.

index.html :

<html>
    <head>
        <style>
            h1{
                color: red;
            }
        </style>
    </head>
    <body>
        <div id="hello-world"></div>
        <script>
            var root = document.getElementById("hello-world").createShadowRoot();
            var content = document.createElement("h1");
            content.innerText = "Hello world!";
            root.appendChild(content);
        </script>·
    </body>
</html>

Remarque : h1 aurait pu être remplacé par n’importe quel élément.

Ici, la variable root correspond à la racine du DOM de notre élément. On peut donc accéder à tous les éléments du shadow DOM de notre div à travers cette variable.

Si on procède à l’inspection de l’élément, on peut remarquer quelque chose d’inhabituel. A la place d’avoir un h1 directement enfant du div, on a un #shadow-root qui signifie simplement qu’on est dans le shadow DOM de notre div.

Il est alors impossible d’atteindre le h1 directement depuis le DOM ce qui veut dire que, ni en javascript, ni en CSS, on ne pourra le modifier. C’est ce que l’on observe lorsque l’on remarque que le style du h1 n’est pas appliqué (le texte devrait être rouge).

Un élément du shadow DOM n’a pas accès au DOM non plus, ce qui permettra de faire de petits parcours pour modifier nos éléments. Par contre, comme on a accès à notre élément dans le DOM et le shadow DOM, rien n’empêche de lier les 2 via les attributs de notre élément, par exemple. Ce qui veut dire que si on voulait modifier la couleur du h1 à partir du DOM principal, on pourrait passer par un attribut (custom ou pas) de notre élément pour réutiliser sa valeur dans le shadow DOM.

Jusqu’à maintenant, on n’a fait qu’insérer du code dans le shadow DOM d’un div, mais si on veut utiliser nos propres balises, ce sont les custom elements qu’il faut regarder.

2) Les custom elements

Comme il est dit un peu plus haut, HTML 5 supporte le fait que l’on puisse inventer et utiliser nos propres éléments. C’est ce que l’on appelle un custom element. Ce principe est beaucoup plus simple à comprendre. Imaginez maintenant qu’à la place du div et de son id, on avait directement un élément <hello-world></hello-world> qui faisait la même chose.

Il suffirait alors de créer notre élément et de lui créer un shadow DOM. Mais ce n’est pas qu’une histoire de balise. Quand on parle de HTML 5, il faut directement penser HTML ET Javascript. Car la plupart de ce que l’on trouve en HTML, on peut le créer et le manipuler en javascript. Ce que j’essaye de vous dire, c’est que pour créer un élément, il ne suffit pas d’insérer une balise inconnue dans notre code HTML. Si on veut qu’elle soit reconnue, qu’elle ait des propriétés custom de base ou que l’on puisse la créer aussi simplement qu’un autre élément HTML natif, il faut “l’enregistrer”. Pour ça, on a la fonction document.registerElement(nom-de-l-element, options).

Cette fonction va nous permettre de dire au navigateur qu’il va devoir prendre en compte notre nouvel élément et agir avec comme si c’était un élément de base du HTML. Il va donc le surveiller pendant toute sa durée de vie et exécuter les différents callbacks qu’il faut, au bon moment, sur l’élément.

Par exemple, pour notre élément hello-world, on va créer une “classe” HelloWorld en utilisant la fonction Object.create(proto[, propriétés]) :

<html>
    <body>
        <hello-world></hello-world>
        <script>
          var HelloWorld = document.registerElement('hello-world', {
            prototype: Object.create(HTMLElement.prototype, {
              createdCallback: { // exécuté à chaque création d'un élément <hello-world>
                value: function() {
                      var root = this.createShadowRoot();
                      var content = document.createElement("h1");
                      content.innerText = "Hello world!";
                      root.appendChild(content);
                }
              }
            })
          });
        </script>
    </body>
</html>

A la création de chaque élément hello-world que le navigateur reconnaitra, il exécutera la fonction que l’on a mise dans la valeur de createdCallback. Il mettra donc le code voulu (le h1) dans son shadow DOM.

A cette étape, on peut déjà comparer notre résultat à ce que l’on a avec Angular JS 1 qui nous permet aussi de créer des éléments (via les directives). Avec Angular JS 1, on remplace directement l’élément par le contenu de son template. Ici, on utilise le shadow DOM donc on n’aura pas accès au contenu de l’élément à partir du DOM (repensez à l’exemple de la balise video) ce qui veut aussi dire qu’en manipulant le contenu de notre élément, on ne parcourra plus tout le DOM principal mais seulement celui de notre élément. La différence au niveau performance n’est pas négligeable. C’est surement la raison pour laquelle Angular JS 2 gérera les shadow DOM. Ils ont fait un document à ce sujet.

Remarque : On a accès à l’élément/objet lui-même dans la fonction. C’est pour cette raison que l’on peut utiliser le mot-clé this qui réfère directement à l’élément. Donc si on veut avoir accès à ses attributs, on peut utiliser this.getAttribute(nom-de-l-attribut).

Par exemple, si on veut qu’à la place de “Hello world!” on ait un nom que l’on aurait spécifié dans un attribut (custom ou non), il suffit de lui faire correspondre.

Donc :

  • on change <hello-world></hello-world> en <hello-world name="Ludovick"></hello-world>
  • on en ajoute un autre <hello-world name="Sophia"></hello-world>
  • et on remplace content.innerText = "Hello world!"; par content.innerText = "Hello " + this.getAttribute("name")+"!";

Notez que c’est grâce à la fonction create que l’on pourra ajouter des attributs et des fonctions propres à nos éléments.

Sachant que c’est ici que l’on gère nos attributs, on peut se rendre compte que les shadow DOM nous permettent aussi de complètement contrôler l’accès au contenu de nos éléments. C’est nous qui définissons les attributs qui seront pris en compte et donc, ce qui est personnalisable.

Ce qui donne index.html :

<html>
    <body>  
        <hello-world name="Ludovick"></hello-world>
        <hello-world name="Sophia"></hello-world>
        <script>
          var HelloWorld = document.registerElement('hello-world', {
            prototype: Object.create(HTMLElement.prototype, {
              name: {                 // optionnel si on n'a pas besoin de valeur par défaut
                value: "Mylan",        // valeur par défaut de l'attribut name
                writable: true,
                enumerable: true,
                configurable: true
              },
              createdCallback: { // exécuté à chaque création d'un élément <hello-world>
                value: function() {
                  var root = this.createShadowRoot();
                  var content = document.createElement("h1");
                  var name = this.getAttribute("name");

                  if(name != null && this.name != name){
                    this.name = name;
                  }

                  content.innerText = "Hello " + this.name +"!";
                  root.appendChild(content);
                }
              }
            })
          });
        </script>
    </body>
</html>

Note : Pour éviter d’utiliser la fonction getAttribute() et faire du data binding pour que notre attribut et notre propriété se mettent à jour automatiquement, on peut utiliser la fonction Object.observe(objet, callback).

Et voilà, on a fini notre élément personnalisé.

Le problème maintenant, c’est qu’on vous a promis du HTML simple alors que là, on ne fait que mettre du contenu en javascript dans des éléments et ça a l’air plus compliqué qu’autre chose. On aimerait bien faire sortir ces scripts de notre DOM… C’est là qu’interviennent les imports.

3) Les imports

Un import est un moyen d’importer et de réutiliser du HTML dans du HTML (en HTML).

Notre but ici, va être de définir le contenu de nos éléments hello-world dans un autre fichier que index.html et de l’inclure grâce à un import. C’est assez simple à faire, mais il y a des choses à savoir avant.

Pour maîtriser la notion d’import, il faut avoir compris qu’on pouvait manipuler plusieurs DOM sur une même page. Le DOM principal (index.html), le DOM des éléments (les shadow DOM), et maintenant, on va apprendre à manipuler des DOM venant d’autres fichiers grâce aux imports.

3.1) Maîtrisons les imports

Vous n’êtes pas obligés de lire ce sous-chapitre (à part la section “attention”), car dans le cas des web components, nous n’allons pas manipuler les DOM des autres fichiers. Il reste cependant très intéressant de savoir ce qui existe et comment ça fonctionne.

Les imports se font via la même balise que l’on utilise pour inclure du style en CSS, la balise <link>. Plus exactement de cette manière :

<head>
    ...
    <link rel="import" href="chemin/vers/notre-fichier.html">
    ...
</head>

Il est possible d’importer plusieurs fichiers pouvant eux-mêmes en importer d’autres (qui peuvent déjà être dans la liste des fichiers importés) mais quel que soit l’arbre de dépendances des imports, chaque fichier importé ne le sera qu’une fois. Ce qui pourra considérablement augmenter nos performances.

Pour accéder au contenu d’un fichier importé, on doit encore passer par du javascript. Il faut d’abord accéder à notre élément link. Ensuite, on accède au DOM du fichier importé grâce l’attribut content du link.

    var link = document.querySelector('link[rel="import"]');
    var content = link.import;

Il est alors possible de récupérer des éléments de cette manière :

    var myElement = content.querySelector('#myElement');

puis les manipuler comme on le veut.

ATTENTION :
Quand un fichier est importé via un import, la variable document que l’on utilise en javascript devient le document de la page en cours. Il est donc possible que l’on se retrouve avec des erreurs parce qu’on ne manipule pas le bon DOM en javascript. Pour éviter ce problème, il est possible d’accéder au DOM du script dans lequel on écrit grâce à cette ligne.

    var myDoc = document.currentScript.ownerDocument;

On pourra utiliser myDoc pour manipuler le DOM de notre fichier importé.

3.2) Revenons à nos éléments

Notre problème était que l’on avait des scripts pas très jolis dans notre index.html qui injectaient du texte (avec de la concaténation en plus…) dans le shadow DOM de nos éléments.

Plutôt que de mettre le javascript dans un fichier .js, on va utiliser le principe de template et mettre le contenu de nos éléments dans un fichier .html.

L’avantage de cette méthode par rapport à l’utilisation d’un fichier .js, c’est que l’on programme directement ce que l’on veut (en HTML) sans passer par des chaînes de caractères pour le HTML mais aussi pour le style. Il est même possible d’inclure des fichiers .css grâce à la méthode d’import que l’on a vu précédemment.

On met donc le HTML voulu dans une balise <template> pour ensuite l’insérer dans le shadow DOM des éléments automatiquement grâce à ce que l’on a vu dans le chapitre précédent.

hello-world.html :

<html>
    <body>
        <template id="template">
            <style>
                #name {
                    color: green;
                }
            </style>
            <h1>Hello <span id="name"></span>!</h1>
        </template>
    </body>
</html>

On peut noter que j’ai voulu que le nom s’affiche en vert cette fois. On remarque aussi que j’ai utilisé un id dans notre élément, ce qu’on n’aurait pas pu faire avec un langage serveur. En effet, comme notre élément peut être utilisé plusieurs fois sur une page, avec un langage serveur (qui ne ferait que concaténer du HTML), on peut se retrouver avec des conflits liés aux ids qui se répètent. Comme ce qui est dans template se retrouvera dans le shadow DOM, les ids seront uniques dans leurs DOM respectifs.

Ensuite, on inclut le javascript que l’on avait avant, en pensant bien à ne pas confondre les DOM et en insérant le texte qu’il faut dans notre span.

Ce qui donne :

hello-world.html

<template id="template">
    <style>
        #name {
            color: green;
        }
    </style>
    <h1>Hello <span id="name"></span>!</h1>
</template>
<script>
    var helloDoc = document.currentScript.ownerDocument; // Ici, la variable document correspond au document de index.html. Avec helloDoc, on s'assure de bien accéder le document de hello-world.html
  var HelloWorld = document.registerElement('hello-world', {
    prototype: Object.create(HTMLElement.prototype, {
        name: {                 // optionnel si on n'a pas besoin de valeur par défaut
            value: "Mylan",        // valeur par défaut de l'attribut name
            writable: true,
            enumerable: true,
            configurable: true
          },
      createdCallback: { // exécuté à chaque création d'un élément <hello-world>
        value: function() {
          var root = this.createShadowRoot();
          var template = helloDoc.querySelector('#template'); // on cherche #template directement dans le DOM de hello-world.html
          var clone = document.importNode(template.content, true);
          var name = this.getAttribute("name");

          if(name != null && this.name != name){
            this.name = name;
          }

          clone.querySelector('#name').innerText = this.name;

          root.appendChild(clone);
        }
      }
    })
  });
</script>

Côté index.html, il ne reste plus qu’à supprimer la balise script et importer notre hello-world.html :

<html>
    <head>
        <link rel="import" href="Chemin/Vers/hello-world.html">
    </head>
    <body>
        <hello-world name="Ludovick"></hello-world>
        <hello-world name="Sophia"></hello-world>
    </body>
</html>

Grâce à l’import du fichier hello-world.html dans index.html, le navigateur reconnaîtra automatiquement la balise hello-world et exécutera tout seul le callback de création de l’élément.

Et voilà, notre web component est terminé. Si vous voulez, vous pouvez voir et éditer le code final sur Plunker. Le code est propre et chaque élément, aussi complexe soit-il, sera créé automatiquement très rapidement. Contrairement au travail côté serveur, le contenu des éléments n’est chargé qu’une fois pour être réutilisé automatiquement donc on peut gagner énormément en performance et sur le poids de la page.

Sachez qu’il est possible d’utiliser un ou plusieurs web components dans le shadow DOM d’un autre web component ce qui offre énormément de possibilités.

L’utilisation d’un web component présente donc beaucoup d’avantages comme :

  • Travailler sur le Shadow DOM et non le DOM ce qui fait que, dans un élément, aussi complexe soit-il, on n’a accès qu’à l’élément lui-même et son DOM. Donc contrairement à ce qu’on ferait si on avait tout dans le DOM, lorsque l’on fait des recherches d’éléments, on ne parcourt plus toute la page mais seulement l’élément.
  • Définir nous-mêmes ce qui est personnalisable dans nos éléments.
  • Réutiliser simplement nos composants grâce aux imports.
  • Nos éléments sont créés automatiquement et proprement grâce aux custom elements.
  • Le code source et le DOM sont beaucoup plus simples et clairs.
  • Les pages sont moins lourdes au chargement.
  • Augmentation des performances grâce à la gestion des imports.

Par contre, on a pu remarquer quelques inconvénients :

  • Le code des web components peut facilement devenir désorganisé
  • Le code des web components peut facilement devenir complexe
  • Seulement supporté par Chrome, Canary et Opéra à la date à laquelle j’écris l’article

Aller plus loin…

Pour vous donner une bonne idée de ce que vous permettent les web components, surtout avec la notion de shadow DOM, je vais vous donner un exemple un peu plus compliqué.

Imaginez que vous faites un réseau social et que, comme sur Facebook, vous voulez que votre page principale soit composée d’une liste de “Post” simples composés chacun, d’un texte, d’une liste de commentaires et d’un champ texte pour pouvoir ajouter un commentaire au Post voulu.

Dans cet exemple, on a une complexité que l’on retrouve souvent : On veut créer une sorte de “lien” entre le champ de texte et la liste de commentaires pour que lorsqu’on finit d’écrire notre commentaire, il s’ajoute à la bonne liste.

Souvent, on utilise l’id du Post pour pouvoir retrouver le post correspondant à notre champ de texte dans le but de chercher la liste de commentaires associée. Il y a plusieurs manières de faire. On peut même s’inventer des attributs pour nous faciliter la tâche. Mais quel que soit la manière que l’on choisira, on sera toujours obligé de parcourir le DOM principal (qui peut vraiment être énorme) et de “polluer” notre code avec des attributs ou des éléments qui ne sont pas forcément nécessaires.

Avec les web components, on peut se créer un élément simple-post. Comme le contenu de son template se retrouvera isolé du DOM principal, chaque élément pourra avoir un id simple. Pour ajouter un commentaire dans la liste qu’il faut, on pourra simplement rechercher la liste (qui sera la seule du DOM) par son id.

Ce que j’essaye de vous expliquer, c’est qu’avec le principe de shadow DOM, en plus d’isoler le code de nos éléments pour ne parcourir qu’un petit DOM, le shadow DOM nous permet de “lier” nos éléments de manière simple car “le champ de texte et la liste de commentaires du Post numéro 1234 de la colonne de droite de la troisième ligne” (parce qu’en plus, le Post numéro 1234 peut être affiché plusieurs fois sur la page) devient juste “champ de texte” et “liste de commentaires” avec abstraction de tout ce qui se trouve à l’extérieur.

Pour nous débarrasser de nos inconvénients

On croirait rêver en voyant le titre du chapitre, mais c’est vrai. Il existe
des librairies comme Bosonic et Polymer de Google (que je vous présenterai surement très prochainement), qui nous aident à mieux organiser notre code et nous offrent des outils qui nous permettent de créer des web components beaucoup plus simplement. Pour ce qui est de la compatibilité avec les autres navigateurs, on a des frameworks comme Polyfill qui changent énormément la compatibilité de nos nouveaux outils.

On se retrouve donc avec des frameworks permettant d’être supporté par des navigateurs moins récents, d’utiliser des {{expressions}} (comme dans Angular JS) dans nos templates, de faire simplement du data binding avec nos variables js, de charger les données pour nos éléments en AJAX assez simplement et plein d’autres fonctionnalités très impressionnantes.

Conclusion

Vous savez maintenant ce qu’est un web component et vous êtes même capables de créer les vôtres. Si vous avez l’intention de vous y mettre, je vous conseille vivement de jeter un coup d’œil à Polymer qui va vraiment vous faciliter la vie.

 

Sources