Quel workflow pour intégrer Polymer à Angular ?


Polymer. Ses promesses de permettre le développement de bibliothèque de composants UI sans s’enfermer dans une stack technique, en se reposant sur les (futures) capacités du navigateur. Simplement écrire ses composants et les réutiliser partout en ayant la garantie de ne pas engendrer d’effet de bord. Ça donne envie, non ? Si vous travaillez avec plusieurs technos web et voulez développer un catalogue de composants UI générique, Polymer pourrait paraître une bonne solution.
Nous allons voir ensemble comment intégrer un composant écrit avec Polymer 2.0 dans une application Angular.

Préambule : Polymer et les Web Components

S’il devait n’y avoir qu’une chose à retenir :
Polymer != Web Components
Les Web Components, apparus pour la première fois en 2011, sont un ensemble de 4 technologies permettant de créer et d’utiliser des éléments dont le style et le code sont encapsulés du reste de l’application. Ces 4 technologies sont basées sur des spécifications du W3C dont certaines sont déjà en statut Living Standard.
En 2013, des ingénieurs de chez Google ont voulu rendre accessibles les Web Components en créant une surcouche à ces technos. Leur but étant de pouvoir commencer à écrire et utiliser des Web Components sans attendre que les spécifications soient complètes et implémentées dans suffisamment de navigateurs. Cette surcouche, du doux nom de Polymer, était vouée à s’alléger – voire disparaître – proportionnellement à la couverture des navigateurs.
Ces 4 technologies sont :

Les Custom Elements

Ce sont des API JS permettant de définir vos éléments HTML et leur comportement associé. Après l’appel à CustomElementRegistry.define(), vous pourrez ensuite utiliser le tag correspondant dans votre HTML.
Ils définissent également les Life cycle callbacks de l’élement que Polymer reprend.

Le Shadow DOM

C’est ce qui va permettre d’encapsuler la structure HTML et le style de l’élément, sans que le monde extérieur puisse interférer de manière non contrôlée.
Polymer gère le Shadow DOM pour nous, tout en nous permettant de le manipuler

Les HTML Template

Cette techno définit deux éléments. <template>, qui permet d’écrire du HTML non rendu et qui sera copié dans les éléments qui les utilisent. Le <slot> nous laisse écrire un espace dans notre structure HTML où les éléments enfants de notre élément viendront s’insérer.

Les HTML Import

Si vous avez écrit votre Web Component dans son fichier .html dédié, un moyen de l’utiliser dans votre application est de l’importer via un import HTML :

<link rel="import" href="myfile.html">

Cette techno est sujette à controverse et ne sera finalement pas adoptée par le standard. Selon Firefox, les outils présents et à venir (ES6 Module) sont suffisants et offrent plus de contrôle que les HTML Import.

Les ajouts de Polymer

Au delà de faire fonctionner ces 4 technos sur tous les navigateurs, on trouvera dans la boite à outils Polymer de quoi faciliter le développement des Web Components :
– Helper pour déclarer les propriétés de notre composant (valeur par défaut, read-only, calculé, fonction observatrice, désérialisation automatique)
– Gestion des événements (inscription & désinscription automatique, événements gestuels pour les mobinautes)
– Gestion des données, avec du 2-way data-binding
– Custom elements équivalents aux ngIf et ngFor d’Angular
– et divers utilitaires comme un debouncer pour éviter d’appeler une callback à intervalle trop serré
En plus de ces fonctionnalités de base, Polymer fourni des solutions pour développer des applications complètes (gestion des routes, internationalisation, gestion du hors-connexion), peu utiles puisque c’est déjà géré par Angular.

et Angular ?

Angular débuta après Polymer, en septembre 2014. Mais à l’inverse de Polymer, les composants Angular ne sont pas des Web Components. Ils utilisent des techniques propres à ceux-ci, comme une émulation du shadow dom (qui peut aussi être activé nativement) pour l’encapsulation du style, mais on ne peut pas utiliser un composant Angular dans un autre environnement qu’Angular.

L’intégration Angular & Polymer

Pour illustrer cette intégration, nous allons voir deux manières: l’une simple et rapide, pratique pour tester, et l’autre plus adaptée à un environnement de production.
Dans les deux cas, vous aurez besoin de bower.

La manière simple

Dans ce cas de figure, les customs elements seront développés localement, connexes au projet Angular. Les éléments sont importés dans l’index.html.

  1. À la racine de votre projet, initialisez bower avec
$ bower init

Les demandes du prompt ne sont pas intéréssantes car nous ne publierons pas via bower. Notre utilisation se cantonnera à gérer les dépendances Polymer.

  1. Toujours à la racine, créez un fichier .bowerrc avec ce contenu :
{
  "directory": "src/assets/bower_components/"
}

assets correspond à votre répertoire d’assets Angular.

  1. Dans le .gitignore, rajoutez le même chemin que le directory du .bowerrc.

Pour l’exemple, nous utiliserons des éléments développés par l’équipe Polymer.

  1. Installez les éléments paper-slider et paper-card
$ bower install --save PolymerElements/paper-slider PolymerElements/paper-card
  1. Ajoutez vos dépendances dans l’index.html.
