Shadow DOM v1 – Composants Web autonomes

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

Résumé

Le Shadow DOM vous permet de créer des applications Web de façon fragile. La fragilité vient de la nature globale du code HTML, CSS et JS. Au fil des années, nous avons a inventé un nombre exorbitant sur outils pour contourner les problèmes. Par exemple, lorsque vous utilisez un nouvel identifiant ou une nouvelle classe HTML, il n’est pas possible de dire s’il entrera en conflit avec un nom existant utilisé par la page. Des insectes subtils apparaissent La spécificité CSS devient un gros problème (!important toutes les choses !), le style les sélecteurs deviennent incontrôlables, vos performances peuvent en pâtir. La liste continue.

Le Shadow DOM corrige le CSS et le DOM. Il introduit des styles cloisonnés pour le Web. Google Cloud. Sans outils ni conventions d'attribution de noms, vous pouvez regrouper les ressources CSS le balisage, le masquage des détails de l'implémentation et l'écriture autonome en vanilla JavaScript.

Introduction

Le Shadow DOM est l'une des trois normes des composants Web: Modèles HTML Shadow DOM et Éléments personnalisés : Importations HTML faisaient partie de la liste, mais sont désormais considérés obsolète.

Vous n'avez pas besoin de créer des composants Web qui utilisent le Shadow DOM. Mais quand vous le faites, vous profitez de ses avantages (champ d'application CSS, encapsulation DOM, la composition) et créer des modèles éléments personnalisés, qui sont résilients, hautement configurables et extrêmement réutilisables. Si personnalisé servent à créer un nouveau code HTML (avec une API JavaScript), le Shadow DOM est dont vous fournissez le code HTML et CSS. Les deux API se combinent pour créer un composant avec du code HTML, CSS et JavaScript autonome.

Shadow DOM est un outil conçu pour 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 les nœuds dans le Shadow DOM du composant).
  • Scoped CSS: le CSS défini dans le Shadow DOM lui est limité. Règles de style ne s'échappent pas et les styles de page s'égarent.
  • Composition: concevez une API déclarative basée sur le balisage pour votre composant.
  • Simplification du CSS : grâce au DOM Scoped, vous pouvez utiliser de simples sélecteurs CSS, les noms génériques d'identifiant ou de classe, et ne vous préoccupez pas des conflits de noms.
  • Productivité : considérez les applications comme des fragments de DOM plutôt qu'une seule grande (globale).

Démonstration de fancy-tabs

Tout au long de cet article, nous ferons référence à un composant de démonstration (<fancy-tabs>). et d'y référencer des extraits de code. 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.

<ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder"> <ph type="x-smartling-placeholder">
</ph> <ph type="x-smartling-placeholder"></ph> Afficher le code source sur GitHub

Qu'est-ce que le Shadow DOM ?

Arrière-plan sur le DOM

Le HTML est au cœur du Web, car il est facile à utiliser. En déclarant quelques balises, peuvent créer une page en quelques secondes qui présente à la fois une présentation et une structure. Toutefois, HTML n'est pas si utile. Il est facile pour un être humain de comprendre un texte... mais les machines ont besoin de quelque chose de plus. Indiquez l'objet du document Model, ou DOM.

Lorsque le navigateur charge une page Web, il fait un tas de choses intéressantes. L'une des valeurs suivantes : il transforme le code HTML de l'auteur en un document en ligne. Fondamentalement, pour comprendre la structure de la page, le navigateur analyse le code HTML (statique chaînes de texte) dans un modèle de données (objets/nœuds). Le navigateur conserve la la hiérarchie HTML en créant une arborescence de ces nœuds: le DOM. Ce qui est génial est qu'il s'agit d'une représentation en direct de votre page. Contrairement à l'architecture les nœuds créés par le navigateur contiennent les propriétés, les méthodes et les peuvent être manipulés par des programmes ! C'est pourquoi nous pouvons créer des objets 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 et bien. Ensuite, Qu'est-ce que le Shadow DOM ?

DOM... dans l'ombre

