Développez votre premier Web Component en 3 étapes

Développez votre premier Web Component en 3 étapes

Ce post fait partie de la catégorie #article. J'y écris des tutoriels et retours d'expérience basés sur mon quotidien de développeur front-end.

Si tu t’es déjà demandé comment créer et réutiliser des modules dans tes pages HTML et à travers tes différents projets, cette technologie devrait t’intéresser, j’ai nommé : les Web Components !

En résumé, les Web Components permettent d’encapsuler une structure, un style et un comportement dans un composant, que tu peux ensuite intégrer facilement dans les pages HTML de tes applications web.

Là où ça devient intéressant, c’est que cette technologie est standardisée par le W3C et supportée nativement par de nombreux navigateurs.

Les Web Components sont basés principalement sur les 4 spécifications suivantes :

  • Custom Elements : pour utiliser de nouveaux éléments HTML ou étendre des éléments existants. Par exemple, tu peux créer ton élément <my-link> qui sera reconnu par ton navigateur au même titre qu’un élément <a> ou <div>.
  • Shadow DOM : pour encapsuler du style et une structure HTML dans un composant et en masquer les détails d’implémentation. Concrètement, quand tu inspecteras un élément <my-link> avec le debugger, le code HTML et le style CSS de ce composant seront invisibles.
  • ES Modules : pour importer et réutiliser des fichiers JavaScript de manière modulaire.
  • HTML Template : pour déclarer des squelettes HTML instanciables après le chargement d’une page. Grâce aux templates, on peut définir une structure HTML dans une balise <template>, qui ne sera pas interprétée au chargement de la page. Cela permet de récupérer cette structure pour la transformer et l’afficher par la suite.

Si tu veux en savoir plus sur les Web Components, tu trouveras pleins d’infos sur https://www.webcomponents.org/.

Maintenant qu’on sait tout ça, voyons comment écrire un petit composant. À ton clavier ! ⌨️

<github-infos />

Dans cet article, tu vas créer un widget, dont le but sera de récupérer des informations depuis un repository GitHub et de les afficher dans une page HTML.

À la fin, on veut pouvoir écrire ce code HTML :

<github-infos repository="jezen/is-thirteen"></github-infos>

Et voir ceci dans le navigateur :

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/7b8ffdd0-fcdd-4a62-b84b-c8c43acc1df1/Capture_decran_2020-01-27_a_17.33.18.png

Prêt ? C’est parti !

1 La structure d’un Web Component

Dans chaque Web Component, tu retrouveras des éléments communs.

Premièrement, il est défini avec une classe JavaScript qui étend HTMLElement. On l’appellera ici GithubInfos.

class GithubInfos extends HTMLElement { }

Tu peux aussi étendre d’autres interfaces tant qu’elles étendent elles-mêmes un HTMLElement. Par exemple, étendre HTMLButtonElement donnera à ton composant toutes les caractéristiques d’un bouton, que tu pourras modifier par la suite.

Comme avec de nombreux frameworks JavaScript, HTMLElement nous fournit des méthodes qui se déclenchent à différents instants du cycle de vie du composant. Elles sont très pratiques pour interagir avec un composant :

// appelée lorsque l'élément est connecté pour la première fois au DOM
connectedCallback()

 // appelée lorsque l'élément est déplacé vers un nouveau document
adoptedCallback()

// appelée lorsqu'un des attributs du composant est modifié
attributeChangedCallback()

// appelée lorsque l'élément est déconnecté du DOM
disconnectedCallback()

Attention, petite subtilité : pour utiliser la méthode attributeChangedCallback que l’on vient de découvrir, il faut explicitement dire à notre composant d’observer les attributs qui nous intéressent.

Pour ça, on définit la méthode statique observedAttributes, qui retourne un tableau de noms d’attributs :

static get observedAttributes() { return ["your_attributes_here"]; }

Aussi, dans les composants, on trouvera souvent un constructeur, ainsi qu’une méthode chargée d’en générer l’affichage.

Une fois notre classe créée, il ne reste plus qu’à l’enregistrer dans l’annuaire des éléments du DOM pour qu’elle soit reconnue et correctement interprétée par le navigateur :

// premier paramètre : le nom de votre composant
// deuxième paramètre : la classe qui décrit votre composant 
customElements.define('github-infos', GithubInfos);

Finalement, on obtient quelque chose comme ceci :

class GithubInfos extends HTMLElement {
  constructor() {
    super();
  }
  
  static get observedAttributes() { return []; }

  connectedCallback() { }
  adoptedCallback() { }
  attributeChangedCallback() { }
  disconnectedCallback() { }

  render() { }
}

customElements.define('github-infos', GithubInfos);

Passons à notre exemple, et remplissons un peu tout ça. 👇

2 Les appels API depuis un Web Component

Rentrons dans le vif du sujet. On a maintenant la structure de base de notre Web Component. Pour rappel, on veut afficher quelques informations d’un repository GitHub à partir de son identifiant.

On va donc utiliser la méthode attributeChangedCallback pour écouter l’attribut repository de notre composant. À chaque appel de la fonction, on doit :

  1. récupérer les données du repository depuis GitHub
  2. mettre à jour l’affichage du widget

Avant tout, il faut ajouter repository aux attributs observés :

static get observedAttributes() { return ["repository"]; }

La méthode attributeChangedCallback sera appelée dès qu’il sera modifié. On l’utilise donc pour récupérer les données depuis GitHub :

/**
 * name: nom de l'attribut qui a changé
 * oldValue: valeur de l'attribut avant le changement
 * newValue: nouvelle valeur de l'attribut
 */
attributeChangedCallback(name, oldValue, newValue) {
  if (name == "repository" && oldValue != newValue) {
    fetch(`https://api.github.com/repos/${newValue}`)
      .then(response => response.json())
      .then(data => this.render(data));
  }
}

