Le Saint Graal des composants réutilisables : éléments personnalisés, Shadow DOM et NPM

Publié: 2022-03-10
Résumé rapide ↬ Cet article examine comment augmenter le HTML avec des composants dotés de fonctionnalités et de styles intégrés. Nous apprendrons également comment rendre ces éléments personnalisés réutilisables dans tous les projets utilisant NPM.

Même pour les composants les plus simples, le coût en main-d'œuvre humaine peut avoir été important. Les équipes UX effectuent des tests d'utilisabilité. Un éventail de parties prenantes doit approuver la conception.

Les développeurs effectuent des tests AB, des audits d'accessibilité, des tests unitaires et des vérifications inter-navigateurs. Une fois que vous avez résolu un problème, vous ne voulez pas répéter cet effort . En construisant une bibliothèque de composants réutilisables (plutôt que de tout construire à partir de zéro), nous pouvons continuellement utiliser les efforts passés et éviter de revoir les défis de conception et de développement déjà résolus.

Une capture d'écran du site Web des composants matériels de Google - montrant divers composants.
Grand aperçu

Construire un arsenal de composants est particulièrement utile pour des entreprises telles que Google qui possèdent un portefeuille considérable de sites Web partageant tous une marque commune. En codifiant leur interface utilisateur dans des widgets composables, les grandes entreprises peuvent à la fois accélérer le temps de développement et assurer la cohérence de la conception visuelle et de l'interaction utilisateur à travers les projets. Il y a eu un intérêt croissant pour les guides de style et les bibliothèques de modèles au cours des dernières années. Compte tenu de plusieurs développeurs et concepteurs répartis sur plusieurs équipes, les grandes entreprises cherchent à atteindre la cohérence. Nous pouvons faire mieux que de simples échantillons de couleurs. Ce dont nous avons besoin, c'est d'un code facilement distribuable .

Partage et réutilisation du code

Copier et coller manuellement le code se fait sans effort. Garder ce code à jour, cependant, est un cauchemar de maintenance. De nombreux développeurs s'appuient donc sur un gestionnaire de packages pour réutiliser le code d'un projet à l'autre. Malgré son nom, le Node Package Manager est devenu la plate-forme inégalée pour la gestion frontale des packages. Il y a actuellement plus de 700 000 packages dans le registre NPM et des milliards de packages sont téléchargés chaque mois. Tout dossier contenant un fichier package.json peut être téléchargé sur NPM en tant que package partageable. Alors que NPM est principalement associé à JavaScript, un package peut inclure du CSS et du balisage. NPM facilite la réutilisation et, surtout, la mise à jour du code. Plutôt que d'avoir à modifier le code dans une myriade d'endroits, vous modifiez le code uniquement dans le package.

Plus après saut! Continuez à lire ci-dessous ↓

Le problème du balisage

Sass et Javascript sont facilement portables avec l'utilisation d'instructions d'importation. Les langages de modèles donnent à HTML la même capacité - les modèles peuvent importer d'autres fragments de HTML sous la forme de partiels. Vous pouvez écrire le balisage de votre pied de page, par exemple, une seule fois, puis l'inclure dans d'autres modèles. Dire qu'il existe une multiplicité de langages de modèles serait un euphémisme. Vous lier à un seul limite considérablement la réutilisation potentielle de votre code. L'alternative est de copier-coller le balisage et d'utiliser NPM uniquement pour les styles et javascript.

C'est l'approche adoptée par le Financial Times avec sa bibliothèque de composants Origami . Dans son discours "Can't You Just Make It More like Bootstrap?" Alice Bartlett a conclu "qu'il n'y a pas de bon moyen de laisser les gens inclure des modèles dans leurs projets". Parlant de son expérience de maintenance d'une bibliothèque de composants chez Lonely Planet, Ian Feather a réitéré les problèmes de cette approche :

"Une fois qu'ils ont copié ce code, ils coupent essentiellement une version qui doit être maintenue indéfiniment. Lorsqu'ils ont copié le balisage d'un composant de travail, il avait un lien implicite vers un instantané du CSS à ce stade. Si vous mettez ensuite à jour le modèle ou refactorisez le CSS, vous devez mettre à jour toutes les versions du modèle dispersées sur votre site.

Une solution : les composants Web

