Web Components, l'avenir du web aujourd'hui

Publié le 5 novembre 2014 par Cédric Brancourt | front

Cet article est publié sous licence CC BY-NC-SA

Web Components, l’avenir du web aujourd’hui

Les web components pourquoi ?

Peut être avez-vous déjà souhaité pouvoir définir de nouveaux éléments HTML que vous pourriez réutiliser ou partager avec d’autres ? AngularJS avec ses directives vous a sûrement apporté un élément de réponse. Si on pousse le raisonnement un peu plus loin, on se met à rêver de pouvoir le faire nativement et de manière standardisée !

Les « web components » sont donc l’encapsulation de comportements et de structure dans un composant qui est réutilisable et qui peut être importé depuis une source externe.

J’en ai rêvé, les groupes de travail du W3C l’ont fait !

Derrière le terme web components on retrouve 4 spécifications en cours d’élaboration, dont vous trouverez un exposé succinct ci-dessous.

Malgré les exemples fournis, il est peu probable que vous arriviez à produire un résultat, pour plusieurs raisons:

  • la spécification est un travail en cours et peut changer.
  • actuellement seule la dernière version de Chrome implémente ces spécifications.

Mais pas d’inquiétude, il y a des solutions qui vont vous permettre de vous y mettre dès aujourd’hui, nous verrons cela par la suite.

Pour l’heure place à la théorie, car je pense qu’il est capital de comprendre les outils que l’on utilise et imprudent de se reposer sur des bibliothèques dont on ne comprend pas les fondements.

Pour que la théorie rejoigne la pratique, imaginons que nous souhaitions créer un composant de type compteur, composé d’un icône, d’un titre et d’une valeur numérique.

Templates

Pour que notre composant ait du corps il va lui falloir une structure interne. Les « templates » sont des éléments inertes, qui ne sont pas évalués. Ils permettent de stocker nativement du balisage et le style associé pour le réifier par la suite.

Un « template » est défini dans une balise plutôt bien nommée :

<template id="#my-notif">
    <style>
        #notif-layer {
            border: 1px solid lightgray;
        }
    </style>
<div id="notif-layer">
</div>
</template>

Tant que le « template » n’est pas instancié, il ne produira aucun effet, ni ne sera visible. Pour utiliser notre template, nous aurons besoin de quelques lignes de JavaScript:

 // Sélectionnons le template qui nous intéresse
 var myNotifTemplate = document.querySelector('#my-notif');

 // Définissons le contenu de l'instance, grâce à .content()
 myNotifTemplate.content.querySelector('#notif-layer').innerHTML = "Mon contenu"

 // C'est ici que la réification se produit, lors du .cloneNode() (.importNode() fonctionne également) le template est évalué pour que nous puissions l'insérer par exemple
 document.body.appendChild(myNotifTemplate.content.cloneNode(true));

Pour davantage de détails sur l’élément « template », vous pouvez vous reporter à la spécification

Shadow DOM

Puisque l’objectif avec les « web components », est entres autre choses de réutiliser des composants tiers dans un document HTML, sans en polluer le contenu, mais tout en permettant une interaction ; il nous faut un moyen d’encapsuler les comportements et styles de notre élément et de l’isoler du reste de la page qui le contient.

C’est le rôle du Shadow DOM.

Il permet d’insérer des sections de DOM (n’oubliez pas qu’il s’agit d’un arbre), en créant un « scope » appelé ShadowRoot sur le « Shadow Host » (votre élément qui reçoit le « ShadowRoot »)

Ce fragment de DOM inséré n’est pas directement visible dans le document hôte, mais il est rendu par le navigateur, d’où « Shadow ». (Avec Chrome il est possible d’inspecter ces éléments en activant l’option ad’hoc)

Le balisage et les styles contenus dans un « Shadow Root » sont isolés de ceux du document parent.

Exemple, si nous reprenons notre « template » défini plus haut qui deviendra notre Shadow Root.

<div class='notif'></div> // Définissons un élément qui accueillera notre Shadow Root, un Shadow Host
// Selection du host
var notifHost = document.querySelector('.notif');
// Création du shadow root pour ce host
var notifRoot = notifHost.createShadowRoot();
// On insère notre template évalué dans notre ShadowRoot
notifRoot.appendChild(document.importNode(myNotifTemplate.content, true));

Plutôt simple et sympathique, à présent les styles de notre document HTML et ceux de notre fragment de DOM sont complètement isolés. Les styles définis dans notre « template » n’auront aucun impact sur notre document et inversement.

Comme dit plus haut, le but n’est pas uniquement d’isoler la structure de notre élément, on a besoin également d’interagir avec le document parent.

Le contenu de notre « notif » ne sera pas identique pour chaque instance, l’utilisateur de notre composant souhaitera sûrement en définir le contenu déclarativement.

Reprenons notre « template » pour l’adapter légèrement :