<head>
  <link rel="import" href="assets/bower_components/paper-card/paper-card.html">
  <link rel="import" href="assets/bower_components/paper-slider/paper-slider.html">
</head>
<body>
  <app-root></app-root>
</body>
  1. Pour pouvoir utiliser vos nouveaux élements, il faut autoriser les balises inconnues par Angular. Dans chaque module déclarant des composants utilisant des custom elements, il faut appliquer la valeur CUSTOM_ELEMENTS_SCHEMA à la propriété schemas :
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from "@angular/core";
@NgModule({
  declarations: [AppComponent],
  bootstrap: [AppComponent],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
})
export class AppModule {}

Et voilà ! Si vous voulez développer vos WebComponents en local, mettez-les dans un dossier d’assets. N’utilisez pas polymer-cli qui vous générera tout le nécessaire à la publication, et entrera en conflit avec les éléments dans bower_components.
Dans votre HTML définissant votre WebComponent, vous pourrez importer le Polymer récupéré après avoir installé des éléments depuis bower :

<link rel="import" href="../bower_components/polymer/polymer.html">

La manière adaptée à la prod

Plutôt que de charger tous vos composants dans le point d’entrée de votre application, vous allez les importer dans vos componnents Angular, côté TypeScript. Ceux-ci seront intégrés dans le chunk correspondant au module de votre composant, ce qui est intéressant lorsque vous lazy-loadez vos modules. Ainsi, vous diminuez beaucoup la charge initiale.
Ceci est rendu possible grâce au projet Polymer Webpack Loader qui va transformer vos définitions d’éléments HTML en bundle JS.
PolymerWebackLoader fonctionnement
Ça tombe bien, Webpack est déjà présent et configuré via Angular CLI. Vous pouvez patcher la config générée par le CLI en utilisant Origami (Polymer + Angular) et voir un exemple d’utilisation avec ce starter-kit.
Attention cependant, si vos éléments hébergent des images, vous devrez changer la façon dont elles sont utilisées. Soit en déplaçant les images dans le dossier asset d’Angular de manière à ce que le importPath de Polymer corresponde, soit en y faisant référence via une variable :

const img = require('./checked.png');

de manière à ce que l’image soit incluse dans le bundle.

Intégration avec les formulaires Angular

Pour intégrer un élément Polymer à un formulaire Angular, il faut s’assurer qu’ils parlent la même langue !
En utilisant la directive ngDefaultControl sur votre composant, vous vous assurez qu’Angular prendra en compte les événements input émit par votre composant. C’est-à-dire que lorsque vous voulez que la valeur du FormControl Angular soit rafraîchie par l’élément Polymer, vous devrez émettre un événement input. Par exemple pour un champ texte, input pourra être émis après chaque frappe du clavier.

class DemoElem extends Polymer.Element {
  static get is() { return 'demo-elem'; }
  static get properties() {
    return {
      value: {
        type: String
      }
    };
  }
  notifyChange() {
    this.dispatchEvent(new CustomEvent('input'));
  }
}

Puis, Angular récupèrera la valeur courante via la propriété value du composant.
Si vous utilisez un WebComponent récupéré et qui n’émet pas input, vous pouvez passer par une directive qui implémentera ControlValueAccessor. L’équipe d’Angular en a développé une bonne flopée (ici) que l’on pourra utiliser comme modèle si l’on ne trouve pas son bonheur. C’est là que vous pourrez écouter l’événement du composant et interpréter le résultat avant de la passer au formulaire Angular, et inversement lorsque le formulaire Angular veut définir une valeur pour ce contrôle.
Vous pourrez retrouver un exemple d’implémentation dans le répo de démo.
Normalement, la validation d’un contrôle se fait toujours du côté d’Angular, via les Validators. Angular peut indiquer à un WebComponent que celui-ci est invalide via le data-binding. C’est géré de base par les WebComponents développés par l’équipe Polymer qui met à disposition une propriété invalid et error-message. Dans certains cas complexes, vous voudrez éventuellement gérer la validité dans le composant (pour avoir un meilleur contrôle de la mise en forme par exemple). Pour qu’Angular soit conscient de l’invalidité de ce composant, plutôt que de dupliquer le code de validation, vous pourrez intercepter un événement généré par le composant pour indiquer que son état de validité a changé.

Conclusion: L’avenir des Web Components avec Angular

Nous avons vu que l’intégration de Polymer 2.0 dans Angular pouvait complexifier le workflow (surtout s’il s’agit de contrôle de formulaire). Avec Polymer 3.0, on passera de Bower à NPM et des HTML Import aux modules ES6, rendant l’étape de build nécessaire. Comme alternative, Angular développe Angular Elements, qui permettra d’exporter nos composants Angular en Web Component utilisable n’importe où ! À l’avenir, je pense qu’il y aura donc moins d’intérêt à utiliser Polymer en plus d’Angular, si les deux permettent autant de réutilisabilité… Ce sera une affaire de goût !
Projet Github de démo
Thibault Chevrin, JS-Craftsman @JS-Republic