Travailler avec Shadow Dom

C’est courant de voir Composants Web directement par rapport aux composants du cadre. Mais la plupart des exemples sont en fait spécifiques aux éléments personnalisés, qui est une pièce de l’image des composants Web. Il est facile d’oublier que les composants Web sont en fait un ensemble d’API de plate-forme Web individuelle qui peuvent être utilisés seuls:
En d’autres termes, il est possible de créer un Élément personnalisé sans utiliser Shadow dom ou Modèles HTMLmais la combinaison de ces fonctionnalités ouvre une stabilité, une réutilisabilité, une maintenabilité et une sécurité améliorées. Ce sont toutes des parties du même ensemble de fonctionnalités qui peuvent être utilisées séparément ou ensemble.
Cela étant dit, je veux accorder une attention particulière Shadow dom et où il s’intègre dans cette image. Travailler avec Shadow Dom nous permet de définir des limites claires entre les différentes parties de nos applications Web – encapsulant HTML et CSS connexes à l’intérieur d’un DocumentFragment
Pour isoler les composants, prévenir les conflits et maintenir une séparation propre des préoccupations.
La façon dont vous profitez de cette encapsulation implique des compromis et une variété d’approches. Dans cet article, nous explorerons ces nuances en profondeur, et dans une pièce de suivi, nous plongerons sur la façon de travailler efficacement avec des styles encapsulés.
Pourquoi Shadow Dom existe
La plupart des applications Web modernes sont construites à partir d’un assortiment de bibliothèques et de composants de divers fournisseurs. Avec le DOM traditionnel (ou «léger»), il est facile pour les styles et les scripts de fuir ou de se lancer les uns avec les autres. Si vous utilisez un cadre, vous pourriez être en mesure de croire que tout a été écrit pour fonctionner de manière transparente ensemble, mais l’effort doit toujours être fait pour s’assurer que tous les éléments ont un ID unique et que les règles CSS sont élaborées aussi spécifiquement que possible. Cela peut conduire à un code trop verbeux qui augmente à la fois le temps de chargement de l’application et réduit la maintenabilité.
<!-- div soup -->
<div id="my-custom-app-framework-landingpage-header" class="my-custom-app-framework-foo">
<div><div><div><div><div><div>etc...</div></div></div></div></div></div>
</div>
Shadow Dom a été introduit pour résoudre ces problèmes en fournissant un moyen d’isoler chaque composant. Le <video>
et <details>
Les éléments sont de bons exemples d’éléments HTML natifs qui utilisent des Shadow Dom en interne par défaut pour empêcher les interférences des styles ou des scripts globaux. Exploiter ce pouvoir caché qui entraîne des composants de navigateur natifs qui distinguent vraiment les composants Web de leurs homologues de framework.

<details>
ombre de l’élément dans Devtools. (Grand aperçu)Éléments qui peuvent héberger une racine d’ombre
Le plus souvent, vous verrez des racines d’ombre associées aux éléments personnalisés. Cependant, ils peuvent également être utilisés avec n’importe quel HTMLUnknownElement
et de nombreux éléments standard Soutenez-les également, notamment:
<aside>
<blockquote>
<body>
<div><footer>
<h1>
à<h6>
<header>
<main>
<nav>
<p>
<section>
<span>
Chaque élément ne peut avoir qu’une seule racine d’ombre. Certains éléments, y compris <input>
et <select>
ont déjà une racine d’ombre intégrée qui n’est pas accessible grâce à des scripts. Vous pouvez les inspecter avec vos outils de développeur en permettant Afficher l’agent utilisateur Shadow Dom Réglage, qui est «désactivé» par défaut.


