Le Shadow DOM permet aux développeurs Web de créer des DOM et des CSS compartimentés pour les composants Web.
Résumé
Shadow DOM élimine la fragilité de la création d'applications Web. La fragilité provient de la nature globale du HTML, du CSS et du JS. Au fil des ans, nous avons inventé un nombre
exorbitant d'outils pour contourner ces problèmes. Par exemple, lorsque vous utilisez un nouvel ID/une nouvelle classe HTML, il est impossible de savoir s'il entrera en conflit avec un nom existant utilisé par la page.
Des bugs subtils apparaissent, la spécificité CSS devient un problème majeur (!important tout !), les sélecteurs de style deviennent incontrôlables et les performances peuvent en pâtir. et bien plus encore.
Shadow DOM corrige les problèmes liés au CSS et au DOM. Il introduit les styles limités sur la plate-forme Web. Sans outils ni conventions d'attribution de noms, vous pouvez regrouper le CSS avec le balisage, masquer les détails d'implémentation et créer des composants autonomes en JavaScript vanilla.
Introduction
Le Shadow DOM est l'une des trois normes de composants Web : Modèles HTML, Shadow DOM et Éléments personnalisés. Les importations HTML faisaient partie de la liste, mais sont désormais considérées comme obsolètes.
Vous n'avez pas besoin de créer des composants Web qui utilisent un DOM fantôme. Mais lorsque vous le faites, vous profitez de ses avantages (définition du champ d'application CSS, encapsulation DOM, composition) et créez des éléments personnalisés réutilisables, résilients, hautement configurables et extrêmement réutilisables. Si les éléments personnalisés permettent de créer un nouveau code HTML (avec une API JS), le Shadow DOM permet de fournir son code HTML et CSS. Les deux API se combinent pour créer un composant avec du code HTML, CSS et JavaScript autonome.
Le Shadow DOM est conçu comme un outil permettant de créer des applications basées sur des composants. Il apporte donc des solutions aux problèmes courants du développement Web :
- DOM isolé : le DOM d'un composant est autonome (par exemple,
document.querySelector()ne renvoie pas de nœuds dans le Shadow DOM du composant). - CSS limité : le CSS défini dans le shadow DOM est limité à celui-ci. Les règles de style ne sont pas divulguées et les styles de page ne sont pas appliqués.
- Composition : concevez une API déclarative basée sur le balisage pour votre composant.
- Simplification du CSS : le DOM limité signifie que vous pouvez utiliser des sélecteurs CSS simples, des noms d'ID/de classe plus génériques, et que vous n'avez pas à vous soucier des conflits de noms.
- Productivité : considérez les applications comme des blocs de DOM plutôt que comme une seule grande page (globale).
fancy-tabs demo
Tout au long de cet article, je ferai référence à un composant de démonstration (<fancy-tabs>) et à des extraits de code de ce composant. Si votre navigateur est compatible avec les API, vous devriez voir une démonstration en direct juste en dessous. Sinon, consultez la source complète sur GitHub.
Qu'est-ce que le shadow DOM ?
Contexte sur le DOM
Le HTML est le moteur du Web, car il est facile à utiliser. En déclarant quelques balises, vous pouvez créer en quelques secondes une page qui présente à la fois une présentation et une structure. Cependant, le code HTML n'est pas très utile en soi. Il est facile pour les humains de comprendre un langage textuel, mais les machines ont besoin de quelque chose de plus. Saisissez le Document Object Model, ou DOM.
Lorsque le navigateur charge une page Web, il effectue un certain nombre d'opérations intéressantes. Il transforme notamment le code HTML de l'auteur en un document actif. En gros, pour comprendre la structure de la page, le navigateur analyse le code HTML (chaînes de texte statiques) et le convertit en un modèle de données (objets/nœuds). Le navigateur conserve la hiérarchie du code HTML en créant une arborescence de ces nœuds : le DOM. L'avantage du DOM est qu'il s'agit d'une représentation en direct de votre page. Contrairement au code HTML statique que nous créons, les nœuds produits par le navigateur contiennent des propriétés, des méthodes et, surtout, peuvent être manipulés par des programmes. C'est pourquoi nous pouvons créer des éléments DOM directement à l'aide de JavaScript :
const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);
génère le balisage HTML suivant :
<body>
<header>
<h1>Hello DOM</h1>
</header>
</body>
Tout cela est bien beau. Alors, qu'est-ce que le Shadow DOM ?
DOM… dans l'ombre
Le Shadow DOM est un DOM normal, à deux différences près : 1) la façon dont il est créé/utilisé et 2) son comportement par rapport au reste de la page. Normalement, vous créez des nœuds DOM et les ajoutez en tant qu'enfants d'un autre élément. Avec le shadow DOM, vous créez une arborescence DOM limitée qui est associée à l'élément, mais distincte de ses enfants réels. Cette sous-arborescence limitée est appelée shadow tree. L'élément auquel il est associé est son hôte shadow. Tout ce que vous ajoutez dans le shadow DOM devient local à l'élément d'hébergement, y compris <style>. C'est ainsi que le shadow DOM réalise la portée du style CSS.
Créer un DOM fantôme
Une racine shadow est un fragment de document associé à un élément "hôte".
L'élément obtient son Shadow DOM en associant une racine Shadow. Pour créer un Shadow DOM pour un élément, appelez element.attachShadow() :
const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().
// header.shadowRoot === shadowRoot
// shadowRoot.host === header
J'utilise .innerHTML pour remplir la racine fantôme, mais vous pouvez également utiliser d'autres API DOM. C'est le Web. Nous avons le choix.
La spécification définit une liste d'éléments qui ne peuvent pas héberger d'arborescence shadow. Un élément peut figurer sur la liste pour plusieurs raisons :
- Le navigateur héberge déjà son propre shadow DOM interne pour l'élément (
<textarea>,<input>). - L'élément ne peut pas héberger un shadow DOM (
<img>).
Par exemple, l'exemple suivant ne fonctionne pas :
document.createElement('input').attachShadow({mode: 'open'});
// Error. `<input>` cannot host shadow dom.
Créer un Shadow DOM pour un élément personnalisé
Le Shadow DOM est particulièrement utile lors de la création d'éléments personnalisés. Utilisez le Shadow DOM pour compartimenter le code HTML, CSS et JS d'un élément, ce qui permet de produire un "composant Web".
Exemple : un élément personnalisé associe un Shadow DOM à lui-même, encapsulant son DOM/CSS :
// Use custom elements API v1 to register a new HTML tag and define its JS behavior
// using an ES6 class. Every instance of <fancy-tab> will have this same prototype.
customElements.define('fancy-tabs', class extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
// Attach a shadow root to <fancy-tabs>.
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! -->
<div id="tabs">...</div>
<div id="panels">...</div>
`;
}
...
});
Voici quelques points à noter : La première est que l'élément personnalisé crée son propre Shadow DOM lorsqu'une instance de <fancy-tabs> est créée. Cela se fait dans le constructor(). Deuxièmement, comme nous créons une racine shadow, les règles CSS de <style> seront limitées à <fancy-tabs>.
Composition et emplacements
La composition est l'une des fonctionnalités les moins bien comprises du shadow DOM, mais c'est sans doute la plus importante.
Dans le monde du développement Web, la composition est la façon dont nous construisons des applications, de manière déclarative à partir du code HTML. Différents composants (<div>, <header>, <form>, <input>) sont combinés pour former des applications. Certaines de ces balises fonctionnent même ensemble. La composition est la raison pour laquelle les éléments natifs tels que <select>, <details>, <form> et <video> sont si flexibles. Chacune de ces balises accepte certains éléments HTML en tant qu'enfants et effectue une action spéciale avec eux. Par exemple, <select> sait comment afficher <option> et <optgroup> dans des widgets de menu déroulant et de sélection multiple. L'élément <details> affiche <summary> sous la forme d'une flèche de développement. Même <video> sait comment gérer certains enfants : les éléments <source> ne sont pas affichés, mais ils affectent le comportement de la vidéo.
C'est magique !
Terminologie : Light DOM et Shadow DOM
La composition Shadow DOM introduit un certain nombre de nouveaux principes fondamentaux dans le développement Web. Avant d'entrer dans les détails, nous allons définir une terminologie standard afin de parler le même langage.
Light DOM
Le balisage qu'un utilisateur de votre composant écrit. Ce DOM se trouve en dehors du Shadow DOM du composant. Il s'agit des enfants réels de l'élément.
<better-button>
<!-- the image and span are better-button's light DOM -->
<img src="gear.svg" slot="icon">
<span>Settings</span>
</better-button>
Shadow DOM
Le DOM écrit par l'auteur d'un composant. Le Shadow DOM est local au composant et définit sa structure interne, son CSS limité et encapsule les détails de votre implémentation. Il peut également définir comment afficher le balisage créé par l'utilisateur de votre composant.
#shadow-root
<style>...</style>
<slot name="icon"></slot>
<span id="wrapper">
<slot>Button</slot>
</span>
Arborescence DOM aplatie
Résultat de la distribution par le navigateur du Light DOM de l'utilisateur dans votre Shadow DOM, ce qui permet d'afficher le produit final. L'arborescence aplatie est ce que vous voyez finalement dans les outils de développement et ce qui est affiché sur la page.
<better-button>
#shadow-root
<style>...</style>
<slot name="icon">
<img src="gear.svg" slot="icon">
</slot>
<span id="wrapper">
<slot>
<span>Settings</span>
</slot>
</span>
</better-button>
L'élément <slot>
Le Shadow DOM compose différents arbres DOM à l'aide de l'élément <slot>.
Les emplacements sont des espaces réservés dans votre composant que les utilisateurs peuvent remplir avec leur propre balisage. En définissant un ou plusieurs emplacements, vous invitez le balisage externe à s'afficher dans le DOM fantôme de votre composant. En gros, vous dites "Affiche le balisage de l'utilisateur ici".
Les éléments sont autorisés à "franchir" la limite du Shadow DOM lorsqu'un <slot> les y invite. Ces éléments sont appelés nœuds distribués. Conceptuellement, les nœuds distribués peuvent sembler un peu bizarres. Les emplacements ne déplacent pas physiquement le DOM. Ils l'affichent à un autre emplacement dans le Shadow DOM.
Un composant peut définir zéro, un ou plusieurs emplacements dans son shadow DOM. Les emplacements peuvent être vides ou fournir un contenu de remplacement. Si l'utilisateur ne fournit pas de contenu Light DOM, l'emplacement affiche son contenu de remplacement.
<!-- Default slot. If there's more than one default slot, the first is used. -->
<slot></slot>
<slot>fallback content</slot> <!-- default slot with fallback content -->
<slot> <!-- default slot entire DOM tree as fallback -->
<h2>Title</h2>
<summary>Description text</summary>
</slot>
Vous pouvez également créer des emplacements nommés. Les emplacements nommés sont des trous spécifiques dans votre shadow DOM que les utilisateurs référencent par leur nom.
Exemple : emplacements dans le Shadow DOM de <fancy-tabs> :
#shadow-root
<div id="tabs">
<slot id="tabsSlot" name="title"></slot> <!-- named slot -->
</div>
<div id="panels">
<slot id="panelsSlot"></slot>
</div>
Les utilisateurs du composant déclarent <fancy-tabs> comme suit :
<fancy-tabs>
<button slot="title">Title</button>
<button slot="title" selected>Title 2</button>
<button slot="title">Title 3</button>
<section>content panel 1</section>
<section>content panel 2</section>
<section>content panel 3</section>
</fancy-tabs>
<!-- Using <h2>'s and changing the ordering would also work! -->
<fancy-tabs>
<h2 slot="title">Title</h2>
<section>content panel 1</section>
<h2 slot="title" selected>Title 2</h2>
<section>content panel 2</section>
<h2 slot="title">Title 3</h2>
<section>content panel 3</section>
</fancy-tabs>
Si vous vous posez la question, l'arborescence aplatie ressemble à ceci :
<fancy-tabs>
#shadow-root
<div id="tabs">
<slot id="tabsSlot" name="title">
<button slot="title">Title</button>
<button slot="title" selected>Title 2</button>
<button slot="title">Title 3</button>
</slot>
</div>
<div id="panels">
<slot id="panelsSlot">
<section>content panel 1</section>
<section>content panel 2</section>
<section>content panel 3</section>
</slot>
</div>
</fancy-tabs>
Notez que notre composant peut gérer différentes configurations, mais que l'arborescence DOM aplatie reste la même. Nous pouvons également passer de <button> à <h2>. Ce composant a été conçu pour gérer différents types d'enfants, tout comme <select>.
Attribution d'un style
Il existe de nombreuses options pour styliser les composants Web. Un composant qui utilise le shadow DOM peut être stylisé par la page principale, définir ses propres styles ou fournir des hooks (sous la forme de propriétés CSS personnalisées) permettant aux utilisateurs de remplacer les valeurs par défaut.
Styles définis par le composant
La fonctionnalité la plus utile du shadow DOM est sans aucun doute le CSS limité :
- Les sélecteurs CSS de la page externe ne s'appliquent pas à l'intérieur de votre composant.
- Les styles définis à l'intérieur ne s'appliquent pas à l'extérieur. Elles sont limitées à l'élément hôte.
Les sélecteurs CSS utilisés dans le shadow DOM s'appliquent localement à votre composant. En pratique, cela signifie que nous pouvons réutiliser des noms d'ID/de classe courants, sans nous soucier des conflits ailleurs sur la page. Il est recommandé d'utiliser des sélecteurs CSS plus simples dans le Shadow DOM. Elles sont également bonnes pour les performances.
Exemple : les styles définis dans une racine fantôme sont locaux
#shadow-root
<style>
#panels {
box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
background: white;
...
}
#tabs {
display: inline-flex;
...
}
</style>
<div id="tabs">
...
</div>
<div id="panels">
...
</div>
Les feuilles de style sont également limitées à l'arborescence fantôme :
#shadow-root
<link rel="stylesheet" href="styles.css">
<div id="tabs">
...
</div>
<div id="panels">
...
</div>
Vous êtes-vous déjà demandé comment l'élément <select> affiche un widget de sélection multiple (au lieu d'un menu déroulant) lorsque vous ajoutez l'attribut multiple ?
<select multiple>
<option>Do</option>
<option selected>Re</option>
<option>Mi</option>
<option>Fa</option>
<option>So</option>
</select>
<select> peut se styliser différemment en fonction des attributs que vous déclarez. Les composants Web peuvent également se styliser eux-mêmes à l'aide du sélecteur :host.
Exemple : un composant qui se stylise lui-même
<style>
:host {
display: block; /* by default, custom elements are display: inline */
contain: content; /* CSS containment FTW. */
}
</style>
L'un des pièges de :host est que les règles de la page parente ont une spécificité plus élevée que les règles :host définies dans l'élément. Autrement dit, les styles externes l'emportent. Cela permet aux utilisateurs de remplacer votre style de niveau supérieur de l'extérieur. De plus, :host ne fonctionne que dans le contexte d'une racine shadow. Vous ne pouvez donc pas l'utiliser en dehors du shadow DOM.
La forme fonctionnelle de :host(<selector>) vous permet de cibler l'hôte s'il correspond à un <selector>. C'est un excellent moyen pour votre composant d'encapsuler des comportements qui réagissent à l'interaction de l'utilisateur, ou de définir l'état ou le style des nœuds internes en fonction de l'hôte.
<style>
:host {
opacity: 0.4;
will-change: opacity;
transition: opacity 300ms ease-in-out;
}
:host(:hover) {
opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
background: grey;
pointer-events: none;
opacity: 0.4;
}
:host(.blue) {
color: blue; /* color host when it has class="blue" */
}
:host(.pink) > #tabs {
color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>
Style basé sur le contexte
:host-context(<selector>) correspond au composant s'il correspond à <selector> ou si l'un de ses ancêtres correspond à. Cette méthode est souvent utilisée pour appliquer un thème en fonction de l'environnement d'un composant. Par exemple, de nombreuses personnes appliquent un thème en appliquant une classe à <html> ou <body> :
<body class="darktheme">
<fancy-tabs>
...
</fancy-tabs>
</body>
:host-context(.darktheme) applique un style à <fancy-tabs> lorsqu'il est un descendant de .darktheme :
:host-context(.darktheme) {
color: white;
background: black;
}
:host-context() peut être utile pour la thématisation, mais une approche encore plus efficace consiste à créer des hooks de style à l'aide de propriétés CSS personnalisées.
Styliser des nœuds distribués
::slotted(<compound-selector>) correspond aux nœuds distribués dans un <slot>.
Imaginons que nous ayons créé un composant de badge nominatif :
<name-badge>
<h2>Eric Bidelman</h2>
<span class="title">
Digital Jedi, <span class="company">Google</span>
</span>
</name-badge>
Le Shadow DOM du composant peut styliser les éléments <h2> et .title de l'utilisateur :
<style>
::slotted(h2) {
margin: 0;
font-weight: 300;
color: red;
}
::slotted(.title) {
color: orange;
}
/* DOESN'T WORK (can only select top-level nodes).
::slotted(.company),
::slotted(.title .company) {
text-transform: uppercase;
}
*/
</style>
<slot></slot>
Comme vous vous en souvenez peut-être, les <slot> ne déplacent pas le DOM léger de l'utilisateur. Lorsque les nœuds sont distribués dans un <slot>, le <slot> affiche leur DOM, mais les nœuds restent physiquement en place. Les styles appliqués avant la distribution continuent de s'appliquer après la distribution. Toutefois, lorsque le Light DOM est distribué, il peut adopter des styles supplémentaires (ceux définis par le Shadow DOM).
Voici un autre exemple plus détaillé de <fancy-tabs> :
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>
#panels {
box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
background: white;
border-radius: 3px;
padding: 16px;
height: 250px;
overflow: auto;
}
#tabs {
display: inline-flex;
-webkit-user-select: none;
user-select: none;
}
#tabsSlot::slotted(*) {
font: 400 16px/22px 'Roboto';
padding: 16px 8px;
...
}
#tabsSlot::slotted([aria-selected="true"]) {
font-weight: 600;
background: white;
box-shadow: none;
}
#panelsSlot::slotted([aria-hidden="true"]) {
display: none;
}
</style>
<div id="tabs">
<slot id="tabsSlot" name="title"></slot>
</div>
<div id="panels">
<slot id="panelsSlot"></slot>
</div>
`;
Dans cet exemple, il y a deux emplacements : un emplacement nommé pour les titres des onglets et un emplacement pour le contenu du panneau des onglets. Lorsque l'utilisateur sélectionne un onglet, nous mettons sa sélection en gras et affichons le panneau correspondant. Pour ce faire, sélectionnez les nœuds distribués qui possèdent l'attribut selected. Le code JS de l'élément personnalisé (non affiché ici) ajoute cet attribut au bon moment.
Styliser un composant de l'extérieur
Il existe plusieurs façons de styliser un composant de l'extérieur. Le moyen le plus simple consiste à utiliser le nom du tag comme sélecteur :
fancy-tabs {
width: 500px;
color: red; /* Note: inheritable CSS properties pierce the shadow DOM boundary. */
}
fancy-tabs:hover {
box-shadow: 0 3px 3px #ccc;
}
Les styles externes l'emportent toujours sur les styles définis dans le shadow DOM. Par exemple, si l'utilisateur écrit le sélecteur fancy-tabs { width: 500px; }, il l'emportera sur la règle du composant : :host { width: 650px;}.
La mise en forme du composant lui-même ne vous mènera pas très loin. Mais que se passe-t-il si vous souhaitez styliser les éléments internes d'un composant ? Pour cela, nous avons besoin de propriétés CSS personnalisées.
Créer des hooks de style à l'aide de propriétés CSS personnalisées
Les utilisateurs peuvent modifier les styles internes si l'auteur du composant fournit des hooks de style à l'aide de propriétés CSS personnalisées. Conceptuellement, l'idée est similaire à <slot>. Vous créez des "espaces réservés de style" que les utilisateurs peuvent remplacer.
Exemple : <fancy-tabs> permet aux utilisateurs de remplacer la couleur d'arrière-plan :
<!-- main page -->
<style>
fancy-tabs {
margin-bottom: 32px;
--fancy-tabs-bg: black;
}
</style>
<fancy-tabs background>...</fancy-tabs>
Dans son Shadow DOM :
:host([background]) {
background: var(--fancy-tabs-bg, #9E9E9E);
border-radius: 10px;
padding: 10px;
}
Dans ce cas, le composant utilisera black comme valeur d'arrière-plan, car l'utilisateur l'a fournie. Sinon, la valeur par défaut serait #9E9E9E.
Rubriques avancées
Créer des racines de shadow DOM fermées (à éviter)
Il existe une autre variante du shadow DOM, appelée mode "fermé". Lorsque vous créez une arborescence Shadow DOM fermée, le code JavaScript externe ne peut pas accéder au DOM interne de votre composant. Ce fonctionnement est semblable à celui des éléments natifs tels que <video>.
JavaScript ne peut pas accéder au DOM fantôme de <video>, car le navigateur l'implémente à l'aide d'une racine fantôme en mode fermé.
Exemple : Créer un arbre fantôme fermé :
const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div
D'autres API sont également concernées par le mode fermé :
Element.assignedSlot/TextNode.assignedSlotrenvoienullEvent.composedPath()pour les événements associés à des éléments dans le DOM fantôme, renvoie []
Voici un résumé des raisons pour lesquelles vous ne devriez jamais créer de composants Web avec {mode: 'closed'} :
Faux sentiment de sécurité. Rien n'empêche un pirate de détourner
Element.prototype.attachShadow.Le mode fermé empêche le code de votre élément personnalisé d'accéder à son propre DOM fantôme. C'est un échec total. Vous devrez plutôt stocker une référence pour plus tard si vous souhaitez utiliser des éléments tels que
querySelector(). Cela va complètement à l'encontre de l'objectif initial du mode fermé.customElements.define('x-element', class extends HTMLElement { constructor() { super(); // always call super() first in the constructor. this._shadowRoot = this.attachShadow({mode: 'closed'}); this._shadowRoot.innerHTML = '<div class="wrapper"></div>'; } connectedCallback() { // When creating closed shadow trees, you'll need to stash the shadow root // for later if you want to use it again. Kinda pointless. const wrapper = this._shadowRoot.querySelector('.wrapper'); } ... });Le mode fermé rend votre composant moins flexible pour les utilisateurs finaux. Lorsque vous créez des composants Web, il peut arriver que vous oubliiez d'ajouter une fonctionnalité. Option de configuration. Un cas d'utilisation souhaité par l'utilisateur. Un exemple courant consiste à oublier d'inclure des hooks de style adéquats pour les nœuds internes. En mode fermé, les utilisateurs ne peuvent pas remplacer les valeurs par défaut ni modifier les styles. La possibilité d'accéder aux composants internes est très utile. En fin de compte, les utilisateurs créeront un fork de votre composant, en trouveront un autre ou créeront le leur s'il ne fait pas ce qu'ils veulent :(
Utiliser des emplacements en JS
L'API Shadow DOM fournit des utilitaires permettant de travailler avec des emplacements et des nœuds distribués. Ils sont utiles lors de la création d'un élément personnalisé.
événement slotchange
L'événement slotchange se déclenche lorsque les nœuds distribués d'un emplacement changent. Par exemple, si l'utilisateur ajoute/supprime des enfants du DOM léger.
const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
console.log('light dom children changed!');
});
Pour surveiller d'autres types de modifications apportées au DOM léger, vous pouvez configurer un MutationObserver dans le constructeur de votre élément.
Quels éléments sont affichés dans un emplacement ?
Il est parfois utile de savoir quels éléments sont associés à un emplacement. Appelez slot.assignedNodes() pour identifier les éléments affichés par l'emplacement. L'option {flatten: true} renvoie également le contenu de remplacement d'un emplacement (si aucun nœud n'est distribué).
Par exemple, imaginons que votre Shadow DOM ressemble à ceci :
<slot><b>fallback content</b></slot>
| Utilisation | Appeler | Résultat |
|---|---|---|
| <my-component>component text</my-component> | slot.assignedNodes(); |
[component text] |
| <my-component></my-component> | slot.assignedNodes(); |
[] |
| <my-component></my-component> | slot.assignedNodes({flatten: true}); |
[<b>fallback content</b>] |
À quel emplacement un élément est-il attribué ?
Il est également possible de répondre à la question inverse. element.assignedSlot vous indique à quel emplacement de composant votre élément est attribué.
Modèle d'événement Shadow DOM
Lorsqu'un événement remonte du Shadow DOM, sa cible est ajustée pour maintenir l'encapsulation fournie par le Shadow DOM. En d'autres termes, les événements sont reciblés pour donner l'impression qu'ils proviennent du composant plutôt que d'éléments internes de votre Shadow DOM. Certains événements ne se propagent même pas en dehors du DOM fantôme.
Les événements qui franchissent la limite du shadow DOM sont les suivants :
- Événements de focus :
blur,focus,focusin,focusout - Événements de souris :
click,dblclick,mousedown,mouseenter,mousemove, etc. - Événements de molette :
wheel - Événements d'entrée :
beforeinput,input - Événements de clavier :
keydown,keyup - Événements de composition :
compositionstart,compositionupdate,compositionend - DragEvent :
dragstart,drag,dragend,drop, etc.
Conseils
Si l'arborescence shadow est ouverte, l'appel de event.composedPath() renvoie un tableau de nœuds par lesquels l'événement est passé.
Utiliser des événements personnalisés
Les événements DOM personnalisés qui sont déclenchés sur des nœuds internes d'une arborescence fantôme ne se propagent pas au-delà de la limite fantôme, sauf si l'événement est créé à l'aide de l'indicateur composed: true :
// Inside <fancy-tab> custom element class definition:
selectTab() {
const tabs = this.shadowRoot.querySelector('#tabs');
tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}
Si la valeur est composed: false (par défaut), les consommateurs ne pourront pas écouter l'événement en dehors de votre racine fantôme.
<fancy-tabs></fancy-tabs>
<script>
const tabs = document.querySelector('fancy-tabs');
tabs.addEventListener('tab-select', e => {
// won't fire if `tab-select` wasn't created with `composed: true`.
});
</script>
Gérer le focus
Si vous vous souvenez du modèle d'événement du DOM fantôme, les événements déclenchés dans le DOM fantôme sont ajustés pour donner l'impression qu'ils proviennent de l'élément d'hébergement.
Par exemple, supposons que vous cliquiez sur un <input> dans une racine fantôme :
<x-focus>
#shadow-root
<input type="text" placeholder="Input inside shadow dom">
L'événement focus semblera provenir de <x-focus>, et non de <input>.
De même, document.activeElement sera <x-focus>. Si la racine fantôme a été créée avec mode:'open' (voir Mode fermé), vous pourrez également accéder au nœud interne qui a été sélectionné :
document.activeElement.shadowRoot.activeElement // only works with open mode.
Si plusieurs niveaux de Shadow DOM sont en jeu (par exemple, un élément personnalisé dans un autre élément personnalisé), vous devez explorer de manière récursive les racines fantômes pour trouver l'élément activeElement :
function deepActiveElement() {
let a = document.activeElement;
while (a && a.shadowRoot && a.shadowRoot.activeElement) {
a = a.shadowRoot.activeElement;
}
return a;
}
Une autre option de sélection est l'option delegatesFocus: true, qui étend le comportement de sélection des éléments dans une arborescence fantôme :
- Si vous cliquez sur un nœud dans le DOM fantôme et que ce nœud n'est pas une zone sélectionnable, la première zone sélectionnable est sélectionnée.
- Lorsqu'un nœud du shadow DOM est sélectionné,
:focuss'applique à l'hôte en plus de l'élément sélectionné.
Exemple : comment delegatesFocus: true modifie le comportement de la mise au point
<style>
:focus {
outline: 2px solid red;
}
</style>
<x-focus></x-focus>
<script>
customElements.define('x-focus', class extends HTMLElement {
constructor() {
super(); // always call super() first in the constructor.
const root = this.attachShadow({mode: 'open', delegatesFocus: true});
root.innerHTML = `
<style>
:host {
display: flex;
border: 1px dotted black;
padding: 16px;
}
:focus {
outline: 2px solid blue;
}
</style>
<div>Clickable Shadow DOM text</div>
<input type="text" placeholder="Input inside shadow dom">`;
// Know the focused element inside shadow DOM:
this.addEventListener('focus', function(e) {
console.log('Active element (inside shadow dom):',
this.shadowRoot.activeElement);
});
}
});
</script>
Résultat
Ci-dessus, vous pouvez voir le résultat lorsque <x-focus> est sélectionné (clic de l'utilisateur, tabulation, focus(), etc.), que l'utilisateur clique sur "Clickable Shadow DOM text" ou que l'élément <input> interne est sélectionné (y compris autofocus).
Si vous définissez delegatesFocus: false, voici ce que vous verrez à la place :
delegatesFocus: false et l'élément <input> interne est sélectionné.
delegatesFocus: false et <x-focus>
reçoivent le focus (par exemple, il comporte tabindex="0").
delegatesFocus: false et "Clickable Shadow DOM text" (Texte Shadow DOM cliquable) sont cliqués (ou une autre zone vide du Shadow DOM de l'élément est cliquée).
Conseils et astuces
Au fil des ans, j'ai appris quelques astuces sur la création de composants Web. Je pense que certains de ces conseils vous seront utiles pour créer des composants et déboguer le Shadow DOM.
Utiliser le confinement CSS
En règle générale, la mise en page, le style et l'affichage d'un composant Web sont assez autonomes. Utilisez le confinement CSS dans :host pour améliorer les performances :
<style>
:host {
display: block;
contain: content; /* Boom. CSS containment FTW. */
}
</style>
Réinitialiser les styles héritables
Les styles héritables (background, color, font, line-height, etc.) continuent d'être hérités dans le Shadow DOM. Autrement dit, ils percent la limite du Shadow DOM par défaut. Si vous souhaitez repartir de zéro, utilisez all: initial; pour réinitialiser les styles héritables à leur valeur initiale lorsqu'ils franchissent la limite du shadow DOM.
<style>
div {
padding: 10px;
background: red;
font-size: 25px;
text-transform: uppercase;
color: white;
}
</style>
<div>
<p>I'm outside the element (big/white)</p>
<my-element>Light DOM content is also affected.</my-element>
<p>I'm outside the element (big/white)</p>
</div>
<script>
const el = document.querySelector('my-element');
el.attachShadow({mode: 'open'}).innerHTML = `
<style>
:host {
all: initial; /* 1st rule so subsequent properties are reset. */
display: block;
background: white;
}
</style>
<p>my-element: all CSS properties are reset to their
initial value using <code>all: initial</code>.</p>
<slot></slot>
`;
</script>
Rechercher tous les éléments personnalisés utilisés par une page
Il est parfois utile de trouver les éléments personnalisés utilisés sur la page. Pour ce faire, vous devez parcourir de manière récursive le DOM fantôme de tous les éléments utilisés sur la page.
const allCustomElements = [];
function isCustomElement(el) {
const isAttr = el.getAttribute('is');
// Check for <super-button> and <button is="super-button">.
return el.localName.includes('-') || isAttr && isAttr.includes('-');
}
function findAllCustomElements(nodes) {
for (let i = 0, el; el = nodes[i]; ++i) {
if (isCustomElement(el)) {
allCustomElements.push(el);
}
// If the element has shadow DOM, dig deeper.
if (el.shadowRoot) {
findAllCustomElements(el.shadowRoot.querySelectorAll('*'));
}
}
}
findAllCustomElements(document.querySelectorAll('*'));
Créer des éléments à partir d'un <template>
Au lieu de remplir une racine fantôme à l'aide de .innerHTML, nous pouvons utiliser un <template> déclaratif. Les modèles sont des espaces réservés idéaux pour déclarer la structure d'un composant Web.
Consultez l'exemple dans Éléments personnalisés : créer des composants Web réutilisables.
Historique et navigateurs compatibles
Si vous suivez l'évolution des composants Web depuis quelques années, vous savez que Chrome 35 et versions ultérieures ainsi qu'Opera proposent une ancienne version du shadow DOM depuis un certain temps. Blink continuera à prendre en charge les deux versions en parallèle pendant un certain temps. La spécification v0 fournissait une méthode différente pour créer une racine shadow (element.createShadowRoot au lieu de element.attachShadow pour la v1). L'appel de l'ancienne méthode continue de créer une racine shadow avec la sémantique v0, de sorte que le code v0 existant ne sera pas interrompu.
Si vous êtes intéressé par l'ancienne spécification v0, consultez les articles html5rocks : 1, 2, 3. Vous trouverez également une excellente comparaison des différences entre le shadow DOM v0 et le shadow DOM v1.
Prise en charge des navigateurs
Shadow DOM v1 est disponible dans Chrome 53 (état), Opera 40, Safari 10 et Firefox 63. Edge a commencé le développement.
Pour détecter la fonctionnalité Shadow DOM, vérifiez l'existence de attachShadow :
const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;
Polyfill
En attendant que la compatibilité avec les navigateurs soit largement disponible, les polyfills shadydom et shadycss vous offrent la fonctionnalité v1. Shady DOM imite la portée DOM de Shadow DOM et shadycss polyfills les propriétés CSS personnalisées et la portée de style fournie par l'API native.
Installez les polyfills :
bower install --save webcomponents/shadydom
bower install --save webcomponents/shadycss
Utilisez les polyfills :
function loadScript(src) {
return new Promise(function(resolve, reject) {
const script = document.createElement('script');
script.async = true;
script.src = src;
script.onload = resolve;
script.onerror = reject;
document.head.appendChild(script);
});
}
// Lazy load the polyfill if necessary.
if (!supportsShadowDOMV1) {
loadScript('/bower_components/shadydom/shadydom.min.js')
.then(e => loadScript('/bower_components/shadycss/shadycss.min.js'))
.then(e => {
// Polyfills loaded.
});
} else {
// Native shadow dom v1 support. Go to go!
}
Consultez la page https://github.com/webcomponents/shadycss#usage pour savoir comment shim/scoper vos styles.
Conclusion
Pour la première fois, nous disposons d'une primitive d'API qui effectue un champ d'application CSS et DOM approprié, et qui dispose d'une véritable composition. Combiné à d'autres API de composants Web telles que les éléments personnalisés, le shadow DOM permet de créer des composants véritablement encapsulés sans piratage ni utilisation d'anciens éléments tels que les <iframe>.
Ne vous méprenez pas. Le Shadow DOM est un concept complexe. Mais c'est un outil qui vaut la peine d'être appris. Prenez le temps de l'explorer. Apprenez-le et posez des questions !
Documentation complémentaire
- Différences entre Shadow DOM v1 et v0
- "Introducing Slot-Based Shadow DOM API" (Présentation de l'API Shadow DOM basée sur les emplacements) sur le blog WebKit.
- Web Components and the future of Modular CSS par Philip Walton
- "Éléments personnalisés : créer des composants Web réutilisables", extrait de WebFundamentals de Google.
- Spécification Shadow DOM v1
- Spécifications des éléments personnalisés v1
Questions fréquentes
Puis-je utiliser Shadow DOM v1 dès aujourd'hui ?
Oui, avec un polyfill. Consultez Navigateurs compatibles.
Quelles fonctionnalités de sécurité le shadow DOM fournit-il ?
Le Shadow DOM n'est pas une fonctionnalité de sécurité. C'est un outil léger pour définir la portée du CSS et masquer les arborescences DOM dans un composant. Si vous souhaitez une véritable limite de sécurité, utilisez un <iframe>.
Un composant Web doit-il utiliser le Shadow DOM ?
Non. Vous n'avez pas besoin de créer des composants Web qui utilisent le shadow DOM. Toutefois, la création d'éléments personnalisés qui utilisent le Shadow DOM vous permet de profiter de fonctionnalités telles que la portée CSS, l'encapsulation DOM et la composition.
Quelle est la différence entre les racines fantômes ouvertes et fermées ?
Consultez Racines de shadow DOM fermées.