Les composants Web résolvent ce problème en définissant le balisage en JavaScript. L'auteur d'un composant est libre de modifier le balisage, CSS et Javascript. Le consommateur du composant peut bénéficier de ces mises à niveau sans avoir à parcourir manuellement un projet modifiant le code. La synchronisation avec les dernières modifications à l'échelle du projet peut être réalisée avec une mise à npm update via un terminal. Seul le nom du composant et son API doivent rester cohérents.

L'installation d'un composant Web est aussi simple que de taper npm install component-name dans un terminal. Le Javascript peut être inclus avec une déclaration d'importation :

 <script type="module"> import './node_modules/component-name/index.js'; </script>

Ensuite, vous pouvez utiliser le composant n'importe où dans votre balisage. Voici un exemple de composant simple qui copie du texte dans le presse-papiers.

Voir la démo du composant Web Pen Simple par CSS GRID (@cssgrid) sur CodePen.

Voir la démo du composant Web Pen Simple par CSS GRID (@cssgrid) sur CodePen.

Une approche centrée sur les composants pour le développement frontal est devenue omniprésente, inaugurée par le framework React de Facebook. Inévitablement, compte tenu de l'omniprésence des frameworks dans les flux de travail frontaux modernes, un certain nombre d'entreprises ont construit des bibliothèques de composants en utilisant le framework de leur choix. Ces composants ne sont réutilisables que dans ce cadre particulier.

Un composant du Carbon Design System d'IBM
Un composant du Carbon Design System d'IBM. Pour une utilisation dans les applications React uniquement. D'autres exemples significatifs de bibliothèques de composants construites dans React incluent Atlaskit d'Atlassian et Polaris de Shopify. ( Grand aperçu )

Il est rare qu'une entreprise de taille importante ait un front-end uniforme et le replatorming d'un framework à un autre n'est pas rare. Les cadres vont et viennent. Pour permettre le maximum de réutilisation potentielle dans les projets, nous avons besoin de composants indépendants du framework .

Une capture d'écran de npmjs.com montrant des composants qui font la même chose construits exclusivement pour des frameworks javascript particuliers.
La recherche de composants via npmjs.com révèle un écosystème Javascript fragmenté. ( Grand aperçu )
Un graphique illustrant la popularité des frameworks au fil du temps. Ember, Knockout et Backbone ont plongé en popularité, remplacés par de nouvelles offres.
La popularité sans cesse changeante des frameworks au fil du temps. ( Grand aperçu )
"J'ai construit des applications Web en utilisant : Dojo, Mootools, Prototype, jQuery, Backbone, Thorax et React au fil des ans... J'aurais adoré pouvoir apporter ce composant Dojo tueur que j'ai asservi avec moi à mon React l'application d'aujourd'hui.

— Dion Almaer, directeur de l'ingénierie, Google

Lorsque nous parlons d'un composant Web, nous parlons de la combinaison d'un élément personnalisé avec le DOM fantôme. Les éléments personnalisés et le shadow DOM font partie à la fois de la spécification DOM du W3C et de la norme WHATWG DOM, ce qui signifie que les composants Web sont une norme Web . Les éléments personnalisés et le shadow DOM sont enfin prêts à être pris en charge par plusieurs navigateurs cette année. En utilisant une partie standard de la plate-forme Web native, nous nous assurons que nos composants peuvent survivre au cycle rapide de restructuration frontale et de refonte architecturale. Les composants Web peuvent être utilisés avec n'importe quel langage de template et n'importe quel framework frontal - ils sont vraiment compatibles entre eux et interopérables. Ils peuvent être utilisés partout, d'un blog Wordpress à une application d'une seule page.

Le projet Custom Elements Everywhere de Rob Dodson documente l'interopérabilité des composants Web avec divers frameworks Javascript côté client.
Le projet Custom Elements Everywhere de Rob Dodson documente l'interopérabilité des composants Web avec divers frameworks Javascript côté client. React, la valeur aberrante ici, résoudra, espérons-le, ces problèmes avec React 17. ( Grand aperçu )

Faire un composant Web

Définir un élément personnalisé

Il a toujours été possible de créer des noms de balises et de faire apparaître leur contenu sur la page.

 <made-up-tag>Hello World!</made-up-tag>

