Composants Web: guide du débutant



Découvrez les avantages de l'utilisation des composants Web, leur fonctionnement et la mise en route.



Avec les composants Web (ci-après appelés composants), les développeurs peuvent créer leurs propres éléments HTML. Dans ce guide, vous apprendrez tout ce qu'il y a à savoir sur les composants. Nous allons commencer par quels sont les composants, quels sont leurs avantages et de quoi ils sont faits.



Après cela, nous commencerons à créer les composants, d'abord avec les modèles HTML et l'interface shadow DOM, puis nous plongerons un peu dans le sujet et verrons comment créer un élément intégré personnalisé.



Quels sont les composants?



Les développeurs adorent les composants (nous entendons ici l'implémentation du design pattern "Module"). C'est un excellent moyen de définir un bloc de code qui peut être utilisé à tout moment, n'importe où. Au fil des ans, plusieurs tentatives plus ou moins réussies ont été faites pour mettre cette idée en pratique.



Le langage de liaison XML de Mozilla et la spécification des composants HTML de Microsoft pour Internet Explorer 5 remontent à environ 20 ans. Malheureusement, les deux implémentations étaient très complexes et n'intéressaient pas les fabricants d'autres navigateurs, et ont donc été vite oubliées. Malgré cela, ce sont eux qui ont jeté les bases de ce que nous avons dans ce domaine aujourd'hui.



Les frameworks JavaScript tels que React, Vue et Angular adoptent une approche similaire. L'une des principales raisons de leur succès est la possibilité d'encapsuler la logique générale de l'application dans certains modèles qui se déplacent facilement d'un formulaire à un autre.



Bien que ces cadres améliorent l'expérience de développement, tout a un prix. Les fonctionnalités de langage telles que JSX doivent être compilées et la plupart des frameworks utilisent un moteur JavaScript pour gérer leurs abstractions. Existe-t-il une autre approche pour résoudre le problème de la division du code en composants? La réponse réside dans les composants Web.



4 piliers de composants



Les composants se composent de trois API - éléments personnalisés, modèles HTML et shadow DOM, ainsi que leurs modules JavaScript sous-jacents (modules ES6). À l'aide des outils fournis par ces interfaces, vous pouvez créer des éléments HTML personnalisés qui se comportent comme leurs homologues natifs.



Les composants sont utilisés de la même manière que les éléments HTML classiques. Ils peuvent être personnalisés à l'aide d'attributs, récupérés à l'aide de JavaScript, stylisés à l'aide de CSS. L'essentiel est d'avertir le navigateur de leur existence.



Cela permet aux composants d'interagir avec d'autres frameworks et bibliothèques. En utilisant le même mécanisme de communication que les éléments réguliers, ils peuvent être utilisés par tout cadre existant ainsi que par les outils qui apparaîtront dans le futur.



Il convient également de noter que les composants sont conformes aux standards du Web. Le Web est basé sur l'idée de compatibilité descendante. Cela signifie que les composants créés aujourd'hui fonctionneront très bien pendant longtemps.



Jetons un coup d'œil à chaque spécification séparément.







1. Éléments personnalisés



Principales caractéristiques:



  • Définition du comportement des éléments
  • Réagir aux changements d'attribut
  • Extension des éléments existants


Souvent, lorsque les gens parlent de composants, ils parlent de l'interface d'éléments personnalisés.



Cette API vous permet d'étendre des éléments en définissant leur comportement lorsqu'ils sont ajoutés, mis à jour et supprimés.



class ExampleElement extends HTMLElement {
  static get observedAttributes() {
      return [...]
  }
  attributeChangedCallback(name, oldValue, newValue) {}
  connectedCallback() {}
}
customElements.define('example-element', ExampleElement)


Chaque élément personnalisé a une structure similaire. Il étend les fonctionnalités de la classe HTMLElements existante.



À l'intérieur d'un élément personnalisé, il existe plusieurs méthodes appelées réactions qui sont responsables de la gestion d'une modification particulière d'un élément. Par exemple, connectedCallback est appelé lorsqu'un élément est ajouté à la page. Ceci est similaire aux étapes du cycle de vie utilisées dans les frameworks (componentDidMount dans React, monté dans Vue).



La modification des attributs d'un élément entraîne une modification de son comportement. Lorsqu'une mise à jour se produit, l'attributChangedCallback est appelé contenant des informations sur la modification. Cela se produit uniquement pour les attributs spécifiés dans le tableau renvoyé par observeAttributes.



L'élément doit être défini avant que le navigateur puisse l'utiliser. La méthode "define" prend deux arguments - le nom de la balise et sa classe. Toutes les balises doivent contenir le caractère "-" pour éviter les conflits avec les éléments natifs existants et futurs.



<example-element>Content</example-element>


L'élément peut être utilisé comme une balise HTML normale. Lorsqu'un tel élément est trouvé, le navigateur associe son comportement à la classe spécifiée. Ce processus est appelé «mise à niveau».



Il existe deux types d'éléments - «autonomes» et «intégrés personnalisés». Jusqu'à présent, nous avons examiné les éléments autonomes. Ce sont des éléments qui ne sont pas liés aux éléments HTML existants. Comme les balises div et span, qui n'ont pas de signification sémantique spécifique.



Les éléments en ligne personnalisés - comme leur nom l'indique - étendent les fonctionnalités des éléments HTML existants. Ils héritent du comportement sémantique de ces éléments et peuvent le changer. Par exemple, si l'élément «input» a été personnalisé, il restera toujours un champ de saisie et une partie du formulaire lors de sa soumission.



class CustomInput extends HTMLInputElement {}
customElements.define('custom-input', CustomInput, { extends: 'input' })


La classe d'élément en ligne personnalisée étend la classe d'élément personnalisé. Lors de la définition d'un élément en ligne, l'élément extensible est passé comme troisième argument.



<input is="custom-input" />


L'utilisation de la balise est également légèrement différente. Au lieu d'une nouvelle balise, la balise existante est utilisée, en spécifiant l'attribut d'extension spécial "est". Lorsque le navigateur rencontre cet attribut, il sait qu'il a affaire à un élément personnalisé et le met à jour en conséquence.



Alors que les éléments autonomes sont pris en charge par la plupart des navigateurs modernes, les éléments intégrés personnalisés ne sont pris en charge que par Chrome et Firefox. Lorsqu'ils sont utilisés dans un navigateur qui ne les prend pas en charge, ces derniers seront traités comme des éléments HTML réguliers, de sorte qu'ils sont généralement sûrs à utiliser même dans ces navigateurs.



2. Modèles HTML



  • Création de structures prêtes à l'emploi
  • Ne sont pas affichés sur la page avant l'appel
  • Contient HTML, CSS et JS


Historiquement, la création de modèles côté client impliquait la concaténation de chaînes en JavaScript ou l'utilisation de bibliothèques telles que Handlebars pour analyser des blocs de balisage personnalisé. Récemment, la spécification a une balise «modèle» qui peut contenir tout ce que nous voulons utiliser.



<template id="tweet">
  <div class="tweet">
    <span class="message"></span>
      Written by @
    <span class="username"></span>
  </div>
</template>


En soi, cela n'affecte en aucun cas la page, c'est-à-dire il n'est pas analysé par le moteur, les demandes de ressources (audio, vidéo) ne sont pas envoyées. JavaScript ne peut pas y accéder, et pour les navigateurs, il s'agit d'un élément vide.



const template = document.getElementById('tweet')
const node = document.importNode(template.content, true)
document.body.append(node)


Nous obtenons d'abord l'élément "template". La méthode "importNode" crée une copie de son contenu, le deuxième argument (true) signifie copie complète. Enfin, nous l'ajoutons à la page comme n'importe quel autre élément.



Les modèles peuvent contenir tout ce que le HTML normal peut contenir, y compris CSS et JavaScript. Lorsqu'un élément est ajouté à la page, des styles lui seront appliqués et des scripts seront lancés. N'oubliez pas que les styles et les scripts sont globaux, ce qui signifie qu'ils peuvent écraser d'autres styles et valeurs utilisés par les scripts.



Les modèles ne sont pas limités à cela. Ils apparaissent dans toute leur splendeur lorsqu'ils sont utilisés avec d'autres parties des composants, en particulier avec le shadow DOM.



3. DOM de l'ombre



  • Évite les conflits de style
  • Il devient plus facile de trouver des noms (de classes, par exemple)
  • Encapsulation de la logique de mise en œuvre


Le modèle d'objet de document (DOM) est la façon dont le navigateur interprète la structure de la page. En lisant le balisage, le navigateur détermine quels éléments contiennent quel contenu et, sur cette base, prend une décision sur ce qui doit être affiché sur la page. Lors de l'utilisation de document.getElemetById (), par exemple, le navigateur accède au DOM pour trouver l'élément dont il a besoin.



Pour une mise en page, c'est bien, mais qu'en est-il des détails cachés à l'intérieur de l'élément? Par exemple, la page ne devrait pas se soucier de l'interface contenue dans l'élément "video". C'est là que shadow DOM est utile.



<div id="shadow-root"></div>
<script>
  const host = document.getElementById('shadow-root')
  const shadow = host.attachShadow({ mode: 'open' })
</script>


Le shadow DOM est créé lorsqu'il est appliqué à un élément. Tout contenu peut être ajouté au DOM shadow, tout comme un DOM normal ("léger"). Le shadow DOM n'est pas affecté par ce qui se passe à l'extérieur, c'est-à-dire en dehors de cela. Le DOM simple ne peut pas non plus accéder directement à l'ombre. Cela signifie que dans le shadow DOM, nous pouvons utiliser tous les noms de classe, styles et scripts sans nous soucier d'éventuels conflits.



Les meilleurs résultats sont obtenus en utilisant shadow DOM en conjonction avec des éléments personnalisés. Grâce au shadow DOM, lorsqu'un composant est réutilisé, ses styles et sa structure n'affectent en rien les autres éléments de la page.



Modules ES et HTML


  • Ajout au besoin
  • Aucune pré-génération requise
  • Tout est stocké au même endroit


Alors que les trois spécifications précédentes ont parcouru un long chemin dans leur développement, la façon dont elles sont emballées et réutilisées reste un sujet de débat intense.



La spécification HTML Imports définit la façon dont les documents HTML, ainsi que CSS et JavaScript, sont exportés et importés. Cela permettrait aux éléments personnalisés, ainsi qu'aux modèles et au shadow DOM, d'être situés ailleurs et utilisés selon les besoins.



Cependant, Firefox a refusé d'implémenter cette spécification dans son navigateur et a proposé une méthode différente basée sur des modules JavaScript.



export class ExampleElement external HTMLElement {}

import { ExampleElement } from 'ExampleElement.js'


Les modules ont leur propre espace de noms par défaut, c'est-à-dire leur contenu n'est pas global. Les variables, fonctions et classes exportées peuvent être importées n'importe où et à tout moment et utilisées comme ressources locales.



Cela fonctionne très bien pour les composants. Les éléments personnalisés contenant un modèle et un shadow DOM peuvent être exportés d'un fichier et utilisés dans un autre.



import { ExampleElement } from 'ExampleElement.html'


Microsoft a proposé d'étendre la spécification des modules JavaScript avec l'exportation / importation HTML. Cela vous permettra de créer des composants en utilisant du HTML déclaratif et sémantique. Cette fonctionnalité sera bientôt disponible sur Chrome et Edge.



Créer votre propre composant



Bien qu'il y ait beaucoup de choses sur les composants que vous pourriez trouver complexes, la création et l'utilisation d'un composant simple ne nécessitent que quelques lignes de code. Prenons quelques exemples.





Les composants vous permettent d'afficher les commentaires des utilisateurs à l'aide des modèles HTML et des interfaces DOM shadow.



Créons un composant pour afficher les commentaires des utilisateurs à l'aide de modèles HTML et de shadow DOM.



1. Créer un modèle


Le composant a besoin d'un modèle à copier avant de générer le balisage. Le modèle peut être situé n'importe où sur la page, la classe d'élément personnalisé peut y accéder via l'ID.



Ajoutez l'élément «modèle» à la page. Tous les styles définis sur cet élément ne l'affecteront que.



<template id="user-comment-template">
  <style>
      ...
  </style>
</template>


2. Ajout de balisage


Outre les styles, un composant peut contenir une mise en page (structure). Pour cela, l'élément "div" est utilisé.



Le contenu dynamique est transmis à travers les emplacements. Ajoutez des emplacements pour l'avatar, le nom et le message de l'utilisateur avec les attributs «nom» appropriés:



<div class="container">
  <div class="avatar-container">
    <slot name="avatar"></slot>
  </div>
  <div class="comment">
    <slot name="username"></slot>
    <slot name="comment"></slot>
  </div>
</div>


Contenu de l'emplacement par défaut




Le contenu par défaut sera affiché lorsqu'aucune information n'est transmise à l'emplacement.



Les données transmises à l'emplacement écrasent les données du modèle. Si aucune information n'est transmise à l'emplacement, le contenu par défaut est affiché.



Dans ce cas, si le nom d'utilisateur n'a pas été transféré, le message "Aucun nom" s'affiche à sa place:



<slot name="username">
  <span class="unknown">No name</span>
</slot>


3. Créer une classe


La création d'un élément personnalisé commence par étendre la classe "HTMLElement". Une partie du processus de configuration consiste à créer une racine fantôme pour le rendu du contenu de l'élément. Nous l'ouvrons pour y accéder à l'étape suivante.



Enfin, nous informons le navigateur de la nouvelle classe UserComment.



class UserComment extends HTMLElement {
  constructor() {
      super()
      this.attachShadow({ mode: 'open' })
  }
}
customElements.define('user-comment', UserComment)


4. Application du contenu de l'ombre


Lorsque le navigateur rencontre l'élément "user-comment", il regarde le nœud racine de l'ombre pour récupérer son contenu. Le deuxième argument indique au navigateur de copier tout le contenu, pas seulement la première couche (éléments de niveau supérieur).



Nous ajoutons un balisage au nœud racine de l'ombre, qui met immédiatement à jour l'apparence du composant.



connectedCallback() {
  const template = document.getElementById('user-comment-template')
  const node = document.importNode(template.content, true)
  this.shadowRoot.append(node)
}


5. Utilisation du composant


Le composant est maintenant prêt à être utilisé. Ajoutez la balise "user-comment" et transmettez-lui les informations nécessaires.



Puisque tous les slots ont des noms, tout ce qui est passé en dehors d'eux sera ignoré. Tout ce qui se trouve à l'intérieur des emplacements est copié exactement comme il a été passé, y compris le style.



<user-comment>
  <img alt="" slot="avatar" src="avatar.png" />
  <span slot="username">Matt Crouch</span>
  <div slot="comment">This is an example of a comment</div>
</user-comment>


Exemple de code étendu:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Web Components Example</title>
    <style>
      body {
        display: grid;
        place-items: center;
      }
      img {
        width: 80px;
        border-radius: 4px;
      }
    </style>
  </head>
  <body>
    <template id="user-comment-template">
      <div class="container">
        <div class="avatar-container">
          <slot name="avatar">
            <slot class="unknown"></slot>
          </slot>
        </div>
        <div class="comment">
          <slot name="username">No name</slot>
          <slot name="comment"></slot>
        </div>
      </div>
      <style>
        .container {
          width: 320px;
          clear: both;
          margin-bottom: 1rem;
        }
        .avatar-container {
          float: left;
          margin-right: 1rem;
        }
        .comment {
          height: 80px;
          display: flex;
          flex-direction: column;
          justify-content: center;
        }
        .unknown {
          display: block;
          width: 80px;
          height: 80px;
          border-radius: 4px;
          background: #ccc;
        }
      </style>
    </template>

    <user-comment>
      <img alt="" slot="avatar" src="avatar1.jpg" />
      <span slot="username">Matt Crouch</span>
      <div slot="comment">Fisrt comment</div>
    </user-comment>
    <user-comment>
      <img alt="" slot="avatar" src="avatar2.jpg" />
      <!-- no username -->
      <div slot="comment">Second comment</div>
    </user-comment>
    <user-comment>
      <!-- no avatar -->
      <span slot="username">John Smith</span>
      <div slot="comment">Second comment</div>
    </user-comment>

    <script>
      class UserComment extends HTMLElement {
        constructor() {
          super();
          this.attachShadow({ mode: "open" });
        }
        connectedCallback() {
          const template = document.getElementById("user-comment-template");
          const node = document.importNode(template.content, true);
          this.shadowRoot.append(node);
        }
      }
      customElements.define("user-comment", UserComment);
    </script>
  </body>
</html>










Créer un élément en ligne personnalisé



Comme indiqué précédemment, les éléments personnalisés peuvent étendre les éléments existants. Cela permet de gagner du temps en conservant le comportement par défaut de l'élément fourni par l'éleveuse. Dans cette section, nous verrons comment vous pouvez étendre l'élément "time".



1. Créer une classe


Les éléments intégrés, comme les éléments autonomes, apparaissent lorsque la classe est étendue, mais au lieu de la classe générale "HTMLElement", ils étendent une classe spécifique.



Dans notre cas, cette classe est HTMLTimeElement - la classe utilisée par les éléments "time". Il inclut le comportement lié à l'attribut "datetime", y compris le format des données.



class RelativeTime extends HTMLTimeElement {}


2. Définition des éléments


L'élément est enregistré par le navigateur en utilisant la méthode "define". Cependant, contrairement à un élément autonome, lors de l'enregistrement d'un élément en ligne, la méthode "define" doit recevoir un troisième argument - un objet avec des paramètres.



Notre objet contiendra une clé avec la valeur de l'élément personnalisé. Il prend le nom de la balise. En l'absence d'une telle clé, une exception sera levée.



customElements.define('relative-time', RelativeTime, { extends: 'time' })


3. Réglage de l'heure


Puisque nous pouvons avoir plusieurs composants sur une page, le composant doit fournir une méthode pour définir la valeur d'un élément. Dans cette méthode, le composant transmet une valeur de temps à la bibliothèque "timeago" et définit la valeur renvoyée par cette bibliothèque comme valeur d'élément (désolé pour la tautologie).



Enfin, nous définissons l'attribut title afin que l'utilisateur puisse voir la valeur définie au survol.



setTime() {
  this.innerHTML = timeago().format(this.getAttribute('datetime'))
  this.setAttribute('title', this.getAttribute('datetime'))
}


4. Mise à jour de la connexion


Le composant peut utiliser la méthode immédiatement après avoir été affiché sur la page. Comme les composants en ligne n'ont pas de DOM shadow, ils n'ont pas besoin d'un constructeur.



connectedCAllback() {
  this.setTime()
}


5. Suivi de la modification des attributs


Si vous mettez à jour l'heure par programme, le composant ne répondra pas. Il ne sait pas qu'il doit surveiller les changements dans l'attribut "datetime".



Une fois les attributs observés définis, attributeChangedCallback sera appelé chaque fois qu'ils changent.



static get observedAttributes() {
  return ['datetime']
}
attributeChangedCallback() {
  this.setTime()
}


6. Ajout à la page


Puisque notre élément est une extension de l'élément natif, son implémentation est légèrement différente. Pour l'utiliser, ajoutez une balise "time" à la page avec un attribut spécial "is", dont la valeur est le nom de l'élément intégré défini lors de l'enregistrement. Les navigateurs qui ne prennent pas en charge les composants rendront le contenu de secours.



<time is="relative-time" datetime="2020-09-20T12:00:00+0000">
  20  2020 . 12:00
</time>


Exemple de code étendu:
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Web Components Another Example</title>
    <!-- timeago.js -->
    <script
      src="https://cdnjs.cloudflare.com/ajax/libs/timeago.js/4.0.2/timeago.min.js"
      integrity="sha512-SVDh1zH5N9ChofSlNAK43lcNS7lWze6DTVx1JCXH1Tmno+0/1jMpdbR8YDgDUfcUrPp1xyE53G42GFrcM0CMVg=="
      crossorigin="anonymous"
    ></script>
    <style>
      body {
        display: flex;
        flex-direction: column;
        align-items: center;
      }
      input,
      button {
        margin-bottom: 0.5rem;
      }
      time {
        font-size: 2rem;
      }
    </style>
  </head>
  <body>
    <input type="text" placeholder="2020-10-20" value="2020-08-19" />
    <button>Set Time</button>

    <time is="relative-time" datetime="2020-09-19">
      19  2020 .
    </time>

    <script>
      class RelativeTime extends HTMLTimeElement {
        setTime() {
          this.innerHTML = timeago.format(this.getAttribute("datetime"));
          this.setAttribute("title", this.getAttribute("datetime"));
        }
        connectedCallback() {
          this.setTime();
        }
        static get observedAttributes() {
          return ["datetime"];
        }
        attributeChangedCallback() {
          this.setTime();
        }
      }
      customElements.define("relative-time", RelativeTime, { extends: "time" });

      const button = document.querySelector("button");
      const input = document.querySelector("input");
      const time = document.querySelector("time");

      button.onclick = () => {
        const { value } = input;
        time.setAttribute("datetime", value);
      };
    </script>
  </body>
</html>










J'espère que je vous ai aidé à comprendre ce que sont les composants Web, à quoi ils servent et comment ils sont utilisés.



All Articles