Bonjour les composants Web
Si vous avez entendu parler des composants Web mais que vous n'en avez jamais construit vous-même, vous êtes au bon endroit. Le temps est venu! Construisons ensemble un Composant Web !
Lorsque j'ai l'occasion d'enseigner aux gens les composants Web, j'aime généralement commencer par quelque chose de simple que beaucoup d'entre nous connaissent du « monde réel ». Construisons donc une « balise de nom » de composant Web qui ressemble à ceci…
INFO : Qu'est-ce qu'un composant Web ? Le terme « composants Web » est un terme générique qui couvre un ensemble de plusieurs normes W3C en HTML et CSS, qui permettent ensemble la création et le partage d'éléments HTML réutilisables natifs de la plate-forme Web (composants).
Étape zéro
- Commencez avec un document HTML de base contenant un script de module JS pointant vers un fichier JavaScript (temporairement) vide.
- Dans le corps du document, ajoutez un
name-tagélément avec le texte "Composants Web" comme contenu. Il s'agit du composant Web que nous aimerions éventuellement faire fonctionner. - Démarrez un serveur Web et accédez à votre fichier HTML. Vous devriez voir le texte "Composants Web" rendu dans votre navigateur.
<!DOCTYPE html>
<html>
<head>
<title>Hello Web Components</title>
</head>
<body>
<name-tag greeting="Hola">Web Components</name-tag>
<script type="module" src="index.js"></script>
</body>
</html>
// Nothing here yet. We'll add our Web Component code in the next step.
La première étape
- Dans votre fichier JavaScript, déclarez le comportement de votre
name-tagélément personnalisé en créant une classe nomméeNameTagqui s'étend deHTMLElement. Tous les composants Web héritent deHTMLElementtout comme les éléments intégrés, tels quediv,spanetinput. - Enregistrez votre élément auprès du navigateur en appelant
customElements.define(...), en fournissant le nom de balise HTML souhaité et laNameTagclasse comme arguments. - Actualisez le navigateur et vous devriez toujours voir le même texte qu'avant. Cependant, si vous inspectez l'
name-tagélément, vous verrez qu'il n'est pas seulement unHTMLElementmais que son constructeur l'est désormaisNameTagégalement.
class NameTag extends HTMLElement {
}
customElements.define('name-tag', NameTag);
Deuxième étape
- Ajoutez un constructeur à votre classe. Après l'appel à
super(), appelezthis.attachShadow(...)pour créer un Shadow DOM pour votre élément personnalisé. Un Shadow DOM, c'est comme avoir un document HTML privé dans lequel seul votre composant peut rendre. Passez les options{ mode: ‘open’ }pour que la racine fantôme et ses éléments internes soient toujours accessibles en externe via JavaScript et via les outils de développement. - Une fois le DOM Shadow attaché, vous pouvez accéder
this.shadowRootà sa propriété et la définirinnerHTMLsur le code HTML de votre choix. - Actualisez le navigateur et observez que votre
innerHTMLcontenu Shadow DOM est maintenant rendu mais que le contenu de l'élément lui-même, le texte "Composants Web", n'est plus rendu. Où est-il passé ? - Ouvrez l'inspecteur et vérifiez qu'il existe un
#shadow-rootnœud que vous pouvez inspecter pour voir ce que vous avez fourni en tant que fichierinnerHTML. Notez que votre contenu est également toujours dans le DOM, même s'il n'est pas rendu. La raison pour laquelle il n'est pas rendu est que le navigateur ne sait pas comment composer le contenu Light DOM avec le Shadow DOM. Nous corrigerons cela ensuite.
class NameTag extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = 'Rendering from Shadow DOM';
}
}
customElements.define('name-tag', NameTag);
Troisième étape
- Révisez le code HTML qui est placé dans la racine fantôme afin qu'il inclue un
<slot>élément. Cela indique au navigateur comment composer votre Light DOM et Shadow DOM ensemble. L'emplacement fournit un emplacement pour "projeter" ou restituer le contenu du Light DOM dans le Shadow DOM. Le contenu existe toujours dans le Light DOM, mais il est rendu comme s'il se trouvait à l'emplacement de l'emplacement. - Actualisez votre navigateur et observez que maintenant vos contenus Light et Shadow DOM sont correctement composés ensemble.
class NameTag extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.innerHTML = 'HELLO <slot></slot>';
}
}
customElements.define('name-tag', NameTag);
Étape 4
- Pour permettre à notre
greetingattribut de fonctionner, nous devrons dire à la plateforme qu'il y a ungreetingattribut que nous voulons observer. Créez un getter statique nomméobservedAttributesqui renvoie un tableau de noms d'attributs que la plate-forme doit observer. Le tableau doit contenir le seul nom d'attribut :['greeting']. - Ensuite, implémentez un
attributeChangedCallback()que la plate-forme peut invoquer chaque fois que l'un de ses attributs observés change. - Ajoutez un getter/setter de propriété pour fournir un accès de propriété à l'attribut, puisque la plupart des éléments HTML ont à la fois des propriétés et des attributs. Cela garantira que notre élément personnalisé ressemble à tout autre élément de la plate-forme et qu'il fonctionne correctement avec les frameworks frontaux populaires qui définissent à la fois les attributs et les propriétés. Le getter et le setter peuvent simplement déléguer aux API
getAttribute()etsetAttribute()deHTMLElement. - Extrayez une
renderfonction qui prend le composant en entrée et appelez-la depuisattributeChangedCallback()afin qu'elle puisse mettre à jour son rendu lorsque l'état de l'élément change. Supprimez le code définiinnerHTMLdans le constructeur. Ce n'est plus nécessaire. - On peut aussi introduire un
connectedCallback().La plateforme l'appellera lorsque l'élément sera connecté au document. Nous l'utiliserons pour nous assurer qu'il existe une valeur par défautgreetingsi aucune n'a été définie au moment où l'élément est connecté. - Actualisez le navigateur pour voir que l'
greetingattribut prend maintenant effet. Expérimentez en utilisant les outils de débogage pour définir lagreetingpropriété et l'attribut. Essayez de placer des points d'arrêt dans le fichierattributeChangedCallback().
const render = x => `${x.greeting.toUpperCase()} <slot></slot>`;
class NameTag extends HTMLElement {
static get observedAttributes() {
return ['greeting'];
}
constructor() {
super();
this.attachShadow({ mode: 'open' });
}
get greeting() {
return this.getAttribute('greeting');
}
set greeting(value) {
this.setAttribute('greeting', value);
}
connectedCallback() {
if (!this.greeting) {
this.greeting = 'Hello';
}
}
attributeChangedCallback(name, oldValue, newValue) {
this.shadowRoot.innerHTML = render(this);
}
}
customElements.define('name-tag', NameTag);
- Améliorons notre
renderfonction afin qu'elle fournisse une structure plus réaliste. - Actualisez le navigateur pour vous assurer que la nouvelle structure s'affiche correctement.
const render = x => `
<div part="header" class="header">
<h3 part="greeting">${x.greeting.toUpperCase()}</h3>
<h4 part="message">my name is</h4>
</div>
<div part="body" class="body">
<slot></slot>
</div>
<div part="footer" class="footer"></div>
`;
Sixième étape
- En tirant parti des nouvelles normes de Constructible StyleSheets et Adopted StyleSheets, créez une
CSSStyleSheetinstance et appelezreplaceSync()pour définir son texte CSS. - Dans votre constructeur d'élément, ajoutez vos styles personnalisés à l'existant
adoptedStyleSheetsdushadowRoot. - Actualisez votre navigateur pour voir un composant entièrement stylé.
const styles = new CSSStyleSheet();
styles.replaceSync(`
:host {
--default-color: red;
--default-radius: 6px;
--default-depth: 5px;
display: inline-block;
contain: content;
color: white;
background: var(--color, var(--default-color));
border-radius: var(--radius, var(--default-radius));
min-width: 325px;
text-align: center;
box-shadow: 0 0 var(--depth, var(--default-depth)) rgba(0,0,0,.5);
}
.header {
margin: 16px 0;
position: relative;
}
h3 {
font-weight: bold;
font-family: sans-serif;
letter-spacing: 4px;
font-size: 32px;
margin: 0;
padding: 0;
}
h4 {
font-family: sans-serif;
font-size: 18px;
margin: 0;
padding: 0;
}
.body {
background: white;
color: black;
padding: 32px 8px;
font-size: 42px;
font-family: cursive;
}
.footer {
height: 16px;
background: var(--color, var(--default-color));
border-radius: 0 0 var(--radius, var(--default-radius)) var(--radius, var(--default-radius));
}
`);
constructor() {
super();
this.attachShadow({ mode: 'open' });
this.shadowRoot.adoptedStyleSheets = [
...this.shadowRoot.adoptedStyleSheets,
styles
];
}
Lisez la suite pour aller un peu plus loin et découvrir des ressources d'apprentissage et des bibliothèques supplémentaires.
Aller plus loin
Propriétés personnalisées CSS (ou variables CSS)
Un moyen courant d'activer le style des éléments personnalisés consiste à baser les styles de composants sur les propriétés personnalisées CSS (ou variables CSS). Les propriétés personnalisées sont déclarées avec un préfixe à double tiret et référencées avec la var(...)fonction. Lorsque vous faites référence à une variable, vous pouvez également fournir une valeur de secours, qui elle-même peut être une autre variable. Vous pouvez voir cette technique utilisée dans tout le CSS ci-dessus. Pour jouer avec cela, créez plusieurs <name-tag>éléments sur votre page, puis utilisez l'inspecteur de style du navigateur pour définir ——color, ——depthet——radiuspropriétés sur des éléments individuels ou sur des éléments parents. Même si Shadow DOM encapsule les styles, les propriétés personnalisées CSS « transpercent » la limite du Shadow DOM par défaut. Cela permet de créer un système de thèmes qui fonctionne sur l'ensemble d'une bibliothèque de composants ou d'une application. Et rappelez-vous, les propriétés personnalisées CSS peuvent être utilisées avec CSS calc(...)pour des effets étonnants.
- En savoir plus sur les propriétés personnalisées CSS sur MDN.
- En savoir plus sur CSS Calc sur MDN.
Les styles Shadow DOM peuvent également tirer parti de sélecteurs spéciaux, tels que le :host sélecteur qui cible l'élément lui-même. Il est recommandé de configurer des styles d'hôte pour la valeur par défaut displayet disabledles états. Consultez la containpropriété CSS pour savoir comment améliorer également les performances de rendu des composants. Si vous avez des styles spéciaux pour les éléments placés à l'intérieur du contenu de votre élément, vous pouvez les spécifier en utilisant le ::slotted()sélecteur.
- En savoir plus sur :host sur MDN.
- En savoir plus sur ::slotted sur MDN.
- En savoir plus sur CSS Contain sur MDN.
Comme adoptedStyleSheetsil n'est pas encore implémenté dans tous les navigateurs (je vous regarde Safari !), pour tous les composants de production que vous créez, vous souhaiterez disposer d'une fonction de détection et de retour à l'injection d'éléments de style si nécessaire. C'est quelque chose que de nombreuses bibliothèques de composants Web (par exemple, FAST ) gèrent automatiquement pour vous.
- En savoir plus sur les feuilles de style adoptées sur MDN.
Vous avez peut-être remarqué que plusieurs éléments du DOM shadow ci-dessus ont un partattribut. Cela permet à un développeur de composants Web de déclarer des parties du composant qui peuvent être stylisées en externe par les consommateurs du composant. Pour l'essayer, créez plusieurs <name-tag>éléments sur votre page, chacun avec un fichier class. Créez ensuite un CSS qui cible les pièces en fonction d'un sélecteur de classe et ajuste les styles de salutation. Voici à quoi cela pourrait ressembler :
.large-greeting::part(greeting) {
font-size: 64px;
}
Le cycle de vie des composants Web
Dans notre NameTagélément, nous avons utilisé le connectedCallback(...), qui est l'un des crochets standard du cycle de vie des composants Web. Mais ce n'est pas le seul. Voici une liste des rappels de cycle de vie disponibles que vous pouvez utiliser dans vos composants :
constructor()— S'exécute lorsque l'élément est créé ou mis à niveau.connectedCallback()— S'exécute lorsque l'élément est inséré dans le DOM.disconnectedCallback()— S'exécute lorsque l'élément est supprimé du DOM.attributedChangedCallback(attrName, oldValue, newValue)— S'exécute chaque fois que l'un des attributs personnalisés de l'élément change.adoptedCallback()— S'exécute lorsque l'élément est déplacé de son actueldocumentvers un nouveaudocumentvia un appel à l'adoptNode(…)API.
Comme mentionné précédemment, la quantité de passe-partout impliquée lors de la création d'un composant Web simple semble un peu trop. En effet, les normes des composants Web ne fournissent que les fonctionnalités de bas niveau, avec peu ou pas d'opinions intégrées. Mais que se passerait-il si un ensemble léger d'opinions facultatives était introduit ? Cela pourrait-il réduire la quantité de code et offrir d'autres avantages ? De nombreuses personnes construisant des composants Web le pensent et utilisent donc une sorte de petite bibliothèque d'assistance. Pour conclure cet article, j'aimerais vous montrer le même composant, construit avec FAST , qui rationalise le processus de création de composants Web et fournit des outils supplémentaires pour créer des solutions plus complexes. Voici le NameTagcomposant Web implémenté avec FAST , en utilisant TypeScript :
import {
attr,
css,
customElement,
FASTElement,
html
} from "@microsoft/fast-element";
// Create a reactive template based on the element's state.
const template = html<NameTag>`
<div part="header" class="header">
<h3 part="greeting">${x => x.greeting.toUpperCase()}</h3>
<h4 part="message">my name is</h4>
</div>
<div part="body" class="body">
<slot></slot>
</div>
<div part="footer" class="footer"></div>
`;
// Create styles that automatically use adopted style sheets when present.
const styles = css`
:host {
--default-color: red;
--default-radius: 6px;
--default-depth: 5px;
display: inline-block;
contain: content;
color: white;
background: var(--color, var(--default-color));
border-radius: var(--radius, var(--default-radius));
min-width: 325px;
text-align: center;
box-shadow: 0 0 var(--depth, var(--default-depth)) rgba(0,0,0,.5);
}
.header {
margin: 16px 0;
position: relative;
}
h3 {
font-weight: bold;
font-family: sans-serif;
letter-spacing: 4px;
font-size: 32px;
margin: 0;
padding: 0;
}
h4 {
font-family: sans-serif;
font-size: 18px;
margin: 0;
padding: 0;
}
.body {
background: white;
color: black;
padding: 32px 8px;
font-size: 42px;
font-family: cursive;
}
.footer {
height: 16px;
background: var(--color, var(--default-color));
border-radius: 0 0 var(--radius, var(--default-radius)) var(--radius, var(--default-radius));
}
`;
// Define the element with a name, template, and styles.
@customElement({
name: 'name-tag',
template,
styles
})
class NameTag extends FASTElement { // Remove boilerplate w/ base class.
@attr greeting = 'Hello'; // A reactive HTML attribute with a default.
}
Remarquez comment tout le passe-partout disparaît? Voici quelques choses que FAST fait pour vous :
- Modèles — FAST fournit un moteur de modèles hautes performances basé sur la réactivité, y compris la prise en charge de MVVM avancés à grande échelle.
- Styles — FAST détecte automatiquement la présence de feuilles de style adoptées et les utilise si possible. Il met également en cache et réutilise les instances de feuille de style dans les instances de composant Web pour améliorer les performances et la gestion de la mémoire.
- FASTElement — La classe de base configure automatiquement le DOM Shadow et se connecte au cycle de vie pour gérer le rendu avec le modèle fourni. Le décorateur fournit un moyen déclaratif de connecter votre modèle, vos styles et votre classe, tout en les enregistrant auprès de la plate-forme à l'aide du nom fourni.
- Attributs - Au lieu de devoir déclarer manuellement un getter/setter, configurer le
observedAttributestableau et gérer les valeurs par défaut et les rappels de changement d'attribut, vous décorez simplement un champ avec@attretFASTElementgère tout cela pour vous.
Vous souhaitez en savoir plus ou rejoindre la communauté grandissante de FAST et des composants Web ?
- Trouvez FAST sur GitHub.
- Explorez la documentation FAST complète.
- Abonnez-vous au blog FAST.
- Rejoignez le serveur FAST Discord.
![Qu'est-ce qu'une liste liée, de toute façon? [Partie 1]](https://post.nghiatu.com/assets/images/m/max/724/1*Xokk6XOjWyIGCBujkJsCzQ.jpeg)



