On vérifie ici que l’attribut qui a changé est bien repository, et que la nouvelle valeur de cet attribut est différente de la précédente (pour ne pas envoyer de requête inutile).

attributeChangedCallback est aussi appelée dès l’initialisation du widget si l’attribut est déjà défini à ce moment là.

Notre composant va maintenant récupérer les informations depuis l’API de GitHub à chaque changement de l’attribut repository.

Il ne reste qu’à écrire la méthode render qui prend en paramètre les données récupérées de l’API, et qui insère du code HTML dans notre élément :

render(data) {
  const content = `
    <header>
      <a 
        href="${data.html_url || '#'}"
        target="_blank"
      >
        ${data.name || ''}
      </a>
      <a
        href="${data.owner.html_url || '#'}" 
        target="_blank"
      >
        @${data.owner.login || ''}
      </a>
    </header>
    <p>${data.description || ''}</p>
    <hr>
    <div>
      <span>${data.subscribers_count} watchers</span>
      <span>${data.watchers} stars</span>
      <span>${data.forks} forks</span>
    </div>
  `;

  this.innerHTML = content;
}

Ici, je fais le choix d’écrire une longue chaîne de caractère et de l’insérer directement dans le composant via l ‘innerHTML pour plus de clarté.

Sachant que l’on étend HTMLElement, on peut aussi utiliser les méthodes d’insertion comme appendChild pour y ajouter des Node. Concrètement, ça donnerait plutôt ça :

render(data) {
    const header = document.createElement('header');
    const repoLink = document.createElement('a');
    repoLink.href = data.html_url || '#';
    repoLink.textContent = data.name || '';
    const authorLink = document.createElement('a');
    authorLink.href = data.owner.html_url || '#';
    authorLink.textContent = `@${data.owner.login || ''}`;
    header.appendChild(repoLink);
    header.appendChild(authorLink);

    const description = document.createElement('p');
    description.textContent = data.description || '';

    const separation = document.createElement('hr');

    const infosContainer = document.createElement('div');
    const subscribers = document.createElement('span');
    subscribers.textContent = data.subscribers_count+' watchers';
    const watchers = document.createElement('span');
    watchers.textContent = data.watchers+' stars';
    const forks = document.createElement('span');
    forks.textContent = data.forks+' forks';
    infosContainer.appendChild(subscribers);
    infosContainer.appendChild(watchers);
    infosContainer.appendChild(forks);

    this.appendChild(header);
    this.appendChild(description);
    this.appendChild(separation);
    this.appendChild(infosContainer);
}

Même résultat, mais moins lisible. En revanche, on obtient un contrôle plus fin de nos différents éléments, auxquels on peut par exemple attacher des évènements.

Maintenant, testons tout ça !

3 L’intégration du Web Component

Importer et utiliser le composant

Pour utiliser ton composant, rien de plus simple. Crée un fichier HTML :

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
  </head>
  <body>
    
  </body>
</html>

Importe ton composant dans le head comme un script classique :

<script src="githubInfos.js"></script>

Ajoute-le dans le body de ta page. N’oublie pas de définir l’attribut repository du repo que tu veux afficher :

<github-infos repository="jezen/is-thirteen"></github-infos>

Ce qui nous donne ceci :

<!doctype html>
<html>
  <head>
    <meta charset="UTF-8">
    <script src="githubInfos.js"></script>
  </head>
  <body>
    <github-infos repository="jawj/github-widget"></github-infos>
  </body>
</html>

Et maintenant ? On ouvre la page dans un navigateur ?

Malheureusement, si tu fais ça, tu verras probablement une page blanche ainsi qu’une jolie erreur CORS en console.

Pour contourner le problème, tu auras besoin d’un mini-serveur web qui va servir ton fichier HTML. Pour ça, j’utilise en général Python, en faisant simplement :

> cd github-infos
> python3 -m http.server 8001

Voilà, ton serveur est accessible à l’adresse http://localhost:8001, et tu devrais voir le résultat !

https://s3-us-west-2.amazonaws.com/secure.notion-static.com/a0c7b55c-2c9b-43f7-998d-3440636c6756/Capture_decran_2020-01-27_a_17.33.32.png

Styliser un Web Component

Et comme c’est triste de laisser un composant tout moche, on peut ajouter notre design. 🎨

Pour ça, tant que ton composant n’utilise pas de Shadow DOM, c’est plutôt simple. Tu peux importer un fichier CSS dans ta page HTML, comme d’habitude.

Dans le cas du Shadow DOM, ça se corse, et c’est un sujet que je ne traiterais pas dans cet article 🙂. Si tu veux en savoir plus, il y a quelques pistes d’exploration ici ou ici.

J’ajoute donc ces quelques lignes de CSS :

github-infos {
  display: block;
  max-width: 300px;
  padding: 1rem;
  font-family: sans-serif;
  box-shadow: 0 1px 4px rgba(0, 0, 0, 0.2);
}

github-infos header a:first-child {
  display: block;
  font-weight: bolder;
  font-size: 110%;
}

github-infos p {
  font-size: 90%;
  opacity: 0.8;
}

github-infos hr {
  opacity: 0.5;
}

github-infos span {
  font-size: 80%;
  opacity: 0.6;
  margin-right: 0.5rem;
}

Et je l’importe dans ma page :

<link rel="stylesheet" href="style.css" />

Tadaa 🎉

Voilà, en quelques lignes, tu as créé un composant que tu va pouvoir ré-importer dans tous tes projets.

Code source

Example of a Web Component which displays some informations about a GitHub repository

Une question, une remarque ?

N'hésite pas à m'envoyer un message, je te répondrai avec plaisir !