Nous avons tous des projets sur lesquels nous préférerions ne pas travailler. Le code est devenu ingérable, la portée a évolué, des corrections rapides ont été appliquées par-dessus d’autres corrections et la structure s’est effondrée sous son poids de code spaghetti. Le codage peut être une activité désordonnée.

Les projets gagnent à utiliser des modules simples et indépendants qui ont une seule responsabilité. Le code modulaire est encapsulé, il y a donc moins besoin de se soucier de la mise en œuvre. Tant que vous savez ce qu’un module va produire lorsqu’il reçoit un ensemble d’entrées, vous n’avez pas nécessairement besoin de comprendre comment il a atteint cet objectif.

Appliquer les concepts modulaires à un seul langage de programmation est simple, mais le développement web nécessite un mélange varié de technologies. Les navigateurs analysent le HTML, le CSS et le JavaScript pour rendre le contenu, les styles et les fonctionnalités de la page.

Ils ne se mélangent pas toujours facilement car :

  • Le code connexe peut être réparti entre trois fichiers ou plus
  • Les styles globaux et les objets JavaScript peuvent interférer les uns avec les autres de manière inattendue.

Ces problèmes s’ajoutent à ceux rencontrés par les moteurs d’exécution des langages, les frameworks, les bases de données et les autres dépendances utilisées sur le serveur.

Consultez notre guide vidéo sur les composants web

Que sont les composants web ?

Un composant web est un moyen de créer un bloc de code encapsulé, à responsabilité unique, qui peut être réutilisé sur n’importe quelle page.

Considérez la balise HTML <video> . Avec une URL, un spectateur peut utiliser des contrôles comme la lecture, la pause, le retour en arrière, l’avance et le réglage du volume.

Le style et les fonctionnalités sont fournis, mais vous peux apporter des modifications en utilisant divers attributs et appels API JavaScript. Un nombre quelconque d’éléments <vidéo> peuvent être placés à l’intérieur d’autres balises, et ils n’entreront pas en conflit.

Et si vous avez besoin de votre propre fonctionnalité personnalisée ? Par exemple, un élément indiquant le nombre de mots sur la page ? Il n’y a pas (encore) de balise HTML <wordcount>.

Les frameworks tels que React et Vue.js permettent aux développeurs de créer des composants web dont le contenu, le style et la fonctionnalité peuvent être définis dans un seul fichier JavaScript. Ils permettent de résoudre de nombreux problèmes de programmation complexes mais n’oubliez pas que :

  • Vous devez apprendre à utiliser ce framework et mettre à jour votre code à mesure qu’il évolue.
  • Un composant écrit pour un framework est rarement compatible avec un autre.
  • Les frameworks gagnent et perdent en popularité. Vous deviendrez dépendant des caprices et des priorités de l’équipe de développement et des utilisateurs.
  • Les composants web standard peuvent ajouter des fonctionnalités au navigateur, ce qui est difficile à réaliser en JavaScript seul (comme le Shadow DOM).

Heureusement, les concepts populaires introduits dans les bibliothèques et les frameworks font généralement leur chemin dans les normes web. Cela a pris du temps, mais les composants web sont arrivés.

Une brève histoire des composants web

Après de nombreux faux départs spécifiques aux fournisseurs, le concept de composants web standard a été présenté pour la première fois par Alex Russell lors de la conférence Fronteers en 2011. La bibliothèque Polymer de Google (un polyfill basé sur les propositions actuelles) est arrivée deux ans plus tard, mais les premières implémentations ne sont apparues dans Chrome et Safari qu’en 2016.

Les fournisseurs de navigateurs ont mis du temps à négocier les détails, mais les composants web ont été ajoutés à Firefox en 2018 et à Edge en 2020 (lorsque Microsoft a adopté le moteur Chromium).

Il est compréhensible que peu de développeurs aient voulu ou pu adopter les composants web, mais nous avons finalement atteint un bon niveau de prise en charge des navigateurs avec des API stables. Tout n’est pas parfait, mais ils constituent une alternative de plus en plus viable aux composants basés sur des frameworks.

Même si vous n’êtes pas encore prêt à abandonner votre préféré, les composants web sont compatibles avec tous les frameworks et les API seront prises en charge pendant des années.