HTML est conçu pour être tolérant aux pannes. Ce qui précède sera rendu, même s'il ne s'agit pas d'un élément HTML valide. Il n'y a jamais eu de bonne raison de le faire - s'écarter des balises standardisées a traditionnellement été une mauvaise pratique. Cependant, en définissant une nouvelle balise à l'aide de l'API d'élément personnalisé, nous pouvons augmenter le code HTML avec des éléments réutilisables dotés de fonctionnalités intégrées. La création d'un élément personnalisé ressemble beaucoup à la création d'un composant dans React - mais ici, nous HTMLElement .

 class ExpandableBox extends HTMLElement { constructor() { super() } }

Un appel sans paramètre à super() doit être la première instruction du constructeur. Le constructeur doit être utilisé pour configurer l'état initial et les valeurs par défaut et pour configurer tous les écouteurs d'événement. Un nouvel élément personnalisé doit être défini avec un nom pour sa balise HTML et la classe des éléments correspondant :

 customElements.define('expandable-box', ExpandableBox)

C'est une convention de capitaliser les noms de classe. La syntaxe de la balise HTML est cependant plus qu'une convention. Et si les navigateurs voulaient implémenter un nouvel élément HTML et qu'ils voulaient l'appeler expandable-box ? Pour éviter les collisions de noms, aucune nouvelle balise HTML standardisée n'inclura de tiret. En revanche, les noms des éléments personnalisés doivent inclure un tiret.

 customElements.define('whatever', Whatever) // invalid customElements.define('what-ever', Whatever) // valid

Cycle de vie des éléments personnalisés

L'API propose quatre réactions d'élément personnalisées - des fonctions qui peuvent être définies dans la classe qui seront automatiquement appelées en réponse à certains événements du cycle de vie d'un élément personnalisé.

connectedCallback est exécuté lorsque l'élément personnalisé est ajouté au DOM.

 connectedCallback() { console.log("custom element is on the page!") }

Cela inclut l'ajout d'un élément avec Javascript :

 document.body.appendChild(document.createElement("expandable-box")) //“custom element is on the page”

ainsi que simplement inclure l'élément dans la page avec une balise HTML :

 <expandable-box></expandable-box> // "custom element is on the page"

Tout travail impliquant la récupération de ressources ou le rendu devrait être ici.

disconnectedCallback est exécuté lorsque l'élément personnalisé est supprimé du DOM.

 disconnectedCallback() { console.log("element has been removed") } document.querySelector("expandable-box").remove() //"element has been removed"

adoptedCallback est exécuté lorsque l'élément personnalisé est adopté dans un nouveau document. Vous n'avez probablement pas besoin de vous en soucier trop souvent.

attributeChangedCallback est exécuté lorsqu'un attribut est ajouté, modifié ou supprimé. Il peut être utilisé pour écouter les modifications apportées aux attributs natifs standardisés tels que disabled ou src , ainsi qu'aux attributs personnalisés que nous créons. C'est l'un des aspects les plus puissants des éléments personnalisés car il permet la création d'une API conviviale.

Attributs d'élément personnalisés

Il existe un grand nombre d'attributs HTML. Pour que le navigateur ne perde pas de temps à appeler notre attributeChangedCallback lorsqu'un attribut est modifié, nous devons fournir une liste des modifications d'attribut que nous voulons écouter. Pour cet exemple, nous ne nous intéressons qu'à un seul.

 static get observedAttributes() { return ['expanded'] }

Alors maintenant, notre attributeChangedCallback ne sera appelé que lorsque nous modifions la valeur de l'attribut étendu sur l'élément personnalisé, car c'est le seul attribut que nous avons répertorié.

Les attributs HTML peuvent avoir des valeurs correspondantes (pensez href, src, alt, value, etc.) tandis que d'autres sont vrais ou faux (par exemple , disabled, selected, required ). Pour un attribut avec une valeur correspondante, nous inclurions les éléments suivants dans la définition de classe de l'élément personnalisé.

 get yourCustomAttributeName() { return this.getAttribute('yourCustomAttributeName'); } set yourCustomAttributeName(newValue) { this.setAttribute('yourCustomAttributeName', newValue); }

Pour notre exemple d'élément, l'attribut sera vrai ou faux, donc définir le getter et le setter est un peu différent.

 get expanded() { return this.hasAttribute('expanded') } // the second argument for setAttribute is mandatory, so we'll use an empty string set expanded(val) { if (val) { this.setAttribute('expanded', ''); } else { this.removeAttribute('expanded') } }

