Published by

Il y a 6 ans -

Temps de lecture 12 minutes

Introduction sur les Web Components en pur Javascript

Découvrez le fonctionnement des web components : qu’est-ce et comment les utiliser ? Pourquoi vont-ils révolutionner la manière dont nous créons des applications web ? Est-ce assez mature pour aller en production ? Si vous cherchez des réponses à ces questions, ou si vous souhaitez tout simplement découvrir ce concept, cet article est pour vous ! Nous aborderons ce qui compose un web component et comment l’utiliser. Des exemples clairs basés sur les unités d’un célèbre jeu viendrons illustrer cet article. Après avoir suivi cet article vous serez capable de créer un web component et d’en parler autour de vous !

Les composants aujourd’hui

Créer des composants sur des applications web ça existe, ça fonctionne et vous le faites déjà très souvent. Que ce soit avec les plugins jQuery, les directives d’Angular, les vues sur BackboneJS ou bien d’autres moyens. Cependant, c’est verbeux ! Vous devez utiliser une bibliothèque ou un framework.

Le minimum viable pour déclarer un composant se résume à :

<link rel="stylesheet" type="text/css" href="zooka-button.css" />
<script src="zooka-button.js"></script>

A chaque fois que vous définissez un composant, vous devez importer la bibliothèque apportant la logique au composant et une feuille de style.

 <div id="zooka-button"></div>

OK, vous avez défini les dépendances, maintenant vous devez ajouter un élément dans votre page pour y injecter une logique.

new MyZooka(document.getElementById('zooka-button'));

Une fois l’élément dans votre DOM et les dépendances ajoutées, vous devez donner vie à votre composant. L’initialisation se fait depuis un code Javascript qui est, dans le meilleur des cas, un fichier externe (par exemple : my-zooka.js) ou dans une balise <script> à la fin de votre page.

Vous en conviendrez, c’est bien verbeux pour, finalement, pas toujours beaucoup de fonctionnalité. Et ce n’est pas le seul inconvénient ! Le DOM généré après initialisation du composant peut ressembler à cela :

<div id="my-zooka">
  <div class="team">Blue</div>
  <div class="name">Zooka</div>
</div>

Cela semble convenable. Mais que se passe-t-il si, dans votre application, les classes CSS team et name existent déjà ? Si vous avez défini une taille correspondant à un titre, changé la position ou le paramètre float ? Misère, votre composant ne ressemble plus à rien :(.

Le problème provient du manque d’encapsulation des composants par rapport au reste de la page.
Heureusement, il existe maintenant une solution !

Les renforts

L’idéal serait d’avoir quelque chose comme cela :

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

Définir une bonne fois pour toutes ce qui défini le composant, à un seul endroit, au début du fichier principal.

<my-zooka/>

Un déclaration sous forme d’un élément du DOM.
Automatiquement, le navigateur sait qu’il doit résoudre ce nouvel élément avec l’import déclaré ci-avant.

Le contenu de <my-zooka/> est encapsulé dans un fragment (isolé du reste de la page).

Cela a l’air bien non ?
Cela tombe bien car c’est ainsi qu’est importé et utilisé un web component ;).

Le règlement

Depuis Juillet 2014, le W3C a commencé à définir les web components.

Le W3C propose 4 nouveaux chapitres pour définir les web components dans leur ensemble :

  1. Custom Element,
  2. HTML Template,
  3. HTML Import,
  4. Shadow DOM.

La compatibilité n’est pas encore assurée par tous les navigateurs :

Ce tableau propose une vue d’ensemble de la compatibilité des navigateurs (source are-we-componentized-yet)

Cependant des bibliothèques appelées polyfill, permettent en partie de pallier ces manques. Il existe notamment :

Web Component : 4 chapitres

Custom Element

