Une racine shadow est un sous-arbre DOM encapsulé associé à un élément hôte : les styles qu'il contient ne s'échappent pas et les styles de la page ne s'y infiltrent pas (à l'exception des propriétés héritées). Les modèles construisent l'arborescence efficacement, et les emplacements y projettent le contenu Light DOM.
Associer une racine fantôme
this.attachShadow({ mode: 'open', delegatesFocus: true });
delegatesFocus: true permet de déplacer le focus vers le premier élément sélectionnable dans l'arborescence fantôme, d'acheminer le style :focus vers l'hôte et, sur les éléments personnalisés associés à un formulaire (static formAssociated = true), de transférer les clics <label> vers le contrôle interne. Définissez-le chaque fois que le composant encapsule des commandes sélectionnables. Utilisez mode: 'open' dans presque tous les cas pour que les outils de développement, les scripts de page et l'hydratation du serveur puissent atteindre element.shadowRoot (voir Orientation des composants Web).
Modèles : créer l'arborescence efficacement
Définissez un <template> une seule fois (champ d'application du module ou champ statique) et clonez-le par instance. Le clonage d'un fragment analysé est beaucoup plus rapide que la réanalyse d'une chaîne innerHTML à chaque construction.
const template = document.createElement('template');
template.innerHTML = `
<div class="card">
<slot name="header"></slot>
<div class="body"><slot></slot></div>
<slot name="footer"></slot>
</div>
`;
class MyCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// MANDATORY: deep-clone the template content so each instance gets its own nodes.
this.shadowRoot.append(template.content.cloneNode(true));
}
}
customElements.define('my-card', MyCard);
Créez des modèles simples. Une "soupe de div" profondément imbriquée et de nombreux niveaux de racines fantômes imbriquées ajoutent un coût mesurable de résolution de style et de mise en page, ce qui a un impact majeur sur les performances à grande échelle. Privilégiez une structure plate et des éléments sémantiques.
Les emplacements : les parties qui surprennent les utilisateurs
Les emplacements par défaut et nommés fonctionnent comme indiqué dans la documentation. Voici les comportements qui posent problème :
- Les espaces comptent. Les nœuds de texte entre les éléments (y compris les espaces blancs) sont attribués à l'emplacement par défaut. Par conséquent, un emplacement par défaut est rarement vraiment "vide".
- L'ordre du DOM est conservé, pas celui des emplacements. Les nœuds projetés conservent leur ordre Light DOM d'origine pour
:first-child/:last-child, quel que soit l'emplacement qu'ils occupent. Une règle::slotted(*:first-child)cible le premier enfant DOM, et non le premier enfant de cet emplacement. Elle ne correspond donc souvent pas à ce que vous attendez. slotchangeest le signal de timing de mise à niveau. Il se déclenche lorsque les nœuds attribués changent (y compris l'attribution initiale) et constitue un moyen fiable de savoir que les enfants du Light DOM sont arrivés après une mise à niveau, alors qu'ils n'existaient peut-être pas pendantconnectedCallback.
this.shadowRoot.querySelector('slot[name="header"]')
.addEventListener('slotchange', (e) => {
const nodes = e.target.assignedElements();
this.toggleAttribute('has-header', nodes.length > 0);
});
Charger des feuilles de style dans une racine fantôme
| Méthode | Avantages | Inconvénients |
|---|---|---|
<style> dans le modèle |
Simple, entièrement encapsulé, fonctionne sans JS. | Dupliqué par instance ; gonfle le DOM à grande échelle. |
<link rel="stylesheet"> |
Fichier externe pouvant être mis en cache. | Requête supplémentaire ; risque d'un flash de contenu sans style. |
adoptedStyleSheets |
Un CSSStyleSheet partagé entre toutes les instances ; le plus rapide à grande échelle. |
Nécessite JS ; les feuilles construites ne sont pas compatibles avec @import. |
Pour de nombreuses instances d'un composant, privilégiez une feuille de style construite partagée :
const sheet = new CSSStyleSheet();
sheet.replaceSync(`:host { display: block; }`);
class MyCard extends HTMLElement {
constructor() {
super();
this.attachShadow({ mode: 'open' });
// The same sheet object is reused by every instance; no per-instance parse.
this.shadowRoot.adoptedStyleSheets = [sheet];
}
}