Des dépôtes de composants web pré-construits sont disponibles pour que tout le monde puisse y jeter un œil :

…mais écrire votre propre code est plus amusant !

Ce tutoriel fournit une introduction complète aux composants web écrits sans framework JavaScript. Vous apprendrez ce qu’ils sont et comment les adapter à vos projets web. Vous aurez besoin de quelques connaissances en HTML5, CSS et JavaScript.

Commencer à utiliser les composants web

Les composants web sont des éléments HTML personnalisés tels que <hello-world></hello-world>. Le nom doit contenir un tiret pour ne jamais entrer en conflit avec des éléments officiellement pris en charge dans la spécification HTML.

Vous devez définir une classe ES2015 pour contrôler l’élément. Elle peut porter n’importe quel nom, mais HelloWorld est une pratique courante. Elle doit étendre l’interface HTMLElement, qui représente les propriétés et méthodes par défaut de chaque élément HTML.

Note : Firefox permet d’étendre des éléments HTML spécifiques tels que HTMLParagraphElement, HTMLImageElement ou HTMLButtonElement. Ceci n’est pas pris en charge par les autres navigateurs et ne te permet pas de créer un Shadow DOM.

Pour faire quoi que ce soit d’utile, la classe nécessite une méthode nommée connectedCallback() qui est invoquée lorsque l’élément est ajouté à un document :

class HelloWorld extends HTMLElement {

  // connect component
  connectedCallback() {
    this.textContent = 'Hello World!' ;
  }

}

Dans cet exemple, le texte de l’élément est défini comme « Hello World »

La classe doit être enregistrée auprès du CustomElementRegistry pour la définir comme gestionnaire d’un élément spécifique :

customElements.define( 'hello-world', HelloWorld ) ;

Le navigateur associe maintenant l’élément <hello-world> à votre classe HelloWorld lorsque votre JavaScript est chargé (par ex <script type="module" src="./helloworld.js"></script>).

Vous avez maintenant un élément personnalisé !

Démonstration CodePen

Ce composant peut être stylé en CSS comme n’importe quel autre élément :

hello-world {
  font-weight: bold;
  coulor: red;
}

Ajout d’attributs

Ce composant n’est pas utile puisque le même texte est édité quoi qu’il arrive. Comme tout autre élément, nous pouvons ajouter des attributs HTML :

<hello-world name="Craig"></hello-world>

On pourrait ainsi remplacer le texte pour que « Hello Craig ! » s’affiche. Pour y parvenir, vous pouvez ajouter une fonction constructor() à la classe HelloWorld, qui est exécutée lorsque chaque objet est créé. Elle doit :

  1. Appeller la méthode super() pour initialiser le HTMLElement parent, et
  2. Effectuer d’autres initialisations. Dans ce cas, nous définirons une propriété name qui sera définie par défaut sur « World » :
class HelloWorld extends HTMLElement {

  constructor() {
    super() ;
    this.name = 'World' ;
  }

