Shadow DOM v1 – Composants Web autonomes

Shadow DOM permet aux développeurs Web de créer des éléments DOM et CSS compartimentés pour les composants Web.

Résumé

Shadow DOM élimine la fragilité liée à la création d'applications Web. La fragilité due à la nature globale du code HTML, CSS et JavaScript. Au fil des ans, nous avons inventé un nombre exorbitant tools permettant de contourner les problèmes. Par exemple, lorsque vous utilisez un nouvel ID ou 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 s'accumulent, la spécificité CSS devient un problème de taille (!important tout cela !), les sélecteurs de style deviennent hors de contrôle et les performances peuvent en pâtir. La liste continue.

Correction du Shadow DOM qui corrige le CSS et le DOM Elle introduit les styles de champ d'application pour la plate-forme Web. Sans outils ni conventions d'attribution de noms, vous pouvez grouper le CSS avec le balisage, masquer les détails de l'implémentation et créer des composants autonomes en JavaScript vanilla.

Introduction

Shadow DOM est l'une des trois normes qui s'appliquent aux composants Web : les modèles HTML, le Shadow DOM et les éléments personnalisés. Les importations HTML faisaient auparavant 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 le Shadow DOM. Toutefois, vous profitez ainsi de ses avantages (scoping CSS, encapsulation DOM, composition) et créez des éléments personnalisés réutilisables, qui sont résilients, hautement configurables et extrêmement réutilisables. Si les éléments personnalisés permettent de créer un code HTML (avec une API JavaScript), le Shadow DOM vous permet de fournir son code HTML et CSS. Les deux API se combinent pour créer un composant avec des éléments HTML, CSS et JavaScript autonomes.

Shadow DOM est conçu comme un outil permettant de créer des applications basées sur des composants. Par conséquent, il apporte 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 délimité: le CSS défini dans le Shadow DOM y est limité. Les règles de style ne s'affichent pas et les styles de page ne s'effacent pas.
  • Composition: concevez une API déclarative basée sur le balisage pour votre composant.
  • Simplification du CSS : le DOM délimité vous permet d'utiliser des sélecteurs CSS simples et des noms d'ID/de classe plus génériques, sans vous soucier des conflits de noms.
  • Productivité : considérez les applications comme des fragments de DOM plutôt que par une grande page (globale).

Démonstration fancy-tabs

Tout au long de cet article, je vais faire référence à un composant de démonstration (<fancy-tabs>) et aux extraits de code qu'il contient. Si votre navigateur est compatible avec les API, vous devriez en voir une démonstration en direct juste en dessous. Sinon, consultez la source complète sur GitHub.

Afficher la source sur GitHub

Qu'est-ce que le Shadow DOM ?

Arrière-plan du DOM

Le HTML est au cœur du Web, car il est facile à utiliser. En déclarant quelques balises, vous pouvez créer en quelques secondes une page présentant à la fois une présentation et une structure. Toutefois, le langage HTML n'est pas très utile en soi. Il est facile de comprendre un langage textuel, mais les machines ont besoin de quelque chose de plus. Saisissez le modèle objet du document, ou DOM.

Lorsque le navigateur charge une page Web, il effectue un tas de choses intéressantes. L'une de ses fonctions consiste à transformer le code HTML de l'auteur en document en ligne. En gros, pour comprendre la structure de la page, le navigateur analyse le code HTML (chaînes de texte statiques) dans un modèle de données (objets/nœuds). Le navigateur préserve la hiérarchie du code HTML en créant une arborescence de ces nœuds: le DOM. L'avantage du DOM est qu'il représente votre page en direct. Contrairement au code HTML statique que nous créons, les nœuds produits par les navigateurs contiennent des propriétés, des méthodes et, surtout, des... qui 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);

produit le balisage HTML suivant:

<body>
    <header>
    <h1>Hello DOM</h1>
    </header>
</body>

Tout cela est bien et bon. Qu'est-ce que le Shadow DOM ?

DOM... dans l'ombre