D’après le W3C, le chapitre custom element vise à proposer un moyen pour les développeurs de créer leurs propres éléments DOM d’une manière rationnelle et unifiée. Plusieurs possibilités sont données aux développeurs pour créer des composants :

  • Créer de nouveaux éléments from scratch,
  • Hériter d’autres éléments,
  • Etendre l’API d’éléments existants,
  • Encapsuler des éléments dans d’autres éléments.

Il n’y pas beaucoup de règles pour définir un nouvel élément, seulement qu’il :

  1. doit contenir un tiret (celui du 6), aussi appelé hiphen-minus ou U+002D,
  2. ne doit pas contenir de lettre capitale,
  3. ne doit pas appartenir aux noms réservés : annotation-xml, color-profile, font-face, font-face-src, etc.

Cycle de vie

Le Custom Element a des callbacks appelés aux différentes étapes du cycle de vie :

  • createdCallback : correspond à l’état créé,
  • attachedCallback : appelé lorsque l’élément est inséré dans le DOM,
  • detachedCallback : appelé lorsque l’élément est supprimé du DOM,
  • attributeChangedCallback : appelé à chaque fois qu’un attribut du custom element change.

Voyons un exemple de définition d’un custom element :

var ZookaButtonPrototype = Object.create(HTMLElement.prototype);
document.registerElement('zooka-button', {prototype: ZookaButtonPrototype});

Avec ce code, le navigateur va automatiquement créer le web component correspondant à zooka-button. La méthode registerElement permet d’ajouter l’élément à la liste des éléments que le navigateur connait dans la page.

L’élément créé hérite d’HTMLElement (tous les éléments existants héritent d’HTMLElement).

Ajouter une action au clic sur le custom element revient à ajouter un événement dans le createdCallback :

var ZookaButtonPrototype = Object.create(HTMLElement.prototype);
ZookaButtonPrototype.who = function() {...}
ZookaButtonPrototype.createdCallback = function() {
  this.addEventListener('click', function(e) {
    ZookaButtonPrototype.who();
  });
}
document.registerElement('zooka-button',{prototype: ZookaButtonPrototype});

Voilà, votre custom element commence à avoir un comportement.

Ajoutons du contenu dans le custom element :

var ZookaButtonPrototype = Object.create(HTMLElement.prototype);
ZookaButtonPrototype.who = function() {...}
ZookaButtonPrototype.createdCallback = function() {
  this.addEventListener('click', function(e) {...});
  this.innerHTML = "<b>I am a Zooka button</b>";
}
document.registerElement('zooka-button', {prototype: ZookaButtonPrototype});

Si nous n’utilisons que le chapitre custom element des web components, voilà comment ajouter de la logique et du contenu.

Voyons comment utiliser un template au lieu d’écrire du DOM en javascript (disons-le, ce n’est pas génial).

HTML Template

<template id="zooka-template">
  <b>I am a zooka button from template</b>
</template>

Le markup du custom element peut être défini dans des balises template.

var ZookaButtonPrototype = Object.create(HTMLElement.prototype);
ZookaButtonPrototype.who = function() {...}
ZookaButtonPrototype.createdCallback = function() {
  this.addEventListener('click', function(e) {...});
  var template = document.getElementById('zooka-template');
  var clone = document.importNode(template.content, true); // node, deep (import descendant)
  this.appendChild(clone);
}
document.registerElement('zooka-button', {prototype: ZookaButtonPrototype});

Pour utiliser le template, il suffit de récupérer le template dans la fonction createdCallback, de créer une copie du markup externe pour l’insérer dans le DOM existant et le tour est joué !

Maintenant, voyons comment externaliser toute la définition du custom element dans un fichier externe qui sera appelé dans le fichier principal.

HTML Import

L’idée du chapitre HTML Import est de pouvoir stocker toute la définition d’un web component dans un fichier externe (la logique en javascript, le template en HTML et le style en CSS).

Aujourd’hui, l’import dans un fichier HTML c’est :

  • une <iframe> : pas génial,
  • un appel ajax qui récupère un markup puis qui l’ajoute à la page : verbeux,
  • écrire du code directement depuis le javascript : ou pas,
  • d’autres solutions non avouables ?

