La notion de Web Component en Javascript

Depuis maintenant plusieurs années, le monde du développement Web a vu arrivé un nombre incalculable de Framework et autres librairies JavaScript afin de faciliter la vie des développeurs front-end. L’un d’entre eux, Angular, s’est imposé comme étant l’un des Frameworks les plus populaires de sa génération, en proposant une méthodologie robuste afin de rendre le code client bien plus maintenable et performant. Concrètement, le but d’Angular est de proposer la méthodologie MVW (Model / View / Whatever) côté client tout en apportant les bienfaits d’une Single Page Application avec la puissance du langage JavaScript.

Aujourd’hui, Angular s’est universalisé et on ne parle plus réellement de version 2, 3 ou 4. Utilisant toutes les facettes de l’HTML et du Javascript récent, il offre une base extrêmement solide pour construire des applications Web modernes. En corrélation avec l’équipe de Microsoft en charge du langage TypeScript, l’équipe d’Angular a complètement réécrit et repensé le Framework afin d’y apporter la performance qui lui manquait, mais aussi pour profiter de toute la puissance de la syntaxe de TypeScript afin de concevoir un code propre bien plus maintenable qu’il ne pouvait l’être auparavant.

Ces quelques lignes ci-dessous vont tenter de vous exposer tout ce qui se cache derrière la notion de Web Component : notion essentielle à la compréhension d’Angular.

Les nouvelles APIs HTML 5

Longtemps le Web a été conçu à base de balise <div> et <span> apportant finalement que très peu de verbosité au code HTML que le développeur écrivait. Certes, il existe les classes CSS permettant de styliser facilement les balises, mais d’un point de vue HTML pur, le code n’est que très peu structurer.

Ne serait-il pas mieux de pouvoir créer et utiliser une balise <left-menu> pour un menu vertical, une balise <grid> pour concevoir une grille ou encore une balise <notification> afin de concevoir un centre de notification pour l’application ? HTML 5 permet maintenant ce genre d’écriture en autorisant les Custom Elements.

Les Custom Elements permettent aux développeurs Web de concevoir leurs propres types d’éléments HTML, posant ainsi la première brique d’un concept plus large des Web Components (expliqué dans la partie suivante). Ces nouveaux éléments permettent de :

  • Définir de nouveaux éléments HTML/DOM ;
  • Créer des éléments qui étendes d’autres éléments ;
  • Concevoir des fonctionnalités personnalisés rassembler dans un même package logique et identifier via un seul tag ;
  • Etendre les APIs d’autres éléments du DOM.

La base de toute création d’un nouvel élément HTML 5 provient de la méthode document.registerElement() en JavaScript. Le code ci-dessous montre comment créer un élément à partir de rien :