<template id="my-notif">
    <style>
        * {
            -webkit-box-sizing: border-box;
            -moz-box-sizing: border-box;
            -ms-box-sizing: border-box;
            box-sizing: border-box;
        }

        #notif-layer {
            border: 1px solid lightgray;
        }

        #notif-icon {
            width: 16px;
        }

        ::content header {
            display: inline;
            font-weight: bold;
        }
    </style>

    <div id="notif-layer">
        <img id="notif-icon" src=""/>
        <content select=".count"></content>
        <content select="header"></content>
    </div>
</template>

Le « box-sizing » de notre style ne s’appliquera qu’à notre «Shadow Root» et ses descendants et n’aura aucun impact sur notre document principal.

L’élément « content » ajouté utilise un sélecteur CSS pour capturer le contenu du « Shadow Host » correspondant. Sans sélecteur il capturera l’ensemble du contenu.

Notez également dans les styles l’apparition d’un pseudo sélecteur (::content) qui permet d’appliquer un style au contenu du « Shadow Host ». Ici la balise header.

(Les sélecteurs utilisables sont étendus par la specification Shadow DOM

Ce contenu sera substitué à l’instanciation. Nous pouvons déclarer le contenu de notre notif de cette manière :

<div class='notif'>
    <header>Messages</header>
    <span class='count'>3</span>
</div>

Bien évidemment il est aussi possible d’accéder aux propriétés du « Shadow Host » (grâce au sélecteur @host ) mais aussi de faire le choix de ne pas totalement isoler votre composant, pour plus d’options reportez vous à la spécification

Custom Elements

Maintenant que nous avons de quoi définir et encapsuler notre composant, il serait souhaitable de pouvoir en créer un élément à part entière : « Custom Element »

Cette spécification définit comment étendre HTML en y ajoutant des éléments et comment utiliser ces éléments dans un document HTML.

Nous pourrions ajouter le support de la balise « notif » grâce la nouvelle fonction de l’API du DOM : registerElement.

Pour cela il nous faut définir un prototype héritant du prototype HTMLElement afin d’avoir la base de travail nécessaire d’un élément HTML.

var MyNotifPrototype = Object.create(HTMLElement.prototype);

Puis étendre ce prototype avec le callback qui sera utilisé à sa création pour récupérer le « template » et l’insérer dans un nouveau « Shadow Root ». (Il existe d’autres callbacks: attachedCallback, detachedCallback, attributeChangedCallback …)

Je suis conscient que cela commence à faire beaucoup d’informations à assimiler depuis le début de cet article, cette étape est juste l’automatisation des étapes précédentes, ne baissez pas les bras maintenant !

MyNotifPrototype.createdCallback = function() {
    // Creation du shadow root pour ce host
    var notifRoot = this.createShadowRoot();
    // insertion dans le document
    notifRoot.appendChild(document.importNode(myNotifTemplate.content, true));
};

Maintenant que nous avons un prototype opérationnel nous pouvons enregistrer notre nouvel élément:

var myNotif =  document.registerElement('my-notif', {prototype: MyNotifTipPrototype});

Par la suite je pourrai simplement utiliser la méthode déclarative pour instancier mon élément :

<my-notif>
    <header>Messages</header>
    <span class='count'>3</span>
</my-notif>

Dans cet exemple notez le tiret dans le nom qui est un pré-requis pour éviter les ambiguïtés avec les éléments existants.

Il existe aussi dans noms réservés à ne pas utiliser (font-face par exemple). Dans les versions précédentes de la spécification, il existait une méthode déclarative pour créer un nouveau type d’élément. Cette méthode sera sûrement de nouveau disponible une fois les spécifications stabilisées.

Si vous aimez les documents normatifs je vous invite à vous reporter à la spécification

HTML imports

Maintenant que nous avons défini notre composant, nous souhaiterions pouvoir importer cette fonctionnalité. Mais comment ?

L’une des nouveautés à venir, les « HTML imports», permettent d’inclure dans le document courant les définitions d’un autre document. Un peu à la manière de feuilles de styles et des scripts, pour lesquels on peut définir une URL source :

<link rel="import" href="src/my-notif.html">

Si je package l’ensemble de ma définition de « component » dans un seul document HTML, j’ai un composant autonome que je peux importer et au passage bénéficier de mécanismes de mise en cache ! Oui je sais c’est magnifique !

Attention, si vous référencez un document d’une autre origine, il vous faudra autoriser les CORS

Plus de détails vous attendent dans la spécification

Utilisons tout ça

Passons à l’assemblage du tout et ajoutons la glu et encore un peu d’huile de coude.

Notre composant sera défini dans son fichier html, il sera indépendant, autonome et pourra être importé grâce aux « HTML imports » :

<!-- Fichier my-notif.html, le template et ses styles -->
<template id="my-notif">
    <style>
        * {
            -webkit-box-sizing: border-box;
            -moz-box-sizing: border-box;
            -ms-box-sizing: border-box;
            box-sizing: border-box;
        }

        #notif-layer {
            border: 1px solid lightgray;
        }

        #notif-icon {
            width: 16px;
        }

        ::content header {
            display: inline;
            font-weight: bold;
        }
    </style>

    <div id="notif-layer">
        <img id="notif-icon" src=""/>
        <content select=".count"></content>
        <content select="header"></content>
    </div>