Maintenant que le passe-partout a été traité, nous pouvons utiliser attributeChangedCallback .

 attributeChangedCallback(name, oldval, newval) { console.log(`the ${name} attribute has changed from ${oldval} to ${newval}!!`); // do something every time the attribute changes }

Traditionnellement, la configuration d'un composant Javascript aurait impliqué de passer des arguments à une fonction init . En utilisant l' attributeChangedCallback , il est possible de créer un élément personnalisé configurable uniquement avec le balisage.

Shadow DOM et les éléments personnalisés peuvent être utilisés séparément, et vous pouvez trouver des éléments personnalisés utiles en eux-mêmes. Contrairement au shadow DOM, ils peuvent être poly-remplis. Cependant, les deux spécifications fonctionnent bien en conjonction.

Attacher le balisage et les styles avec Shadow DOM

Jusqu'à présent, nous avons géré le comportement d'un élément personnalisé. En ce qui concerne le balisage et les styles, cependant, notre élément personnalisé équivaut à un <span> vide sans style. Pour encapsuler HTML et CSS dans le cadre du composant, nous devons attacher un DOM fantôme. Il est préférable de le faire dans la fonction constructeur.

 class FancyComponent extends HTMLElement { constructor() { super() var shadowRoot = this.attachShadow({mode: 'open'}) shadowRoot.innerHTML = `<h2>hello world!</h2>` }

Ne vous souciez pas de comprendre ce que signifie le mode - vous devez inclure son passe-partout, mais vous voudrez presque toujours open . Cet exemple de composant simple affichera simplement le texte "hello world". Comme la plupart des autres éléments HTML, un élément personnalisé peut avoir des enfants, mais pas par défaut. Jusqu'à présent, l'élément personnalisé ci-dessus que nous avons défini ne rendra aucun enfant à l'écran. Pour afficher tout contenu entre les balises, nous devons utiliser un élément slot .

 shadowRoot.innerHTML = ` <h2>hello world!</h2> <slot></slot> `

Nous pouvons utiliser une balise de style pour appliquer du CSS au composant.

 shadowRoot.innerHTML = `<style> p { color: red; } </style> <h2>hello world!</h2> <slot>some default content</slot>`

Ces styles ne s'appliqueront qu'au composant, nous sommes donc libres d'utiliser des sélecteurs d'éléments sans que les styles n'affectent quoi que ce soit d'autre de la page. Cela simplifie l'écriture de CSS, rendant inutiles les conventions de nommage telles que BEM.

Publier un composant sur NPM

Les packages NPM sont publiés via la ligne de commande. Ouvrez une fenêtre de terminal et déplacez-vous dans un répertoire que vous souhaitez transformer en un package réutilisable. Tapez ensuite les commandes suivantes dans le terminal :

  1. Si votre projet n'a pas encore de package.json, npm init vous guidera pour en générer un.
  2. npm adduser relie votre machine à votre compte NPM. Si vous n'avez pas de compte préexistant, il en créera un nouveau pour vous.
  3. npm publish
Les packages NPM sont publiés via la ligne de commande
Grand aperçu

Si tout s'est bien passé, vous avez maintenant un composant dans le registre NPM, prêt à être installé et utilisé dans vos propres projets - et partagé avec le monde.

Un exemple de composant dans le registre NPM, prêt à être installé et utilisé dans vos propres projets.
Grand aperçu

L'API des composants Web n'est pas parfaite. Les éléments personnalisés ne peuvent actuellement pas inclure de données dans les soumissions de formulaires. L'histoire de l'amélioration progressive n'est pas géniale. Gérer l'accessibilité n'est pas aussi simple qu'il devrait l'être.

Bien qu'initialement annoncé en 2011, la prise en charge des navigateurs n'est toujours pas universelle. Le support de Firefox est attendu plus tard cette année. Néanmoins, certains sites Web de premier plan (comme Youtube) les utilisent déjà. Malgré leurs lacunes actuelles, pour les composants universellement partageables, ils constituent l'option unique et, à l'avenir, nous pouvons nous attendre à des ajouts passionnants à ce qu'ils ont à offrir.