  // more code...

Votre composant ne se soucie que de l’attribut name. Une propriété statique observedAttributes() doit renvoyer un tableau de propriétés à observer :

// components attributes
static get observedAttributes() {
  return ['name'] ;
}

Une méthode attributeChangedCallback() est appelée lorsqu’un attribut est défini dans le HTML ou modifié à l’aide de JavaScript. On lui passe le nom de la propriété, l’ancienne valeur et la nouvelle valeur :

// attribut change
attributeChangedCallback(property, oldValue, newValue) {

  if (oldValue === newValue) return ;
  this[ property ] = newValue ;

}

Dans cet exemple, seule la propriété name sera mise à jour, mais vous pourrez ajouter des propriétés supplémentaires si nécessaire.

Enfin, vous devez modifier le message dans la méthode connectedCallback():

// connect component
connectedCallback() {

  this.textContent = `Hello ${ this.name }!` ;

}

Démonstration CodePen

Méthodes du cycle de vie

Le navigateur appelle automatiquement six méthodes tout au long du cycle de vie de l’état du composant web. La liste complète est fournie ici, bien que vous ayez déjà vu les quatre premières dans les exemples ci-dessus :

constructor()

Il est appelé lorsque le composant est initialisé pour la première fois. Il doit appeler super() et peut définir tous les paramètres par défaut ou effectuer d’autres processus de pré-rendu.

static observedAttributes()

Renvoie un tableau d’attributs que le navigateur observera.

attributeChangedCallback(propertyName, oldValue, newValue)

Appelé chaque fois qu’un attribut observé est modifié. Ceux définis en HTML sont transmis immédiatement, mais JavaScript peut les modifier :

document.querySelector('hello-world').setAttribute('name', 'Everyone') ;

La méthode peut avoir besoin de déclencher un nouveau rendu lorsque cela se produit.

connectedCallback()

Cette fonction est appelée lorsque le composant web est ajouté à un DOM (Document Object Model). Elle doit exécuter tout rendu nécessaire.

disconnectedCallback()

Cette fonction est appelée lorsque le composant web est retiré d’un DOM. Cela peut être utile si vous devez faire le ménage, par exemple en supprimant l’état stocké ou en abandonnant les requêtes Ajax.

adoptedCallback()

Cette fonction est appelée lorsqu’un composant web est déplacé d’un document à un autre. Vous trouverez peut-être une utilité à cette fonction, bien que j’aie eu du mal à penser à un quelconque cas !

Comment les composants web interagissent avec les autres éléments

Les composants web offrent certaines fonctionnalités uniques que vous ne trouverez pas dans les frameworks JavaScript.

Le Shadow DOM

Même si le composant web que nous avons construit ci-dessus fonctionne, il n’est pas à l’abri des interférences extérieures, et CSS ou JavaScript pourraient le modifier. De même, les styles que vous définissez pour votre composant pourraient fuir et affecter les autres.

Le Shadow DOM résout ce problème d’encapsulation en attachant un DOM séparé au composant web avec :

const shadow = this.attachShadow({ mode : 'closed' }) ;

Le mode peut être soit :

  1. « open » – Le JavaScript dans la page externe peut accéder au Shadow DOM (en utilisant Element.shadowRoot), ou
  2. « closed » – le Shadow DOM n’est accessible que dans le composant web.

Le Shadow DOM peut être manipulé comme n’importe quel autre élément DOM :

connectedCallback() {

  const shadow = this.attachShadow({mode : 'closed' }) ;

  shadow.innerHTML = `
    <style>
      p {
        text-align: center;
        font-weight: normal;
        padding: 1em;
        margin: 0 0 2em 0;
        background-color: #eee;
        border: 1px solid #666;
      }
    </style>

    <p>Hello ${ this.name }!</p>`;

}

Le composant rend maintenant le texte « Hello » à l’intérieur d’un élément <p> et lui donne un style. Il ne peut pas être modifié par JavaScript ou CSS en dehors du composant, bien que certains styles tels que la police et la couleur soient hérités de la page car ils n’ont pas été définis explicitement.

Démonstration CodePen

Les styles attribués à ce composant web ne peuvent pas affecter les autres paragraphes de la page ni même les autres composants <hello-world>.

Notez que le sélecteur CSS :host peut donner un style à l’élément externe <hello-world> à partir du composant web :

:host {
  transform: rotate(180deg);
}

Vous pouvez aussi définir des styles à appliquer lorsque l’élément utilise une classe spécifique, par ex <hello-world class="rotate90">:

:host(.rotate90) {
  transform: rotate(90deg);
}

Modèles HTML

Définir du HTML à l’intérieur d’un script peut devenir peu pratique pour les composants web plus complexes. Un modèle vous permet de définir un morceau de HTML dans votre page que votre composant web peut utiliser. Cela présente plusieurs avantages :

  1. Vous pouvez modifier le code HTML sans avoir à réécrire les chaînes de caractères dans votre JavaScript.
  2. Les composants peuvent être personnalisés sans avoir à créer des classes JavaScript distinctes pour chaque type.
  3. Il est plus facile de définir le HTML dans le HTML – et il peut être modifié sur le serveur ou le client avant le rendu du composant.

Les modèles sont définis dans une balise <Template> et il est pratique d’attribuer un ID pour pouvoir le référencer dans la classe du composant. Dans cet exemple, trois paragraphes pour afficher le message « Hello » :

<template id="hello-world">

