Les composants Web

Les composants Web sont devenus en quelques années un nouveau standard du web. Ils permettent d’étendre les tags HTML existants (div, form, input, etc) pour créer de nouveaux composants HTML.  Les avantages des composants sont nombreux :

  • Votre application est découpée en petits composants, facilitant la maintenabilité (plus facile de comprendre les briques graphiques, plus facile de tester le composant)
  • Les composants sont réutilisables s’ils sont suffisamment bien conçus, vous pouvez ainsi réutiliser le composant dans différentes applications, augmentant votre productivité et la cohérence entre vos applications (si elles sont développées pour une même entreprise)
  • S’incorpore bien avec la majorité des librairies / frameworks javascript qui fonctionnent avec du HTML

Il existe de nombreuses librairies qui permettent de réaliser des composants: VueJS, React, Polymer, etc. Pour cet article, nous créerons des composants avec Polymer 3.

Polymer 3

Polymer est une librairie javascript open-source développée par Google. De nombreux clients tels que Netflix, Mc Donalds, IBM utilisent cette librairie. Google l’utilise également dans ses outils (YouTube, Google Play Music, …). Cette librairie a été conçue dans le but de simplifier autant que possible la création de composants. Nous allons passer à l’étape pratique pour essayer de comprendre le fonctionnement de Polymer.

Les prérequis pour démarrer le tutoriel

Pour commencer ce tutoriel, vous aurez besoin d’avoir d’installé sur votre machine :

  • Node
  • Polymer CLI via la commande :
    npm install -g polymer-cli
  • Votre IDE préféré

Tutoriel : création de deux composants

1. Présentation du projet

Le but de ce tutoriel est de construire une page avec 2 composants, un bouton et une barre de progression. Le clic sur le bouton démarrera un traitement quelconque et déclenchera le chargement de la barre de progression.

Pour cela, nous allons créer 3 projets :

  • Un contenant le bouton
  • Un contenant la barre de progression
  • Un contenant la page qui inclura les 2 composants

Vous pouvez récupérer l’intégralité des sources en vous rendant sur le repository Git suivant : https://github.com/ineat/polymer-tutorial

2. Initialisation des composants

Pour créer le 1er composant, placez-vous dans votre workspace courant, créez un dossier my-button qui contiendra les sources du bouton et placez-vous dedans. Exécuter la commande suivante :

polymer init

Le CLI de Polymer va vous présenter une liste de templates, sélectionnez le template polymer-3-element. Nommez le composant my-button.

Le projet généré ressemblera à cela :

  • /demo : le code source permettant d’afficher le composant avec son code source
  • /node_modules : les dépendances nécessaires au fonctionnement du composant
  • /test : les tests unitaires du composant
  • my-button.js : le code source du composant (partie template + script)

Pour vérifier que le projet fonctionne, lancez la commande suivante :

polymer serve

Le message suivant doit s’afficher :

info: [cli.command.serve]    Files in this directory are available under the following URLs
      applications: http://127.0.0.1:8081
      reusable components: http://127.0.0.1:8081/components/my-button/

Rendez-vous sur http://127.0.0.1:8081, vous devriez voir la page suivante :

Notre composant démarre bien, passons maintenant aux choses sérieuses !

Créez le 2ème composant my-progress-bar dans un nouveau dossier de la même façon que le premier avec la commande polymer init et en sélectionnant le template. Vous pouvez également faire la commande suivante :

polymer init --name polymer-3-element

Pour finir l’initialisation du projet, créez un dernier dossier main-project qui contiendra la page qui affichera les 2 composants.

A ce stade, vous devriez avoir l’arborescence suivante :

3. Composants statiques

Le fichier my-button.js est composé de plusieurs éléments:

  • la classe du composant avec 2 méthodes, template() et properties(). La première contient la partie template HTML et CSS, la 2ème contient les propriétés exposées par le composant.
  • Un appel à la méthode window.customElements.define qui permet au navigateur d’interpréter le composant.

Pour le moment, nous n’allons pas exposer de propriétés, supprimez la méthode properties().