Shadow DOM est un DOM normal, à deux différences près: 1) comment il est créé/utilisé et 2) son comportement par rapport au reste de la page. Généralement, vous créez et les ajouter en tant qu'enfants d'un autre élément. Avec le Shadow DOM, créer une arborescence DOM délimitée qui est associée à l'élément, mais séparée de son des 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 l'ombre devient local à l'élément d'hébergement, y compris <style>. Voici comment le Shadow DOM atteint la portée du style CSS.

Création du Shadow DOM...

Une racine fantôme est un fragment de document qui est attaché à un élément "hôte". Le fait d'associer une racine fantôme est la façon dont l'élément acquiert son Shadow DOM. À créez 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 de l'ombre, mais vous pouvez aussi utiliser d'autres DOM API. Voici 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. Il existe plusieurs raisons pour lesquelles un élément peut dans la liste:

  • 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 pour créer éléments personnalisés. Utiliser le Shadow DOM pour compartimenter le code HTML, CSS et JS d'un élément produisant un "composant Web".

Exemple : un élément personnalisé s'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>
    `;
    }
    ...
});

Il y a un certain nombre de choses intéressantes qui se passent ici. Premièrement, L'élément personnalisé crée son propre Shadow DOM lorsqu'une instance de <fancy-tabs> est créé. Cette opération est effectuée dans constructor(). Deuxièmement, parce que nous créons une racine fantôme, les règles CSS dans <style> seront 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 notre monde du développement Web, la composition est la manière de créer des applications, de manière déclarative en HTML. Différents éléments de base (<div>, <header>, les <form> et les <input>) se rejoignent pour former des applications. Certaines de ces balises fonctionnent même les uns avec les autres. C'est pour cela que les éléments natifs comme <select>, <details>, <form> et <video> sont si flexibles. Chacune de ces balises accepte un certain code HTML comme enfants et fait quelque chose de spécial avec eux. Par exemple : <select> sait comment afficher <option> et <optgroup> dans un menu déroulant et avec la sélection multiple. L'élément <details> affiche <summary> en tant que flèche extensible. Même <video> sait comment gérer certains enfants: Les éléments <source> ne sont pas affichés, mais ils ont une incidence sur le comportement de la vidéo. Quelle magie !

Terminologie: Light DOM et Shadow DOM

La composition Shadow DOM introduit de nombreux nouveaux principes de base pour le Web développement d'applications. Avant d'entrer dans les détails, prenons la terminologie, nous parlons donc le même jargon.

DOM léger

Balisage écrit par un utilisateur de votre composant. Ce DOM se trouve en dehors de 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

DOM écrit par l'auteur d'un composant. Le Shadow DOM est local dans le composant. définit sa structure interne, son champ d'application CSS et encapsule votre implémentation plus de détails. Il peut également définir comment afficher le balisage créé par le consommateur. de votre composant.

#shadow-root
    <style>...</style>
    <slot name="icon"></slot>
    <span id="wrapper">
    <slot>Button</slot>
    </span>

Arborescence DOM aplatie

Résultat du navigateur qui distribue le Light DOM de l'utilisateur dans votre ombre DOM, qui affiche le produit final. L'arbre aplati est ce que vous voyez au final dans les outils de développement et ce qui s'affiche 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>

La balise <slot> élément

Le DOM Shadow compose différentes arborescences DOM ensemble à 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 votre propre balisage. En définissant un ou plusieurs espaces, vous invitez un balisage externe à afficher dans le Shadow DOM de votre composant. En gros, vous dites "Afficher l'image de l'utilisateur du code source.

Les éléments sont autorisés à croiser limite du Shadow DOM lorsqu'une <slot> les utiliser. 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. cette à 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 du contenu de remplacement. Si l'utilisateur ne fournit pas de Light DOM l'espace publicitaire 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 Shadow DOM que les utilisateurs référencent par leur nom.

Exemple – Emplacements du 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'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 peut gérer différentes configurations, l'arborescence DOM aplatie reste la même. Nous pouvons aussi passer de <button> à <h2> Ce composant a été conçu pour gérer différents types d'enfants... comme le fait <select>.

Attribuer un style

Il existe de nombreuses options pour appliquer un style aux composants Web. Composant qui utilise une ombre Le DOM peut être stylisé par la page principale, définir ses propres styles ou fournir des hooks (dans sous la forme de propriétés CSS personnalisées) pour que les utilisateurs puissent remplacer les valeurs par défaut.

Styles définis par le composant

La fonctionnalité la plus utile du Shadow DOM est le CSS dé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'effacent pas. Ils sont limités à l'élément hôte.

Les sélecteurs CSS utilisés dans le Shadow DOM s'appliquent localement à votre composant. Dans Cela signifie que nous pouvons à nouveau utiliser des noms d'ID/de classe courants, sans nous soucier sur les conflits ailleurs sur la page. Utiliser des sélecteurs CSS plus simples est une bonne pratique dans 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 multi-sélection (au lieu de une liste déroulante) 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 adopter un style différent selon les attributs que vous à déclarer. Les composants Web peuvent également s'appliquer un style à l'aide de :host sélecteur.

Exemple : le style d'un composant lui-même

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

:host présente un inconvénient : les règles de la page parente présentent une spécificité plus élevée. que les règles :host définies dans l'élément. Autrement dit, les styles extérieurs l'emportent. Ce permet aux utilisateurs de remplacer votre style de premier niveau depuis l'extérieur. En outre, :host ne fonctionne que dans le contexte d'une racine fantôme. Vous ne pouvez donc pas l'utiliser en dehors de 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 qui réagissent aux interactions des utilisateurs, ou en fonction de l'état ou du style des nœuds internes sur 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 s'il ou l'un de ses ancêtres correspond à <selector>. Une utilisation courante de cela est la thématisation basée sur l'expérience d'un composant environnement. Par exemple, de nombreuses personnes créent un thème en appliquant une classe à <html> ou <body>:

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

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

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

:host-context() peut être utile pour la thématisation, mais une meilleure approche 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 du composant 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>

Comme nous l'avons vu précédemment, les <slot> ne déplacent pas le Light DOM de l'utilisateur. Quand ? sont répartis dans un <slot>, le <slot> affiche son DOM, mais le les nœuds restent physiquement en place. Les styles appliqués avant la distribution continuent s'appliquent après distribution. Toutefois, lorsque le Light DOM est distribué, il peut utilisent des styles supplémentaires (définis par le Shadow DOM).

Autre exemple plus approfondi issu 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>
`;