  <style>
    p {
      text-align: center;
      font-weight: normal;
      padding: 0.5em;
      margin: 1px 0;
      background-color: #eee;
      border: 1px solid #666;
    }
  </style>

  <p class="hw-text"></p>
  <p class="hw-text"></p>
  <p class="hw-text"></p> <p class="hw-text">.

</template>

La classe de composant web peut accéder à ce modèle, obtenir son contenu et cloner les éléments pour s’assurer que vous créez un fragment DOM unique partout où il est utilisé :

const template = document.getElementById('hello-world').content.cloneNode(true) ;

Le DOM peut être modifié et ajouté directement au Shadow DOM :

connectedCallback() {

  const

    shadow = this.attachShadow({mode : 'closed' }),
    template = document.getElementByID('hello-world').content.cloneNode(true),
    hwMsg = `Hello ${ this.name }` ;

  Array.from( template.querySelectorAll('.hw-text') )
    .forEach( n => n.textContent = hwMsg ) ;

  shadow.append( template ) ;

}

Démonstration CodePen

Slots de modèle

Les slots vous permettent de personnaliser un modèle. Supposons que vous voulez utiliser votre composant web <hello-world> mais placer le message dans un titre <h1> dans le Shadow DOM. Vous pourrez écrire ce code :

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Défaut!</h1>

</hello-world>

(Note l’attribut slot .)

Vous pourrez éventuellement vouloir ajouter d’autres éléments, comme un autre paragraphe :

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Défaut!</h1>
  <p>This text will become part of the document.</p>

</hello-world>

Les slots peuvent maintenant être mis en œuvre dans votre modèle :

<template id="hello-world">

  <slot name="msgtext" class="hw-text"></slot>

  <slot></slot>

</template>

Un attribut de slot d’élément défini sur « msgtext » (l’élément <h1>) est inséré à l’endroit où il y a un élément <slot> nommé « msgtext » Le <p> n’a pas de nom de slot attribué, mais il est utilisé dans le prochain élément sans nom disponible <slot>. En effet, le modèle devient :

<emballage id="hello-world">

  <slot name="msgtext" class="hw-text">
    <h1 slot="msgtext">Hello Default!</h1>
  </slot>

  <slot>
    <p>This text will become a part of the component.</p>
  </slot>

</template>

Ce n’est pas aussi simple dans la réalité. Un <slot> dans le Shadow DOM pointe vers les éléments insérés. Vous ne pouvez y accéder qu’en localisant un <slot> puis en utilisant la méthode .assignedNodes() pour renvoyer un tableau enfant interne. La méthode connectedCallback() mise à jour :

connectedCallback() {

  const
    shadow = this.attachShadow({mode : 'closed' }),
    hwMsg = `Hello ${ this.name }` ;

  // append shadow DOM
  shadow.append(
    document.getElementByID('hello-world').content.cloneNode(true)
  ) ;

  // find all slots with a hw-text class
  Array.from( shadow.querySelectorAll('slot.hw-text') )

    // update first assignedNode in slot
    .forEach( n => n.assignedNodes()[0].textContent = hwMsg ) ;

}

Démonstration CodePen

De plus, vous ne pouvez pas donner un style direct aux éléments insérés, bien que vous puissiez cibler des slots spécifiques dans votre composant web :

<Template id="hello-world">

  <style>
    slot[name="msgtext"] {coulor : green ; }
  </style>

  <slot name="msgtext" class="hw-text"></slot>
  <slot></slot>

</template>

Les slots de modèle sont un peu inhabituels, mais l’un des avantages est que votre contenu sera affiché si JavaScript ne s’exécute pas. Ce code montre un titre et un paragraphe par défaut qui ne sont remplacés que lorsque la classe du composant web s’exécute bien :

<hello-world name="Craig">