Remplacez le contenu de la méthode template() par :

return html`
  <style>
    #dummy-button {
      width: 150px;
      height: 45px;
      background-color: #eb1e54;
      color: white;
      font-size: 1.2em;
      border-radius: 5px;
      border: none;
      outline: none;
      box-shadow: 0 0 5px lightgrey;
      cursor: pointer;
    }

    /* Ripple effect */
    .ripple {
      background-position: center;
      transition: background 0.7s;
    }
    .ripple:hover {
      background: #f55a82 radial-gradient(circle, transparent 1%, #f55a82 1%) center/15000%;
    }
    .ripple:active {
      background-size: 100%;
      transition: background 0s;
    }
  </style>
  <button id="dummy-button" class="ripple" type="submit">Click on me !</button>
`;

Relancez la commande polymer serve pour vérifier que le bouton apparait correctement, il est rouge avec un effet ripple lors d’un clic.

Passons à my-progress-bar, supprimer également la méthode properties() et remplacer le contenu de la méthode template() par :

return html`
  <style>
    :host {
      display: block;
    }
    .progress {
      height: 3em;
      width: 100%;
      background-color: black;
      position: relative;
    }

    .progress:before {
      content: attr(data-label);
      font-size: 1.5em;
      font-family: Arial;
      color: white;
      position: absolute;
      text-align: center;
      top: 10px;
      left: 0;
      right: 0;
    }

    .progress .value {
      background-color: #eb1e54;
      display: inline-block;
      height: 100%;
    }
  </style>
  <div class="progress" data-label="50% Complete">
    <span class="value" style="width:50%;"></span>
  </div>
`;

my-progress-bar devrait ressembler à ceci :

Et voilà, vous avez créer vos premiers composants ! Heureux ? Comment ça vous n’êtes pas satisfaits ? Vous trouvez que ces composants ne sont pas suffisamment dynamiques ? Vous voulez changer le contenu du bouton ou la couleur ? Pas de panique, nous allons y venir.

4. Variabiliser le style avec les custom properties

Il n’est pas possible de modifier le css d’un composant depuis un composant parent. Cependant, vous pouvez exposer des propriétés CSS pour permettre de modifier certaines propriétés stylistiques du composant.

Pour exposer des propriétés CSS, vous devez ajouter dans la partie CSS de votre composant ceci :

width: var(--button-width, 150px);

width est la propriété CSS, ––button-width est le nom de la propriété CSS, 150px est la valeur par défaut.

Ajoutons quelques propriétés au style du composant my-button :

#dummy-button {
  width: var(--button-width, 150px);
  height: var(--button-height, 45px);
  background-color: var(--button-background-color, #eb1e54);
  color: var(--button-color, white);
  font-size: var(--button-font-size, 1.2em);
  border-radius: 5px;
  border: none;
  outline: none;
  box-shadow: 0 0 5px lightgrey;
  cursor: pointer;
}