Mais heureusement, avec les web components, la ligne… :

<link rel="import" href="zooka-button.html">

… permet d’importer toute la définition du web component :

<template>...</template>
<script>
  (function() {
    var importDoc = document.currentScript.ownerDocument;
    var ZookaButtonPrototype = Object.create(HTMLElement.prototype);
    ZookaButtonPrototype.who = function() {...}
    ZookaButtonPrototype.createdCallback = function() {...}
    document.registerElement('zooka-button',
      {prototype: ZookaButtonPrototype});
  })()
<script>

La page index.html où l’import et l’utilisation du composante sont faits :

<html>
  <head>
    <link rel="import" href="zooka-button.html">
  </head>
  <body><zooka-button></zooka-button></body>
</html>

Attention cependant, le navigateur vérifie la provenance de l’import et ne tolère que les imports sur le même nom de domaine. En cas de nom de domaine différent vous aurez des erreurs CORS.

Voilà déjà beaucoup d’informations intéressantes, vous pouvez dès maintenant :

  • créer des nouveaux composants DOM, 
  • ajouter du contenu dans vos éléments provenant de markups définis dans la balise template, 
  • externaliser tout ça dans un fichier dédié. 

Il reste encore un problème : le cloisonnement du DOM et des styles.

Shadow DOM

Le chapitre Shadow DOM traite de la possibilité d’insérer des markups HTML dans le DOM général. le Shadow DOM est un DOM encapsulé, qui n’est pas directement accessible par le DOM principal. Le navigateur interprète ce DOM comme le DOM normal (un span reste un span). Le style appliqué sur le Shadow DOM ne s’applique pas au DOM principal. De la même manière le style défini pour le DOM principal ne s’applique pas au Shadow DOM.

Voyons comment créer un élément root contenant des markups shadow (sans custom element, html import et html template) :

var shadow = document.getElementById('zooka-button').createShadowRoot();
shadow.innerHTML =
    "<style>div { color: red; }</style>" +
    "<div>My name is <content></content></div>";

Ouvrez votre navigateur et regardez le contenu de l’élément zooka-button (en faisant apparaitre le code source).

<div id="zooka-button">
  #shadow-root
  |  <style>
  |    div {
  |      color: red;
  |    }
  |  </style>
  |  <div>
  |     My name is <content></content>
  |  </div>
  "Rifleman"
</div>

Bien que le CSS défini dans ce shadow DOM change la couleur du texte à rouge, seul le composant du shadow DOM est modifié.

L’encapsulation proposée par le shadow DOM n’est cependant pas complètement hermétique. Il est possible de définir un style qui s’applique depuis le shadow DOM vers le DOM principal ou inversement (vous trouverez plus d’information sur l’article Shadow DOM 201 d’Eric Bidelman).

Voici un exemple complet d’un web component défini dans une page web à part qui utilise le shadow DOM :

<html>
  <template id="zooka-template">
    <h2>I am an imported Zooka button</h2>
  </template>
  <script>
    (function() {
      var thatDoc = document;
      var thisDoc = thatDoc.currentScript.ownerDocument;
      var template = thisDoc.querySelector('template');
      var ZookaButtonPrototype = Object.create(HTMLElement.prototype);
      ZookaButtonPrototype.who = function() {
        alert('Zooka!');
      }
      ZookaButtonPrototype.createdCallback = function() {
        this.addEventListener('click', function(e) {
          ZookaButtonPrototype.who();
        });
        var clone = thatDoc.importNode(template, true); // node, deep (import descendant)
        var shadow = this.createShadowRoot();
        shadow.appendChild(clone);
      }
      document.registerElement('zooka-button', {prototype: ZookaButtonPrototype});
    })();
  </script>
</html>