</template>

<!-- le script  -->
<script>

(function(window,document) {

    <!-- Lors de son import notre script sera évalué dans de contexte du document qui l'importe
    puisque notre script a besoin d'une référence au template du document importé, nous allons devoir ajouter un peu de logique ici.
    Nous faisons référence à « importer » pour le document appelant et « importee » pour notre document composant dans cette closure.
    -->
    var importer = document;
    var importee =  (importer.currentScript || importer._currentScript).ownerDocument;

    <!-- Comme vu précédemment, on référence le template et un prototype de HTMLElement -->
    var notifTemplate = importee.querySelector('#my-notif');
    var notifProto = Object.create(HTMLElement.prototype);

    <!-- On définit le prototype -->
    notifProto.createdCallback = function() {

        <!-- Le « Shadow Root » de l'élément courant -->
        var shadowRoot = this.createShadowRoot();

        <!-- Je veux que l url de mon composant soit définie dans un attribut src sur celui-ci
          ainsi la déclaration pourra se faire de la manière suivante
          <my-notif src='mon image'>
            <header>le texte</header>
            <span class='count'>un nombre</span>
          </my-notif>
          -->
        if (this.hasAttribute('src')) {
            this.imgSrc =  this.getAttribute('src');
        } else {
            this.imgSrc = '';
        }

        <!-- On injecte notre template qui sera instancié -->
        shadowRoot.appendChild(importer.importNode(notifTemplate.content, true));
        <!-- Sans oublier de mettre à jour l'url de l'image associée -->
        shadowRoot.querySelector('img').setAttribute('src', this.imgSrc);
    };

    <!-- On enregistre notre nouveau type d'élément -->
    var myNotif = importer.registerElement('my-notif', {prototype: notifProto});

})(window, document);

</script>

Maintenant nous pouvons utiliser notre composant où bon nous semble de la manière suivante :

<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">

        <!-- On importe notre composant -->
        <link rel="import" href="src/my-notif.html">
    </head>
    <body>
        <!-- On utilise notre composant -->
        <my-notif src="mail.png">
            <header>Messages</header>
            <span class="count">3</span>
        </my-notif>
    </body>
</html>

Voilà pour la théorie, cela peut sembler représenter beaucoup de travail, mais la clé ici est la réutilisation. Donc on capitalise au lieu de recréer.

Rassurez-vous maintenant que la théorie est connue vous allez pouvoir prendre des raccourcis dans tout ça, grâce aux bibliothèques existantes !

Mais le support des navigateurs alors ?

Les « web components » étant indéniablement la prochaine révolution du côté navigateur, il serait bien dommage de ne pas s’y préparer ou prendre de l’avance.

Comme dit précédemment seul Chrome (au moment où j’écris ces lignes) implémente l’ensemble des spécifications citées plus haut, les autres navigateurs sont en voie de le faire.

Mais notre expérience du web nous permet d’affirmer que l’implémentation sur toutes les plateformes n’est pas pour 2014, ni 2015 ni…

Bref, il va falloir combler les vides, non pas avec de l’enduit de rebouchage, mais avec des polyfills…

Les solutions sont plurielles :

  • x-tags Une bibliothèque crée par Mozilla, qui nivelle les navigateurs récents pour accueillir les « custom elements », il s’appuie sur webcomponents.js (anciennement connu sous le nom de polymer/plateform.js)
  • Polymer qui va un peu au-delà et propose un ensemble de fonctionnalités cohérentes, mais qui peuvent parfois sembler superflues.
  • Bosonic qui a pour objectif de mettre les « web components » à portée d’IE9 ( vous connaissez mon point de vue sur les navigateurs pas à jour …)

Il existe déjà des bibliothèques de composants à télécharger, ils s’appuient presque tous sur les bibliothèques comme x-tags ou polymer:

Pour aller plus loin

Cette introduction vous laisse peut-être sur votre faim ?

Pour vous exercer vous pouvez reprendre l’exemple de cet article et le compléter:

  • Que se passe t’il si l’attribut src est mis à jour ? Petit indice il y a un callback pour ça.
  • Faire de notre <span class='count'> une balise à part entière.
  • Allez faire un tour du côté de webcomponents.org

Dans un prochain article nous passerons à la vitesse supérieure avec Polymer !

L’équipe Synbioz.

Libres d’être ensemble.