/* Ripple effect */
.ripple {
  background-position: center;
  transition: background 0.7s;
}
.ripple:hover {
  background: var(--button-background-ripple-color, #f55a82) radial-gradient(circle, transparent 1%, var(--button-background-ripple-color, #f55a82) 1%) center/15000%;
}
.ripple:active {
  background-size: 100%;
  transition: background 0s;
}

Faisons de même pour my-progress-bar :

.progress {
  height: var(--progressbar-height, 3em);
  width: 100%;
  background-color: var(--progressbar-background, black);
  position: relative;
}

.progress:before {
  content: attr(data-label);
  font-size: var(--progressbar-font-size, 1.5em);
  font-family: Arial;
  color: var(--progressbar-text-color, white);
  position: absolute;
  text-align: center;
  top: var(--progressbar-top-position, 10px);
  left: 0;
  right: 0;
}

.progress .value {
  background-color: var(--progressbar-value-background, #eb1e54);
  display: inline-block;
  height: 100%;
}

Pour vérifier que tout fonctionne, nous allons maintenant intégrer ces 2 composants dans un même projet.

5. Création d’un projet intégrant les composants

Placez-vous dans le dossier main-project depuis votre terminal et exécuter la commande suivante :

polymer init --name polymer-3-application

Créez un dossier components, ainsi que 2 dossiers à l’intérieur, my-button et my-progress-barVous allez maintenant copier les 2 fichiers my-button.js et my-progress-bar.js dans les dossiers correspondants.

Normalement, les composants, que nous avons créés précédemment, auraient dû être déposés dans un npm registry pour pouvoir l’installer via la commande npm install. Mais, dans un souci de facilité, nous copions-collons ce que nous avons développé précédemment.

Dans le fichier index.html du projet, importez les scripts des éléments et utilisez les composants.

<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">

    <title>main-project</title>
    <script src="https://d3uyj2gj5wa63n.cloudfront.net/node_modules/@webcomponents/webcomponentsjs/webcomponents-loader.js"></script>

    <script type="module">
      import './components/my-progress-bar/my-progress-bar.js';
      import './components/my-button/my-button.js';
    </script>

    <link rel="stylesheet" type="text/css" href="./src/assets/style.css">
  </head>
  <body>
    <section class="content">
      <my-progress-bar class="progressbar"></my-progress-bar>
      <div class="container-button">
        <my-button class="button"></my-button>
      </div>
    </section>
  </body>
</html>

Ajoutez le fichier CSS suivant dans src/assets/style.css

body {
  margin: 0;
  background: #f4f0f0;
}

.container-button {
  text-align: center;
  margin: 2em;
}

.button {
  margin: 2em;
  text-align: center;
}

.content {
  margin: 1em;
  margin-top: 50px;
  height: 350px;
}

Depuis le terminal, exécutez la commande suivante, et allez sur l’url indiquée :

polymer build && polymer serve
info: [cli.command.serve]    Files in this directory are available under the following URLs
      applications: http://127.0.0.1:8081
      reusable components: http://127.0.0.1:8081/components/main-project/

Si tout est OK, le bouton et la barre de progression vont s’afficher.

Comme vous pouvez le constater, nous n’avons pas touché aux propriétés CSS mais les composants ont déjà des valeurs par défaut.

Nous allons maintenant passer des nouvelles propriétés CSS aux composants. Dans le fichier style.css, mettez à jour le code comme ceci :

body {
  margin: 0;
  background: #f4f0f0;
}

.container-button {
  text-align: center;
  margin: 2em;
}

.button {
  margin: 2em;
  text-align: center;

  --button-width: 350px;
  --button-height: 100px;
  --button-background-color: lightgreen;
  --button-color: black;
  --button-font-size: 2em;
  --button-background-ripple-color: white;
}

.content {
  margin: 1em;
  margin-top: 50px;
  height: 350px;
}

.progressbar {
  --progressbar-height: 5em;
  --progressbar-background: lightgrey;
  --progressbar-font-size: 3em;
  --progressbar-text-color: black;
  --progressbar-top-position: 12px;
  --progressbar-value-background: lightgreen;
}

Relancer la commande :

polymer serve && polymer build

Le style des composants a été modifié.

L’avantage des custom properties CSS est que lors de la création d’une application, vous pouvez créer une feuille de style général qui va définir le style de tous vos composants. Votre application sera alors cohérente. Si vous développez différentes applications au sein d’une même entreprise, la feuille de style peut être généralisée à tous les projets afin que les composants aient toujours le même style, renforçant ainsi la cohérence des différentes applications.

6. Dynamiser votre composant grâce aux propriétés

Tout cela est très bien mais nos composants ne sont pas encore dynamiques, le bouton affiche toujours le même message et la barre de progression est toujours bloquée à 50%. Pour dynamiser tout cela, nous allons passer des props aux composants. A partir de maintenant, nous allons directement travailler sur les fichiers js contenus dans le dossier components.

Pour exposer des props, il faut ajouter la méthode properties() qui retournera un objet contenant toutes les props avec leurs types.

Pour le composant my-button, nous allons ajouter une propriété text qui sera le texte présent dans le bouton. Pour le composant my-progress-bar, nous allons ajouter une propriété value qui affichera le pourcentage de progression.

Dans la classe MyButton du fichier my-button.js, ajoutez la méthode comme ceci :

static get properties() {
  return {
    text: {
      type: String,
      value: 'Click on me!',
    },
  };
}

La propriété text est une String qui contient par défaut “Click on me!”.

Pour afficher la propriété (ou une data en général) dans le template, vous devez entourer la valeur par [[ ]] ou par {{ }}.

  • [[ ]] est pour le one-way data binding
  • {{ }} est pour le two-way data binding

Comme notre propriété est exclusivement affichée et est modifiée uniquement par le parent, nous allons utiliser le one-way data binding.

Mettez à jour le template comme ceci :

<button id="dummy-button" class="ripple" type="submit">[[ text ]]</button>

Dans la classe MyProgressBar du fichier my-progress-bar.js :

static get properties() {
  return {
    value: {
      type: Number,
      value: 0,
    },
  };
}

Mettez à jour le template :

<div class="progress" data-label$="[[value]]% Complete">
  <span class="value" style="width:[[value]]%;"></span>
</div>

Si vous buildez et lancez l’application, vous devriez voir que le bouton a toujours le texte “Click on me!” et la barre de progression est à 0%.

7. Générer des événements

Pour finir cette mise en bouche sur Polymer, nous allons lier le clic sur le bouton au chargement de la barre de progression. Le clic sur le bouton va devoir générer un événement qui sera intercepté par la page parente.

Pour dispatcher un événement, il faut faire appel à la méthode dispatchEvent() en passant en paramètre un CustomEvent.

Le constructeur de CustomEvent contient 2 paramètres, le nom de l’événement et un objet optionnel { detail: {}, composed: true, bubbles: true }.

  • detail : est un objet qui contient des valeurs que nous voulons remonter au parent
  • composed (true par défaut) : permet de faire remonter l’événement au-delà du shadow-dom. Si le flag est à false, les consommateurs ne pourront pas écouter l’événement généré par le composant
  • bubbles (true par défaut): permet de faire remonter l’événement dans le DOM.

Pour le bouton, nous allons générer un événement start-process, sans detail.

Dans MyButton, ajoutez la méthode suivante :

handleClick() {
  this.dispatchEvent(new CustomEvent('start-process'));
}

Mettez à jour le template :

<button id="dummy-button" class="ripple" type="submit" on-click="handleClick">[[ text ]]</button>

Le bouton va maintenant, à chaque clic, remonter un événement ‘start-process’. Pour vérifier que cela fonctionne, nous allons intercepter l’événement dans le fichier index.html du main-project.

Pour intercepter l’événement généré, il faut faire un addEventListener(‘<nom de l’événement>’, <fonction callback de traitement>).

Lorsque l’événement sera intercepté, nous allons faire charger la barre de progression en incrémentant l’attribut value de my-progress-bar toutes les 30 ms, dès que la barre de progression sera arrivé à 100%, nous stopperons le traitement.

<script>
  document.querySelector('.button').addEventListener('start-process', () => {
    let processValue = 0
    const id = setInterval(process, 30)
    document.querySelector('.button').setAttribute('text', 'In progress...')
    function process() {
      if (processValue === 100) {
        clearInterval(id);
        document.querySelector('.button').setAttribute('text', 'Start process !')
      } else {
        processValue = processValue + 1
        document.querySelector('.progressbar').setAttribute('value', processValue)
      }
    }
  })
</script>

Si vous cliquez sur le bouton, la barre de progression devrait se charger et le bouton affichera le message “In progress…” jusqu’à la fin du traitement.

Conclusion

Les composants web vont de plus en plus s’imposer dans les applications de par leur flexibilité, leur facilité d’utilisation (chaque composant se doit d’être le plus simple possible, quitte à faire de la composition de composants pour gérer des cas plus complexes). Une fois qu’une application a été découpée en composants, il est beaucoup plus facile de créer des interfaces en composant avec ceux-ci. Cela améliore la cohérence des applications, des écrans au sein d’une même application,  d’apprentissage par les utilisateurs.

Listes de liens utiles