Dans cet exemple nous retrouvons :

  • l’utilisation d’un template pour définir le contenu du web component,
  • la définition du prototype, qui contient :

    • la récupération du document courant,
    • la création de l’élément,
    • une méthode affichant une alert javascript,
    • un callback qui contient :

      • la création d’un listener sur le clic sur le bouton,
      • la récupération du template dans le document local,
      • le clone du template pour transformer le markup en DOM,
      • la création d’un noeud shadow,
      • l’ajout du DOM cloné dans le noeud.
    • l’enregistrement de l’élément.

Le shadow DOM recèle d’autres fonctionnalités (comme les points d’insertions) que nous n’aborderons pas dans cet article, mais je vous encourage à lire l’article Shadow DOM 201 d’Eric Bidelman si vous souhaitez approfondir le sujet.

Pour que les web components soient exploitables, il ne reste plus qu’une chose primordiale à maitriser : la communication depuis le web component vers l’extérieur et vice-versa.

Communication

Du composant vers l’hôte

La communication du composant vers l’hôte se fait par événement : le composant émet un événement. L’hôte écoute ce que le web component émet. La création de l’événement se fait dans le prototype du web component.

this.dispatchEvent(new Event('fire-tank'));

L’émission d’événement se fait avec la méthode dispatchEvent, elle prend un événement en paramètre.

document.addEventListener('fire-tank', function (event) {
    // logic
}, true);

L’hôte est capable d’écouter les événements qu’émet le web component grâce à la méthode addEventListener qui prend en paramètre l’événement à écouter et une fonction à appeler une fois l’événement reçu.

De l’hôte vers le composant

La communication depuis l’hôte vers le composant peut se faire de deux manière différentes :

  1. par surveillance des attributs,
  2. par des méthodes du prototype.
Par surveillance des attributs

Comme vu précédemment, il existe un callback appelé lorsque les attributs du web component sont changés. Ainsi, à chaque modification des attributs, le web component est capable de s’adapter :

var xElement = document.querySelector('boom-grenadier');
xElement.setAttribute('action', 'launch');

Lorsque l’attribut action est changé et que la valeur launch est appliquée, le callback attributeChangedCallback est appelé :

proto.attributeChangedCallback = function(attrName, oldVal, newVal) {
    // logic
};

Le nom de l’attribut, l’ancienne valeur et la nouvelle sont accessibles dans le callback.

Par méthode définie dans le prototype

Puisqu’un custom element étend au minimum un HTMLElement, l’API d’un HTMLElement est naturellement disponible. Dans le prototype du custom element, l’utilisateur peut définir de nouvelles méthodes qui viendront étoffer l’API existante.

var xElement = document.querySelector('boom-zooka');
xElement.fireWithBazooka(forceLevel);

L’hôte peut récupérer le custom element et appeler des méthodes de son prototype.

var proto = Object.create(HTMLElement.prototype);
proto.createdCallback =  ...
proto.fireWithBazooka = function(data) {
    // logic
};
document.registerElement('boom-zooka', {prototype: proto});

Définir une nouvelle méthode dans le prototype du custom element revient à créer une nouvelle méthode rattachée au contexte du prototype puis à enregistrer l’élément avec ce prototype.

Conclusion

Les web components apportent donc des fonctionnalités très intéressantes : les éléments custom, les templates, l’import et l’encapsulation. Les spécifications ne sont pas terminées, la compatibilité n’est pas optimale mais le sujet est très en vogue et de plus en plus de bibliothèques basées sur les web components voient le jour.

Si ce billet vous a plu et que vous souhaitez passer à la pratique et approfondir un peu, Xebia organise un Tech Event Web component  le 2 avril à 19h. N’hésitez pas à venir nous rencontrer.

 

Published by

Publié par Benjamin Lacroix

Benjamin Lacroix est lead développeur et manager chez Publicis Sapient Engineering.

Commentaire

Laisser un commentaire

Votre adresse de messagerie ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Nous recrutons

Être un Sapient, c'est faire partie d'un groupe de passionnés ; C'est l'opportunité de travailler et de partager avec des pairs parmi les plus talentueux.