Cet exemple présente deux emplacements: un emplacement nommé pour les titres d'onglets et un pour le contenu du panneau d'onglets. Lorsque l'utilisateur sélectionne un onglet, nous mettons sa sélection en gras. et afficher son panneau. Pour cela, vous devez sélectionner des nœuds distribués selected. Le code JS de l'élément personnalisé (non présenté ici) ajoute au bon moment.

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

Il existe plusieurs façons de styliser un composant de l'extérieur. 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 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 la règle du composant: :host { width: 650px;}.

Appliquer un style au composant lui-même ne vous mènera qu'à loin. Mais que se passe-t-il si vous vous voulez appliquer un style aux composants internes d'un composant ? Pour cela, nous avons besoin de CSS propriétés.

Créer des hooks de style à l'aide de propriétés CSS personnalisées

Les utilisateurs peuvent ajuster 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" à remplacer par les utilisateurs.

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 est #9E9E9E.

Rubriques avancées

Création de racines fantômes fermées (à éviter)

Il existe un autre type de Shadow DOM appelé "closed" (fermé) . Lorsque vous créez un arborescence fantôme fermée. En dehors de JavaScript, vous ne pourrez 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éation d'une arborescence fantôme fermée:

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 affectées par le mode fermé:

  • Element.assignedSlot / TextNode.assignedSlot renvoie null
  • Event.composedPath() pour les événements associés aux éléments situés à l'intérieur de l'ombre DOM, renvoie [].