Créer une racine d’ombre
Avant de tirer parti des avantages de Shadow Dom, vous devez d’abord établir un racine d’ombre sur un élément. Cela peut être instancié impératif ou de manière déclarative.
Instanciation impérative
Pour créer une racine d’ombre à l’aide de JavaScript, utilisez attachShadow({ mode })
sur un élément. Le mode
peut être open
(Permettre l’accès via element.shadowRoot
) ou closed
(cacher la racine de l’ombre des scripts extérieurs).
const host = document.createElement('div');
const shadow = host.attachShadow({ mode: 'open' });
shadow.innerHTML = '<p>Hello from the Shadow DOM!</p>';
document.body.appendChild(host);
Dans cet exemple, nous avons établi un open
Root de l’ombre. Cela signifie que le contenu de l’élément est accessible de l’extérieur, et nous pouvons l’interroger comme tout autre nœud DOM:
host.shadowRoot.querySelector('p'); // selects the paragraph element
Si nous voulons empêcher les scripts externes d’accéder entièrement à notre structure interne, nous pouvons définir le mode sur closed
plutôt. Cela provoque l’élément shadowRoot
propriété à retourner null
. Nous pouvons toujours y accéder à partir de notre shadow
référence dans la portée où nous l’avons créée.
shadow.querySelector('p');
Il s’agit d’une fonctionnalité de sécurité cruciale. Avec un closed
Shadow Root, nous pouvons être convaincus que les acteurs malveillants ne peuvent pas extraire des données utilisateur privées de nos composants. Par exemple, considérons un widget qui montre les informations bancaires. Peut-être qu’il contient le numéro de compte de l’utilisateur. Avec un open
Root d’ombre, tout script sur la page peut percer dans notre composant et analyser son contenu. Dans closed
Mode, seul l’utilisateur peut effectuer ce type d’action avec une copie manuelle ou en inspectant l’élément.
Je suggère un approche close d’abord Lorsque vous travaillez avec Shadow Dom. Prendre l’habitude d’utiliser closed
Mode à moins que vous ne déboguez, ou seulement lorsqu’il est absolument nécessaire pour contourner une limitation du monde réel qui ne peut être évitée. Si vous suivez cette approche, vous constaterez que les cas où open
Le mode est réellement requis, sont rares.
Instanciation déclarative
Nous n’avons pas à utiliser JavaScript pour profiter de Shadow Dom. L’enregistrement d’une racine d’ombre peut être fait de manière déclarative. Nicher un <template>
avec un shadowrootmode
L’attribut à l’intérieur de tout élément pris en charge amènera le navigateur à mettre à niveau automatiquement cet élément avec une racine d’ombre. La connexion d’une racine d’ombre de cette manière peut même être effectuée avec JavaScript désactivée.
<my-widget>
<template shadowrootmode="closed">
<p> Declarative Shadow DOM content </p>
</template>
</my-widget>
Encore une fois, cela peut être soit open
ou closed
. Considérez les implications de sécurité avant d’utiliser open
mode, mais notez que vous ne pouvez pas accéder au closed
Mode Contenu via tous les scripts à moins que cette méthode ne soit utilisée avec un inscrit Élément personnalisé, auquel cas, vous pouvez utiliser ElementInternals
Pour accéder à la racine de l’ombre attachée automatiquement:
class MyWidget extends HTMLElement {
#internals;
#shadowRoot;
constructor() {
super();
this.#internals = this.attachInternals();
this.#shadowRoot = this.#internals.shadowRoot;
}
connectedCallback() {
const p = this.#shadowRoot.querySelector('p')
console.log(p.textContent); // this works
}
};
customElements.define('my-widget', MyWidget);
export { MyWidget };
Configuration de l’ombre DOM
Il y a trois autres options en plus mode que nous pouvons passer à Element.attachShadow()
.
Option 1: clonable:true
Jusqu’à récemment, si un élément standard avait une racine d’ombre attachée et que vous avez essayé de la cloner en utilisant Node.cloneNode(true)
ou document.importNode(node,true)
vous n’obtiendrez qu’une copie peu profonde de l’élément hôte sans le contenu root de l’ombre. Les exemples que nous venons de regarder retourneraient en fait un vide <div>
. Cela n’a jamais été un problème avec des éléments personnalisés qui ont construit leur propre racine d’ombre en interne.
Mais pour un Decarative Shadow Dom, cela signifie que chaque élément a besoin de son propre modèle, et ils ne peuvent pas être réutilisés. Avec cette fonction nouvellement ajoutée, nous pouvons cloner sélectivement les composants lorsqu’il est souhaitable:
<div id="original">
<template shadowrootmode="closed" shadowrootclonable>
<p> This is a test </p>
</template>
</div>
<script>
const original = document.getElementById('original');
const copy = original.cloneNode(true); copy.id = 'copy';
document.body.append(copy); // includes the shadow root content
</script>
Option 2: serializable:true
L’activation de cette option vous permet d’enregistrer une représentation de chaîne du contenu à l’intérieur de la racine de l’ombre d’un élément. Appel Element.getHTML()
sur un élément hôte renverra une copie de modèle de l’état actuel de Shadow Dom, y compris toutes les instances imbriquées de shadowrootserializable
. Cela peut être utilisé pour injecter une copie de votre racine d’ombre dans un autre hôte, ou le mettre en cache pour une utilisation ultérieure.
Dans Chrome, c’est en fait Fonctionne à travers une racine d’ombre ferméealors faites attention à la fuite accidentelle des données utilisateur avec cette fonctionnalité. Une alternative plus sûre serait d’utiliser un closed
Emballage pour protéger le contenu intérieur des influences externes tout en gardant les choses open
intérieurement:
<wrapper-element></wrapper-element>
<script>
class WrapperElement extends HTMLElement {
#shadow;
constructor() {
super();
this.#shadow = this.attachShadow({ mode:'closed' });
this.#shadow.setHTMLUnsafe(`
<nested-element>
<template shadowrootmode="open" shadowrootserializable>
<div id="test">
<template shadowrootmode="open" shadowrootserializable>
<p> Deep Shadow DOM Content </p>
</template>
</div>
</template>
</nested-element>
`);
this.cloneContent();
}
cloneContent() {
const nested = this.#shadow.querySelector('nested-element');
const snapshot = nested.getHTML({ serializableShadowRoots: true });
const temp = document.createElement('div');
temp.setHTMLUnsafe(`<another-element>${snapshot}</another-element>`);
const copy = temp.querySelector('another-element');
copy.shadowRoot.querySelector('#test').shadowRoot.querySelector('p').textContent="Changed Content!";
this.#shadow.append(copy);
}
}
customElements.define('wrapper-element', WrapperElement);
const wrapper = document.querySelector('wrapper-element');
const test = wrapper.getHTML({ serializableShadowRoots: true });
console.log(test); // empty string due to closed shadow root
</script>
Avis setHTMLUnsafe()
. C’est là parce que le contenu contient <template>
Éléments. Cette méthode doit être appelée lors de l’injection de confiance Contenu de cette nature. Insertion du modèle en utilisant innerHTML
ne déclencherait pas l’initialisation automatique dans une racine d’ombre.
Option 3: delegatesFocus:true
Cette option fait essentiellement notre élément hôte comme un <label>
pour son contenu interne. Lorsqu’il est activé, cliquez n’importe où sur l’hôte ou appelez .focus()
Il se déplacera le curseur vers le premier élément focalisable de la racine de l’ombre. Cela appliquera également le :focus
Pseudo-classe à l’hôte, ce qui est particulièrement utile lors de la création de composants destinés à participer aux formulaires.
<custom-input>
<template shadowrootmode="closed" shadowrootdelegatesfocus>
<fieldset>
<legend> Custom Input </legend>
<p> Click anywhere on this element to focus the input </p>
<input type="text" placeholder="Enter some text...">
</fieldset>
</template>
</custom-input>
Cet exemple ne démontre que la délégation de focus. L’une des bizarreries de l’encapsulation est que les soumissions de formulaire ne sont pas automatiquement connectées. Cela signifie que la valeur d’une entrée ne sera pas dans la soumission de formulaire par défaut. La validation du formulaire et les états ne sont pas non plus communiqués à partir de l’ombre DOM. Il existe des problèmes de connectivité similaires avec l’accessibilité, où la limite de la racine de l’ombre peut interférer avec Aria. Ce sont toutes des considérations spécifiques aux formulaires avec lesquels nous pouvons aborder ElementInternals
qui est un sujet pour un autre article, et est de vous demander si vous pouvez plutôt compter sur un formulaire Light Dom.
Contenu à fentes
Jusqu’à présent, nous n’avons examiné que des composants entièrement encapsulés. Une fonctionnalité de Dom clé clé utilise machines à sous Pour injecter sélectivement le contenu dans la structure interne du composant. Chaque racine d’ombre peut en avoir une défaut (anonyme) <slot>
; Tous les autres doivent être nommé. Nommer un emplacement nous permet de fournir du contenu pour remplir des parties spécifiques de notre composant ainsi que du contenu de secours pour remplir les emplacements omis par l’utilisateur:
<my-widget>
<template shadowrootmode="closed">
<h2><slot name="title"><span>Fallback Title</span></slot></h2>
<slot name="description"><p>A placeholder description.</p></slot>
<ol><slot></slot></ol>
</template>
<span slot="title"> A Slotted Title</span>
<p slot="description">An example of using slots to fill parts of a component.</p>
<li>Foo</li>
<li>Bar</li>
<li>Baz</li>
</my-widget>
Les emplacements par défaut prennent également en charge le contenu de secours, mais tous les nœuds de texte parasites les rempliront. En conséquence, cela ne fonctionne que si vous effondrez tous les espaces blancs dans le balisage de l’élément hôte:
<my-widget><template shadowrootmode="closed">
<slot><span>Fallback Content</span></slot>
</template></my-widget>
Les éléments de l’emplacement émettent slotchange
événements où leur assignedNodes()
sont ajoutés ou supprimés. Ces événements ne contiennent pas de référence à la fente ou aux nœuds, vous devrez donc les transmettre dans votre gestionnaire d’événements:
class SlottedWidget extends HTMLElement {
#internals;
#shadow;
constructor() {
super();
this.#internals = this.attachInternals();
this.#shadow = this.#internals.shadowRoot;
this.configureSlots();
}
configureSlots() {
const slots = this.#shadow.querySelectorAll('slot');
console.log({ slots });
slots.forEach(slot => {
slot.addEventListener('slotchange', () => {
console.log({
changedSlot: slot.name || 'default',
assignedNodes: slot.assignedNodes()
});
});
});
}
}
customElements.define('slotted-widget', SlottedWidget);
Plusieurs éléments peuvent être attribués à un seul emplacement, soit de manière déclarative avec le slot
attribut ou par script:
const widget = document.querySelector('slotted-widget');
const added = document.createElement('p');
added.textContent="A secondary paragraph added using a named slot.";
added.slot="description";
widget.append(added);
Notez que le paragraphe de cet exemple est ajouté à la hôte élément. Le contenu à fente appartient en fait au Dom «léger», pas au domaine de l’ombre. Contrairement aux exemples que nous avons couverts jusqu’à présent, ces éléments peuvent être interrogés directement à partir du document
objet:
const widgetTitle = document.querySelector('my-widget [slot=title]');
widgetTitle.textContent="A Different Title";
Si vous souhaitez accéder à ces éléments en interne à partir de votre définition de classe, utilisez this.children
ou this.querySelector
. Seul le <slot>
Les éléments eux-mêmes peuvent être interrogés à travers l’ombre DOM, pas leur contenu.
Du mystère à la maîtrise
Maintenant tu sais pourquoi Vous voudriez utiliser Shadow Dom, quand vous devriez l’intégrer dans votre travail, et comment Vous pouvez l’utiliser dès maintenant.
Mais votre voyage de composants Web ne peut pas se terminer ici. Nous n’avons couvert que le balisage et les scripts dans cet article. Nous n’avons même pas abordé un autre aspect majeur des composants Web: Encapsulation de style. Ce sera notre sujet dans un autre article.

(GG, YK)
Source link