  <h1 slot="msgtext">Hello Défaut!</h1>
  <p>This text will become a part of the component.</p>

</hello-world>

Vous pourrez donc mettre en place une forme d’amélioration progressive, même si ce n’est qu’un message « Vous avez besoin de JavaScript » !

Le Shadow DOM déclaratif

Les exemples ci-dessus construisent un Shadow DOM en utilisant JavaScript. Cela reste la seule option, mais un Shadow DOM déclaratif expérimental est en cours de développement pour Chrome. Cela permet un rendu côté serveur et évite tout décalage de mise en page ou flash de contenu non stylisé.

Le code suivant est détecté par l’analyseur HTML, qui crée un Shadow DOM identique à celui que tu as créé dans la dernière section (tu devras mettre à jour le message si nécessaire) :

<hello-world name="Craig">

  <template shadowroot="closed">
    <slot name="msgtext" class="hw-text"></slot>
    <slot></slot>
  </template>

  <h1 slot="msgtext">Hello Default!</h1>
  <p>This text will become part of the component.</p>.

</hello-world>

La fonctionnalité n’est disponible dans aucun navigateur, et il n’y a aucune garantie qu’elle atteigne Firefox ou Safari. Vous pouvez en savoir plus sur le Shadow DOM déclaratif, et un polyfill est simple, mais sachez que l’implémentation pourrait changer.

Événements Shadow DOM

Votre composant web peut attacher des événements à n’importe quel élément du Shadow DOM comme vous le feriez dans le DOM de la page, par exemple pour écouter les événements de clic sur tous les enfants internes :

shadow.addEventListener('click', e => {

  // do something

}) ;

Si vous utilisez stopPropagation, l’événement remontera dans le DOM de la page, mais il sera reciblé. Ainsi, il semble provenir de votre élément personnalisé plutôt que des éléments qui le composent.

Utiliser des composants web dans d’autres frameworks

Tout composant web que vous créez fonctionnera dans tous les frameworks JavaScript. Aucun d’entre eux ne connaît les éléments HTML ou ne s’en soucie –votre <hello-world> sera traité de la même façon qu’un <div> et placé dans le DOM où la classe sera activée.

custom-elements-everywhere.com fournit une liste de frameworks et de notes sur les composants web. La plupart sont entièrement compatibles, bien que React.js présente quelques difficultés. Il est possible d’utiliser <hello-world> en JSX :

import React from 'react';
import ReactDOM from 'react-dom' ;
import from './hello-world.js' ;

function MyPage() {

  return (
    <>
      <hello-world name="Craig"></hello-world> 
    </>
  ) ;

}

ReactDOM.render(<MyPage />, document.getElementById('root')) ;

…mais :

  • React ne peut transmettre que des types de données primitifs aux attributs HTML (pas les tableaux ou les objets)
  • React ne peut pas écouter les événements des composants web, vous devez donc joindre manuellement vos propres gestionnaires.

Critiques et problèmes liés aux composants web

Les composants web se sont considérablement améliorés, mais certains aspects peuvent être délicats à gérer.

Difficultés de stylisation

La stylisation des composants web pose quelques difficultés, surtout si vous voulez remplacer les styles scopés. Il existe de nombreuses solutions :

  1. Évitez d’utiliser le Shadow DOM. Vous pouvez ajouter du contenu directement à votre élément personnalisé, mais tout autre JavaScript pourrait le modifier accidentellement ou malicieusement.
  2. Utilisez les classes :host. Comme nous l’avons vu plus haut, le scoped CSS peut appliquer des styles spécifiques lorsqu’une classe est appliquée à l’élément personnalisé.
  3. Vérifiez les propriétés personnalisées CSS (variables). Les propriétés personnalisées se retrouvent en cascade dans les composants web, donc, si votre élément utilise var(--my-color) vous pouvez définir --my-color dans un conteneur externe (comme :root), et il sera utilisé.
  4. Profitez des parties d’ombre. Le nouveau sélecteur ::part() peut donner du style à un composant intérieur qui a un attribut part, c’est-à-dire <h1 part="heading"> dans un <hello-world> peut être stylisé avec le sélecteur hello-world::part(heading).
  5. Transmettez une chaîne de styles. Vous pouvez les transmettre en tant qu’attribut à appliquer dans un bloc <style>.

Aucune n’est idéale, et vous devrez planifier soigneusement la façon dont les autres utilisateurs peuvent personnaliser votre composant web.

Entrées ignorées

Tout <input>, <textarea>, ou <selection> dans votre Shadow DOM n’est pas automatiquement associé au formulaire qui le contient. Les premiers adeptes des composants web ajoutaient des champs cachés au DOM de la page ou utilisaient l’interface FormData pour mettre à jour les valeurs. Ni l’un ni l’autre ne sont particulièrement pratiques et rompent l’encapsulation des composants web.

La nouvelle interface ElementInternals permet à un composant web de se hooker aux formulaires afin de pouvoir définir des valeurs personnalisées et la validité. Elle est implémentée dans Chrome, mais un polyfill est disponible pour les autres navigateurs.

Pour faire une démonstration, vous allez créer un <input-age name="your-age"></input-age> basique. La classe doit avoir une valeur statique formAssociated définie sur true et, en option, une méthode formAssociatedCallback() peut être appelée lorsque le formulaire externe est associé :

// Composant web <input-age>
class InputAge extends HTMLElement {