Voici un résumé des raisons pour lesquelles vous ne devriez jamais créer de composants Web avec {mode: 'closed'}:

  1. Sens artificiel de la sécurité. Rien n’empêche un attaquant de piratage de 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. Au lieu de cela, vous devez conserver une référence pour plus tard si vous souhaitez utiliser des éléments comme querySelector(). C’est complètement annule 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. En créer des composants Web, vous oublierez peut-être d'ajouter . Option de configuration. Un cas d'utilisation souhaité par l'utilisateur. Une approche vous pouvez par exemple oublier d'inclure des hooks de style adéquats pour les nœuds internes. En mode fermé, les utilisateurs n'ont aucun moyen d'ignorer les valeurs par défaut et d'ajuster . Être capable d'accéder aux composants internes du composant est très utile. En fin de compte, les utilisateurs dupliqueront votre composant, en trouveront un autre ou créeront leur si elle ne fait pas ce qu'elle veut :(

Utiliser les emplacements dans JS

L'API Shadow DOM fournit des utilitaires permettant d'utiliser des emplacements et des fichiers nœuds. 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 sont modifiés. Pour (par exemple, si l'utilisateur ajoute ou supprime des enfants dans le 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. Appeler slot.assignedNodes() pour identifier les éléments affichés par l'espace publicitaire. La L'option {flatten: true} renvoie également le contenu de remplacement d'un espace (s'il n'existe aucun nœud sont distribués).

Par exemple, imaginons que votre Shadow DOM se présente comme suit:

<slot><b>fallback content</b></slot>
UtilisationAppelerRésultat
<my-component>texte du composant</my-component> slot.assignedNodes(); [component text]
&lt;my-component&gt;&lt;/my-component&gt; slot.assignedNodes(); []
&lt;my-component&gt;&lt;/my-component&gt; 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 raconte vous indique à quel composant place 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 maintenir fournie par le Shadow DOM. Autrement dit, les événements sont reciblés comme si elles provenaient du composant plutôt que d'éléments internes à l'intérieur de votre Shadow DOM Certains événements ne se propagent même pas depuis le Shadow DOM.

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

  • Événements de focus: blur, focus, focusin, focusout
  • Événements de souris: click, dblclick, mousedown, mouseenter, mousemove, etc.
  • Événements associés à la roue: wheel
  • Événements d'entrée: beforeinput, input
  • Événements du 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 de nœuds par lesquels l'événement a traversé.

Utiliser des événements personnalisés

Les événements DOM personnalisés qui sont déclenchés sur les nœuds internes d'une arborescence fantôme ne hors de la limite de l'ombre, sauf si l'événement est créé à l'aide de la méthode 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 (valeur par défaut), les clients ne pourront pas écouter l'événement. en dehors de la racine de votre ombre.

<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 dans le modèle d'événement du Shadow DOM, les événements déclenchés dans le Shadow DOM sont ajustés pour sembler provenir de l'élément d'hébergement. Par exemple, imaginons que vous cliquiez sur un élément <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 devrait provenir de <x-focus>, et non de <input>. De même, document.activeElement sera <x-focus>. Si la racine fantôme a été créé avec mode:'open' (voir mode fermé), vous serez également en mesure d'accéder au nœud interne qui a été sélectionné:

document.activeElement.shadowRoot.activeElement // only works with open mode.

S'il existe plusieurs niveaux de Shadow DOM en jeu (par exemple, un élément personnalisé dans un autre élément personnalisé), vous devez explorer les racines de l'ombre de manière récursive recherchez 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 la comportement de focus des éléments dans une arborescence d'ombre:

  • Si vous cliquez sur un nœud dans le Shadow DOM et que le nœud n'est pas une zone sélectionnable, la première zone sélectionnable devient active.
  • Lorsqu'un nœud dans le Shadow DOM est sélectionné, :focus s'applique à l'hôte dans à 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: comportement réel.

Le résultat ci-dessus apparaît lorsque <x-focus> est sélectionné (clic de l'utilisateur, onglets, focus(), etc.), "Texte Shadow DOM cliquable" ou l'adresse e-mail interne <input> est sélectionné (y compris autofocus).

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

<ph type="x-smartling-placeholder">
</ph> delegatesFocus: false et l&#39;entrée interne est ciblée. <ph type="x-smartling-placeholder">
</ph> delegatesFocus: false et le <input> interne est sélectionné.
<ph type="x-smartling-placeholder">
</ph> delegatesFocus: faux et x-focus
    peut être sélectionné (c&#39;est-à-dire si la valeur tabindex est définie sur &quot;0&quot;). <ph type="x-smartling-placeholder">
</ph> delegatesFocus: false et <x-focus> permet de cibler les utilisateurs (tabindex="0", par exemple).
<ph type="x-smartling-placeholder">
</ph> delegatesFocus: faux et &quot;Clickable Shadow DOM text&quot; correspond à
    (ou toute autre zone vide dans le Shadow DOM de l&#39;élément). <ph type="x-smartling-placeholder">
</ph> delegatesFocus: false et "texte Shadow DOM cliquable" correspond à (ou toute 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 pensez 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 la peinture d'un composant Web sont assez autonomes. Utilisez Blocage CSS dans :host pour un perf victoire:

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

Réinitialiser des styles hérités

Les styles héritables (background, color, font, line-height, etc.) continuent à hériter dans le Shadow DOM. Autrement dit, ils traversent la limite du Shadow DOM par défaut. Si vous voulez recommencer avec un nouvel écran, utilisez all: initial; pour le réinitialiser leur valeur initiale 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 trouver les éléments personnalisés utilisés sur la page. Pour ce faire, vous devez devez balayer récursivement 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 renseigner une racine fantôme à l'aide de .innerHTML, nous pouvons utiliser une méthode <template> Les modèles sont un espace réservé idéal pour déclarer la structure un composant Web.

Consultez l'exemple dans "Éléments personnalisés: créer des composants Web réutilisables".

Histoire et compatibilité avec les navigateurs

Si vous suivez les composants Web depuis les deux dernières années, vous sachez que Chrome 35 ou version ultérieure/Opera expédie une ancienne version du Shadow DOM pour un certain temps. Blink continuera de prendre en charge les deux versions en parallèle pour certaines en temps réel. La spécification v0 proposait une méthode différente pour créer une racine fantôme (element.createShadowRoot au lieu de element.attachShadow dans la version 1). En appelant la méthode l'ancienne méthode continue de créer une racine fantôme avec la sémantique v0. La version v0 existante le code est toujours prêt.

Si l'ancienne spécification V0 vous intéresse, consultez les html5rocks articles: 1, 2 3. Vous pouvez aussi comparer différences entre les versions 0 et 1 du Shadow DOM.

Prise en charge des navigateurs

Shadow DOM v1 est disponible dans Chrome 53 (état), Opera 40, Safari 10 et Firefox 63. Périphérie 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 pris en charge, le shadydom et Les polyfills shadycss vous donnent la version 1 . Le DOM ombré imite le champ d'application DOM du Shadow DOM et des polyfills shadycss. Propriétés CSS personnalisées et 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 obtenir des instructions sur la façon d'ajuster la portée de vos styles.

Conclusion

Pour la première fois, nous disposons d'une primitive d'API qui effectue un cadrage CSS appropriés, le champ d'application DOM, et utilise une composition réelle. Combiné à d'autres API de composants Web comme les éléments personnalisés, le Shadow DOM permet de créer composants sans piratage ni utilisant d'anciens bagages comme des <iframe>.

Ne vous méprenez pas. Shadow DOM est certainement un monstre complexe ! Mais c'est une bête. digne d'intérêt d'apprendre. Passez du temps avec elle. Apprenez-en plus et posez des questions !

Documentation complémentaire

Questions fréquentes

Puis-je utiliser Shadow DOM v1 aujourd'hui ?

Avec un polyfill, oui. Consultez Compatibilité des navigateurs.

Quelles sont les fonctionnalités de sécurité fournies par le Shadow DOM ?

Le Shadow DOM n'est pas une fonctionnalité de sécurité. Cet outil léger permet de définir la portée des CSS et masquer les arborescences DOM dans le composant. Si vous voulez 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, Lorsque vous créez des éléments personnalisés qui utilisent le Shadow DOM, vous pouvez des fonctionnalités telles que la portée CSS, l'encapsulation DOM et la composition.

Quelle est la différence entre une racine fantôme ouverte et fermée ?

Consultez la section Rracines de l'ombre fermées.