Le Shadow DOM est un DOM normal, avec deux différences: 1) comment il est créé/utilisé et 2) comment il se comporte 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 délimitée est appelée arborescence fantôme. L'élément auquel il est rattaché est son hôte fantôme. Tout ce que vous ajoutez dans les ombres devient local par rapport à l'élément hôte, y compris <style>. C'est ainsi que le Shadow DOM parvient à définir la portée du style CSS.

Créer un Shadow DOM

Une racine fantôme est un fragment de document associé à un élément "hôte". L'association d'une racine fantôme correspond à la manière dont l'élément acquiert son Shadow DOM. 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. Ceci est le Web. Nous avons le choix.

La spécification définit une liste d'éléments qui ne peuvent pas héberger une arborescence fantôme. Un élément peut figurer dans la liste pour plusieurs raisons:

  • Le navigateur héberge déjà son propre Shadow DOM interne pour l'élément (<textarea>, <input>).
  • Il n'est pas logique que l'élément héberge un Shadow DOM (<img>).

Par exemple, cela 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 génère un "composant Web".

Exemple : Un élément personnalisé associe le DOM Shadow à lui-même, en 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>
    `;
    }
    ...
});

Il y a deux choses intéressantes à ce sujet. La première est que l'élément personnalisé crée son propre Shadow DOM lorsqu'une instance de <fancy-tabs> est créée. Cette opération s'effectue dans constructor(). Ensuite, comme nous créons une racine fantôme, les règles CSS dans <style> sont limitées à <fancy-tabs>.

Composition et emplacements

La composition est l'une des fonctionnalités les moins comprises du Shadow DOM, mais elle est sans doute la plus importante.

Dans l'univers du développement Web, la composition correspond à la façon dont nous construisons des applications, de manière déclarative à partir de HTML. Différents éléments de base (<div>, <header>, <form>, <input>) se rejoignent pour former des applications. Certaines de ces balises fonctionnent même les unes avec les autres. C'est pourquoi les éléments natifs tels que <select>, <details>, <form> et <video> sont si flexibles. Chacune de ces balises accepte certains codes HTML comme éléments enfants et effectue une action spéciale avec elles. Par exemple, <select> sait comment afficher <option> et <optgroup> dans des widgets déroulants et multi-sélections. L'élément <details> affiche <summary> sous la forme d'une flèche extensible. 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. Quelle magie !

Terminologie: Light DOM et Shadow DOM

La composition Shadow DOM introduit un grand nombre de nouveaux principes fondamentaux dans le développement Web. Avant d'entrer dans les détails, normalisons une terme afin que nous parlions le même jargon.

DOM léger

Balisage écrit par un utilisateur de votre composant. Ce DOM se trouve en dehors du Shadow DOM du composant. Il s'agit des éléments 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

DOM écrit par un auteur de composant. Le Shadow DOM est local dans le composant, définit sa structure interne, son code CSS cloisonné et encapsule les détails de votre implémentation. Elle 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 du fait que le navigateur distribue le Light DOM de l'utilisateur dans votre Shadow DOM, ce qui affiche le produit final. L'arborescence aplatie correspond au résultat des outils de développement et au rendu 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ément <slot>

Shadow DOM compose différentes arborescences DOM à l'aide de l'élément <slot>. Les emplacements sont des espaces réservés à l'intérieur de 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 Shadow DOM de votre composant. L'action à effectuer consiste à dire "Afficher le balisage de l'utilisateur ici".

Les éléments sont autorisés à "traverser" la limite du Shadow DOM lorsqu'un <slot> les invite. Ces éléments sont appelés nœuds distribués. Conceptuellement, les nœuds distribués peuvent sembler un peu étranges. Les emplacements ne déplacent pas physiquement le DOM, mais le rendent à un autre emplacement à l'intérieur du Shadow DOM.

Un composant peut définir zéro, un ou plusieurs emplacements dans son Shadow DOM. Ils peuvent être vides ou fournir un contenu de remplacement. Si l'utilisateur ne fournit pas de contenu Light DOM, l'emplacement affiche le 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 pour les 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>

Et si vous vous posez la question, l'arbre aplati 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 est capable de 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é créé pour gérer différents types d'enfants, tout comme le fait <select>.

Attribuer 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) pour que les utilisateurs remplacent les valeurs par défaut.

Styles définis par les composants

La fonctionnalité la plus utile du Shadow DOM est le CSS cloisonné:

  • 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 sont pas perdus. Ils sont limités à 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 à nouveau 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 Shadow DOM. Elles sont aussi 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 à 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 y déclarez. Les composants Web peuvent également se styliser eux-mêmes à l'aide du sélecteur :host.

Exemple : Attribuer un style au composant lui-même

<style>
:host {
    display: block; /* by default, custom elements are display: inline */
    contain: content; /* CSS containment FTW. */
}
</style>

L'un des problèmes avec :host réside dans le fait 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 extérieurs gagnent. Cela permet aux utilisateurs de remplacer votre style de premier niveau depuis l'extérieur. De plus, :host ne fonctionne que dans le contexte d'une racine fantôme. 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>. Il s'agit d'un excellent moyen pour votre composant d'encapsuler les comportements qui réagissent à l'interaction de l'utilisateur ou à l'état ou au 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>

Appliquer un style en fonction du contexte

:host-context(<selector>) correspond au composant si lui-même ou l'un de ses ancêtres correspond à <selector>. Il est généralement utilisé pour la thématisation basée sur l'environnement d'un composant. Par exemple, de nombreuses personnes utilisent la thématisation en appliquant une classe à <html> ou <body>:

<body class="darktheme">
    <fancy-tabs>
    ...
    </fancy-tabs>
</body>

:host-context(.darktheme) applique un style à <fancy-tabs> s'il s'agit d'un descendant de .darktheme:

:host-context(.darktheme) {
    color: white;
    background: black;
}

:host-context() peut être utile pour la thématisation, mais une approche encore meilleure consiste à créer des hooks de style à l'aide de propriétés CSS personnalisées.

Appliquer un style aux 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 de nom:

<name-badge>
    <h2>Eric Bidelman</h2>
    <span class="title">
    Digital Jedi, <span class="company">Google</span>
    </span>
</name-badge>

Le Shadow DOM peut appliquer un style aux <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>

Souvenez-vous que les éléments <slot> ne déplacent pas le Light DOM de l'utilisateur. Lorsque les nœuds sont répartis dans un <slot>, <slot> affiche son 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 accepter d'autres styles (définis par le Shadow DOM).

Voici un autre exemple plus détaillé tiré 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 d'onglets et un emplacement pour le contenu du panneau d'onglets. Lorsque l'utilisateur sélectionne un onglet, nous mettons sa sélection en gras et affichons son panneau. Pour ce faire, sélectionnez les nœuds distribués possédant l'attribut selected. Le fichier JS de l'élément personnalisé (non illustré ici) ajoute cet attribut au bon moment.

Appliquer un style depuis l'extérieur à un composant

Il existe plusieurs façons de styliser un composant depuis 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 extérieurs prévalent toujours sur les styles définis dans le Shadow DOM. Par exemple, si l'utilisateur écrit le sélecteur fancy-tabs { width: 500px; }, il prévaut sur la règle du composant: :host { width: 650px;}.

Définir le style du composant lui-même ne vous mènera qu'à ce stade. Mais que se passe-t-il si vous souhaitez styliser les composants 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. D'un point de vue conceptuel, l'idée est semblable à <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>

À l'intérieur de 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 est #9E9E9E.

Rubriques avancées

Créer des racines fantômes fermées (à éviter)

Il existe un autre type de Shadow DOM, le mode "fermé". Lorsque vous créez une arborescence fantôme fermée, en dehors de JavaScript, vous ne pouvez pas accéder au DOM interne de votre composant. Le fonctionnement est semblable à celui des éléments natifs tels que <video>. JavaScript ne peut pas accéder au Shadow DOM de <video>, car le navigateur l'implémente à l'aide d'une racine fantôme en mode fermé.

Exemple - Créer une arborescence ombrée fermée:

const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div

Les autres API sont également affectées par le mode fermé:

  • Element.assignedSlot / TextNode.assignedSlot renvoie null
  • Event.composedPath() pour les événements associés à des éléments dans le Shadow DOM, renvoie []

Voici un résumé expliquant pourquoi vous ne devez jamais créer de composants Web avec {mode: 'closed'}:

  1. Sentiment de sécurité artificiel. Rien n'empêche un pirate informatique de pirater Element.prototype.attachShadow.

  2. Le mode fermé empêche le code de votre élément personnalisé d'accéder à son propre Shadow DOM. C'est un échec complet. À la place, vous devez stocker une référence pour la réutiliser plus tard si vous souhaitez utiliser des éléments comme querySelector(). Cela va à 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');
        }
        ...
    });
    
  3. Le mode fermé rend votre composant moins flexible pour les utilisateurs finaux. Au fur et à mesure que vous créez des composants Web, vous oubliez d'ajouter une fonctionnalité à un moment donné. Une option de configuration 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. Pouvoir accéder aux éléments internes du composant est très utile. En fin de compte, les utilisateurs dupliqueront votre composant, en trouveront un autre ou en créeront le leur s'il ne répond pas à leurs attentes :(

Utiliser des emplacements en JavaScript

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 ou supprime des enfants du Light DOM).

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 Light DOM, vous pouvez configurer un MutationObserver dans le constructeur de votre élément.

Quels éléments s'affichent dans un espace publicitaire ?

Il est parfois utile de savoir quels éléments sont associés à un emplacement. Appelez slot.assignedNodes() pour connaître 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 se présente comme suit:

<slot><b>fallback content</b></slot>
UtilisationCallRésultat
<my-component>texte du composant</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é ?

Répondre à la question inverse est également possible. element.assignedSlot vous indique les emplacements de composants auxquels votre élément est attribué.

Modèle d'événement Shadow DOM

Lorsqu'un événement remonte à partir du Shadow DOM, sa cible est ajustée pour conserver l'encapsulation fournie par le Shadow DOM. Autrement dit, les événements sont reciblés pour donner l'impression qu'ils proviennent du composant plutôt que d'éléments internes dans votre Shadow DOM. Certains événements ne se propagent même pas à partir du Shadow DOM.

Les événements qui passent la limite de l'ombre sont les suivants:

  • Événements de concentration: blur, focus, focusin, focusout
  • Événements de souris: click, dblclick, mousedown, mouseenter, mousemove, etc.
  • Événements de la roue: 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 fantôme est ouverte, l'appel de event.composedPath() renvoie un tableau des nœuds traversés par l'événement.

Utiliser des événements personnalisés

Les événements DOM personnalisés qui sont déclenchés sur des nœuds internes dans une arborescence fantôme ne dépassent pas les limites de l'ombre, sauf s'ils sont créés à 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 la sélection

Comme nous l'avons vu avec le modèle d'événement du Shadow DOM, les événements déclenchés dans le Shadow DOM sont ajustés de manière à ressembler à ceux de l'élément hôte. Par exemple, supposons que vous cliquiez sur un <input> à l'intérieur d'une racine fantôme:

<x-focus>
    #shadow-root
    <input type="text" placeholder="Input inside shadow dom">

L'événement focus semble 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 le 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 les racines fantômes de manière récursive pour trouver 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 développe le comportement de sélection des éléments dans une arborescence d'ombres:

  • Si vous cliquez sur un nœud dans le Shadow DOM et que ce nœud n'est pas une zone sélectionnable, la première zone sélectionnable est mise en surbrillance.
  • Lorsqu'un nœud à l'intérieur du Shadow DOM est sélectionné, :focus s'applique à l'hôte en plus de l'élément sélectionné.

Exemple : Comment delegatesFocus: true modifie le comportement de sélection

<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

delegatesFocus: vrai comportement.

Vous trouverez ci-dessus le résultat lorsque <x-focus> est sélectionné (clic de l'utilisateur, onglet activé, focus(), etc.). L'utilisateur clique sur le texte cliquable du composant Shadow DOM, ou le <input> interne est sélectionné (y compris autofocus).

Si vous deviez définir delegatesFocus: false, voici ce que vous verriez à la place:

delegatesFocus : &quot;false&quot; et l&#39;entrée interne est sélectionnée.
delegatesFocus: false et la <input> interne sont ciblées.
delegatesFocus : &quot;false&quot; et &quot;x-focus&quot; permet de cibler (par exemple, avec tabindex=&#39;0&#39;).
delegatesFocus: false et <x-focus> gagnent le focus (par exemple, avec tabindex="0").
delegatesFocus: false et le texte Clickable Shadow DOM est sélectionné (ou cliquez sur une autre zone vide dans le Shadow DOM de l&#39;élément).
delegatesFocus: false et "Clickable Shadow DOM text" sont sélectionnés (ou cliquez sur une autre zone vide dans le Shadow DOM de l'élément).

Conseils et astuces

Au fil des ans, j'ai appris une chose ou deux 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 ou la peinture d'un composant Web sont relativement autonomes. Utilisez le confinement CSS dans :host pour tenter d'obtenir des résultats positifs:

<style>
:host {
    display: block;
    contain: content; /* Boom. CSS containment FTW. */
}
</style>

Réinitialiser des styles hérités

Les styles hérités (background, color, font, line-height, etc.) continuent d'hériter dans Shadow DOM. Autrement dit, ils perdent la limite du Shadow DOM par défaut. Si vous souhaitez partir d'un nouvel écran, utilisez all: initial; pour rétablir la valeur initiale des styles hérités lorsqu'ils dépassent la limite de l'ombre.

<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 rechercher les éléments personnalisés utilisés sur la page. Pour ce faire, vous devez traverser de manière récursive le Shadow DOM 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 constituent un espace réservé idéal pour déclarer la structure d'un composant Web.

Consultez l'exemple de la section Éléments personnalisés: créer des composants Web réutilisables.

Historique et prise en charge du navigateur

Si vous suivez les composants Web depuis deux ans, vous savez que Chrome 35+/Opera propose une ancienne version du Shadow DOM depuis un certain temps. Blink continuera de prendre en charge les deux versions en parallèle pendant un certain temps. La spécification v0 propose une méthode différente pour créer une racine fantôme (element.createShadowRoot au lieu de la racine element.attachShadow de v1). L'appel de l'ancienne méthode continue de créer une racine fantôme avec la sémantique v0, de sorte que le code v0 existant n'est pas défaillant.

Si l'ancienne spécification v0 vous intéresse, consultez les articles html5rocks : 1, 2, 3. Vous pouvez également effectuer une comparaison efficace des différences entre la version v0 et la version v1 du Shadow DOM.

Prise en charge des navigateurs

Shadow DOM v1 est disponible dans Chrome 53 (status), Opera 40, Safari 10 et Firefox 63. Edge a commencé le développement.

Pour détecter le Shadow DOM, vérifiez l'existence de attachShadow:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Polyfill

En attendant que les navigateurs soient largement compatibles, les polyfills shadydom et shadycss vous offrent les fonctionnalités de la version 1. Shady DOM imite le champ d'application DOM des propriétés personnalisées CSS Shadow DOM et des polyfills shadycss, ainsi que le champ d'application du style fourni 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 modifier/modifier le champ d'application de vos styles.

Conclusion

Pour la première fois, nous disposons d'une primitive d'API qui effectue un champ d'application CSS et un champ d'application DOM appropriés, et dont la composition est authentique. Combiné à d'autres API de composants Web tels que des éléments personnalisés, le Shadow DOM permet de créer des composants vraiment encapsulés sans piratage ni utiliser d'anciens bagages tels que des <iframe>.

Ne vous méprenez pas. Shadow DOM est sans doute un monstre complexe ! Mais ça vaut le coup d'apprendre. Passez du temps avec elle. Apprenez-le et posez des questions !

Complément d'informations

Questions fréquentes

Puis-je utiliser Shadow DOM v1 aujourd'hui ?

Avec un polyfill, oui. Consultez la page Navigateurs compatibles.

Quelles sont les fonctionnalités de sécurité du Shadow DOM ?

Shadow DOM n'est pas une fonctionnalité de sécurité. Cet outil léger permet de définir la portée du code CSS et de masquer les arborescences DOM dans un composant. Si vous souhaitez disposer d'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 Shadow DOM vous permet de bénéficier de fonctionnalités telles que la portée CSS, l'encapsulation DOM et la composition.

Quelle est la différence entre des racines fantômes ouvertes et fermées ?

Consultez Racines fantômes fermées.