  static formAssociated = true ;

  formAssociatedCallback(form) {
    console.log('form associated:', form.id) ;
  }

Le constructeur doit maintenant exécuter la méthode attachInternals(), qui permet au composant de communiquer avec le formulaire et d’autres codes JavaScript qui veulent inspecter la valeur ou la validation :

  constructor() {

    super() ;
    this.internals = this.attachInternals() ;
    this.setValue('') ;

  }

  // set form value

  setValue(v) {

    this.value = v ;

    this.internals.setFormValue(v) ;

  }

La méthode setFormValue() de l’ElementInternal définit la valeur de l’élément pour le formulaire parent initialisé avec une chaîne vide ici (on peut aussi lui passer un objet FormData avec plusieurs paires nom/valeur). Les autres propriétés et méthodes comprennent :

  • form : le formulaire parent
  • labels : un tableau d’éléments qui labélisent le composant
  • Des options de Constraint Validation API telles que willValidate, checkValidity et validationMessage

La méthode connectedCallback() crée un DOM Shadow comme précédemment, mais doit également surveiller les changements du champ, afin que setFormValue() puisse être exécuté :

  connectedCallback() {

    const shadow = this.attachShadow({mode : 'closed' }) ;

    shadow.innerHTML = `
      <style>input { largeur : 4em ; }</style>
      <input type="number" placeholder="age" min="18" max="120" />` ;

    // monitor input values
    shadow.querySelector('input').addEventListener('input', e => {
      this.setValue(e.target.value) ;
    }) ;

  }

Vous pouvez maintenant créer un formulaire HTML à l’aide de ce composant web qui agit de manière similaire aux autres champs de formulaire :

<form id="myform">

  <input type="text" name="your-name" placeholder="name" />

  <input-age name="your-age"></input-age>

  <button>submit</button>

</form>

Cela fonctionne, mais il faut avouer que c’est un peu alambiqué.

Vérifiez-le dans la démonstration CodePen

Pour plus d’informations, consultez cet article sur les contrôles de formulaire plus performants.

Résumé

Les composants web ont eu du mal à faire l’unanimité et à être adoptés à une époque où les frameworks JavaScript ont gagné en stature et en capacités. Si vous venez de React, Vue.js ou Angular, les composants web peuvent sembler complexes et encombrants, surtout s’il manque des fonctionnalités comme la liaison de données et la gestion d’état.

Il y a des problèmes à régler, mais l’avenir des composants web est prometteur. Ils sont indépendants du framework, légers, rapides et peuvent mettre en œuvre des fonctionnalités qui seraient impossibles uniquement en JavaScript.

Il y a dix ans, peu de gens se seraient attaqués à un site sans jQuery, mais les fournisseurs de navigateurs ont pris les meilleures parties et ont ajouté des alternatives natives (comme querySelector). Il en sera de même pour les frameworks JavaScript, et les composants web sont cette première étape provisoire.

Vous avez des questions sur la façon d’utiliser les composants web ? Parlons-en dans la section des commentaires !

Craig Buckler

Freelance UK web developer, writer, and speaker. Has been around a long time and rants about standards and performance.