var TestAngular = document.createElement('test-angular);
document.body.appendChild(new TestAngular());

Ce qui résulte au code HTML suivant qui est introduit dans le DOM de la page :

<test-angular></test-angular>

Le nommage des éléments est très important. Tous les navigateurs supportent une liste finie d’élément HTML qui sont connu. Les éléments non connue, tel que <ville> ou <client> ne vont pas forcément provoquer une erreur du parseur HTML et vont plutôt être reconnu comme étant des éléments inconnus via l’interface HTMLUnknownElement. La norme W3C préconise plutôt de rajouter un tiret (-) à chacun des « customs elements » rajoutés par le développeur afin que le parseur HTML puisse faire la différence. Cela prévient aussi des futures évolutions du standard en évitant des collisions avec les nouveaux éléments HTML 5. Au final, la balise <ville> sera plutôt <ma-ville> ou <x-ville>.

Le code JavaScript ci-dessous permet de savoir rapidement si l’élément créé est conforme aux préconisations du W3C. Pour ce faire, il suffit de tester le prototype de l’élément créé.

// "tabs" n'est pas un nom valide
document.createElement('tabs').__proto__ === HTMLUnknownElement.prototype


// "x-tabs" est un nom valide pour un élément personnalisé
document.createElement('x-tabs').__proto__ === HTMLElement.prototype

Il est également possible de rattacher des événements aux éléments personnalisés via la méthode addEventListener() et ainsi rajouter de l’interaction avec ce nouvel élément.

var myCity = document.createElement('my-city');

myCity.addEventListener('click', function(e) {
  alert('Thanks!');
});

C’est ainsi qu’il est possible de créer des éléments autonomes reliant le côté structurel de l’HTML 5 avec l’interactivité du JavaScript. Le code suivant créé un objet JavaScript avec une fonction et une propriété, le tout encapsuler dans un custom element HTML 5 et ajouté au DOM.

// Création de l'élément

var MyCity = document.registerElement('my-city', {
  prototype: Object.create(HTMLElement.prototype, {
    population: {
      get: function() { return 5; }
    },
    makeAlert: {
      value: function() {
        alert('makeAlert() called');
      }
    }
  })
});

// Création de l'élément
var xfoo = document.createElement('x-foo');

// Ajout du nouvel élément sur la page
document.body.appendChild(xfoo);

Pour finir sur cette API, il manque une chose aux éléments créés ci-dessus : du contenu. Afin de définir un HTML à l’intérieur d’un élément, il suffit d’utiliser les callback qui permettent de définir des fonctions à certains moments précis du cycle de vie de l’élément (création, attachement au DOM, détachement au DOM, changement d’attribut). Le code ci-dessous exploite le callback de création afin d’ajouter de l’HTML à l’intérieur de l’élément.

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

MyCityProto.createdCallback = function() {
  this.innerHTML = "<b>Je suis un x-city-with-markup!</b>";
};

var MyCity = document.registerElement('x-city-with-markup', {prototype: MyCityProto });

Cependant, le code à l’intérieur de notre élément n’est pas vraiment isolé du reste de la page. En effet, lorsqu’un nouvel élément HTML 5 est définit avec des événements et du comportement, il est préférable d’encapsuler le code HTML afin de laisser le monopole au nouvel élément de modifier et d’enrichir cet HTML. Pour ce faire, il suffit d’utiliser une nouvelle API : le Shadow DOM.

De manière synthétique, le Shadow DOM permet de :

  • Cacher toute la mécanique derrière un nouvel élément ;
  • Eviter d’exposer l’HTML au reste de la page ;
  • Encapsuler le style de l’élément.

Le code ci-dessous créé un Shadow DOM à l’intérieur de l’élément. Concrètement cela revient à utiliser la méthode createShadowRoot() pour la custom element courant et d’y définir l’HTML voulu à l’intérieur. Ce processus est simple et ne nécessite que très eu de code supplémentaire.

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

MyCityProto.createdCallback = function() {
  // 1. Attachement d'un Shadow DOM à l'élément.
  var shadow = this.createShadowRoot();

  // 2. Insertion d'un contenu
  shadow.innerHTML = "<b>Je suis le Shadow DOM!</b>";
};

var MyCity = document.registerElement('my-city-shadowdom', {prototype: MyCityProto });

Avec l’option « Show Shadow DOM » des DevTools, le Shadow DOM est affiché correspondant à l’HTML inséré dans le nouvel élément.

<my-city-shadowdom>
   #shadow-root
     <b>Je suis le Shadow DOM!</b>
 </ my-city -shadowdom>

Le dernier point important concernant cet API est l’utilisation d’un template pour la création du Shadow DOM. En effet, il est tout de même plus lisible de séparer l’HTML du JavaScript. Pour ce faire, les template permettent de définir des fragments d’HTML utilisable ensuite dans le JavaScript avec simplement l’identifiant de ce dernier.

Le code suivant possède les caractéristiques suivantes :

  • Enregistrement du nouveau composant ;
  • Création du Shadom DOM de l’élément avec un template ;
  • Encapsulation du DOM de l’élément. En effet, toutes les balises <p> ne deviennent pas rouges.
<template id="mytemplate">
  <style>
    p { color: red; }
  </style>

  <p>C'est le Shadow DOM, depuis un template !</p>
</template>

<script>
var proto = Object.create(HTMLElement.prototype, {
  createdCallback: {
    value: function() {
      var template = document.querySelector('# mytemplate);
      var clone = document.importNode(template.content, true);

      this.createShadowRoot().appendChild(clone);
    }
  }
});

document.registerElement('x-element-from-template', {prototype: proto});

</script>

La dernière API à explorer est l’HTML import : elle permet de facilement importer des documents HTML à l’intérieur d’autres documents HTML. De manière concrète, tout ce qu’un fichier .html accepte peut-être importer via l’API, comme par exemple des fichiers CSS ou encore des fichiers JavaScripts. Cela permet d’être beaucoup plus modulable au niveau de l’organisation du projet, mais cela donne aussi la possibilité charger de manière asynchrone les fichiers nécessaires.

Le code ci-dessous montre comment importer facilement un autre fichier .html :

<head>
  <link rel="import" href="/chemin/du/fichier/stuff.html">
</head>

L’intérêt de cette API est également le fait de pouvoir packager les imports nécessaires à l’application Web. La librairie Bootstrap est un bon exemple, car souvent le développeur a besoin d’importer plusieurs librairies et scripts comme jquery.js, bootstrap.js, bootstrap.css, fonts.css et ainsi de suite afin de faire fonctionner la librairie. Avec l’HTML import, il suffit d’importer un seul .html qui regroupe toutes les librairies.

<head>
  <link rel="import" href="bootstrap.html">
</head>

Le fait d’inclure un import dans une page permet également de parser cet import et d’exécuter les scripts qui sont à l’intérieur. Un document importer est appel » un import document, et le développeur peut manipuler le DOM de l’importe avec les API de manipulation de DOM classique. Le code ci-dessous importe un document et garde une référence du contenu en JavaScript, permettant de le manipuler par la suite :

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

L’import sera nul si :

  • Le navigateur ne supporte pas les imports HTML ;
  • La balise <link> n’a pas d’attribut rel= »imports » ;
  • La balise <link> n’est pas présente dans le DOM ;

Les règles d’import en JavaScript sont :

  • Les scripts de l’import sont exécutés dans le même ordre qu’ils sont définis et dans le même contexte que le contexte appelant.
  • Les imports ne bloquent pas les parsing de la page, et donc les scripts à l’intérieur de l’import sont exécutés de manière différé de ce parsing.

L’utilité de cet API ne s’arrête pas là, car elle permet aussi d’importer des templates.  L’avantage des templates est que le JavaScript n’est pas exécuté tant que le template n’est pas complètement inclus dans le DOM, facilitant ainsi grandement le découpage entre code structurel (la vue) et le code métier (les scripts). Le template suivant définit un script qui ne sera lancé que lorsqu’il sera complètement chargé et activé. La balise <img> ne va également charger l’image que lorsque le template sera complètement inclut dans le DOM.

<template>
    <img src="html5.png">
    <script>alert("Le template est chargé !");</script>
</Template

Le code ci-dessous importe alors le document ci-dessus, mais ne fait rien de particulier afin d’activer les scripts de l’import :

<head>
    <link rel="import" href="import.html">
</head>

<body>

    <div id="container"></div>

    <script>
        var link = document.querySelector('link[rel="import"]');

        // Récupération du template
        var template = link.import.querySelector('template');
        var clone = document.importNode(template.content, true);

        document.querySelector('#container').appendChild(clone);
    </script>
</body>

Il est clair à présent que toutes ces APIs sont reliées entre elles. Dans l’exemple ci-dessus, il est intéressant de compléter le code en rajouter l’API Shadow DOM afin d’encapsuler un peu plus encore la structure de l’import, et de rajouter un custom element afin d’encapsuler le template dans un composant JavaScript pour y rajouter de l’interactivité.

Le code ci-dessous est packagé dans un autre fichier HTML qui est importé dans la page principal de l’application. L’import s’occupe de lancer les scripts automatiquement. Il définit d’abord un template qui est ensuite chargé par script puis inséré dans un custom element intitulé <shadow-element>. L’élément se charge lui-même de récupérer l’HTML qui le compose.

<template id="template">
    <style>
        ::content > * {
            color: red;
        }
    </style>

    <span>C'est le Shadow DOM!</span>

    <content></content>
</template>

<script>

(function() {

    var importDoc = document.currentScript.ownerDocument; // importee

    // Define and register <shadow-element>
    // that uses Shadow DOM and a Template.
    var ProtoTest = Object.create(HTMLElement.prototype);

    ProtoTest.createdCallback = function() {

        // get template in import
        var template = importDoc.querySelector('#template');

        // import template into
        var clone = document.importNode(template.content, true);
        var root = this.createShadowRoot();

        root.appendChild(clone);
    };

    document.registerElement('shadow-element', {prototype: ProtoTest });
})();
</script>

Il suffit ensuite d’intégrer l’élément comme étant un custom element :

<head>
    <link rel="import" href="elements.html">
</head>

<body>
    <shadow-element></shadow-element>
</body>

Voilà au final comment ces nouvelles APIs HTML 5 se combinent entre elles afin d’offrir au développeur un moyen puissant et flexible de découper son code client. Il est évident qu’Angular utilise ces APIs mais de manière totalement transparente pour le développeur, et donc il n’a pas besoin de se focaliser sur la façon d’importer les éléments : il indique simplement un fichier qui servira de template, et la mécanique d’Angular se chargera de faire l’import. Tous ces processus n’ont qu’un seul but : la création de Web Component.

Faites tourner ! Share on Facebook
Facebook
Tweet about this on Twitter
Twitter
Share on LinkedIn
Linkedin

Laisser un commentaire

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