Shadow DOM 101

Dominic Cooney
Dominic Cooney

Introduction

Les composants Web sont un ensemble de normes de pointe qui:

  1. Permettre la création de widgets
  2. ... qui peuvent être réutilisés de manière fiable
  3. qui n'affectera pas les pages si la version suivante du composant modifie les détails de l'implémentation interne.

Cela signifie-t-il que vous devez décider quand utiliser HTML/JavaScript et les composants Web ? Non ! HTML et JavaScript peuvent créer des éléments visuels interactifs. Les widgets sont des éléments visuels interactifs. Il est judicieux de mettre à profit vos compétences en HTML et JavaScript lors du développement d'un widget. Les normes Web Components sont conçues pour vous aider dans cette tâche.

Toutefois, il existe un problème fondamental qui rend difficiles l'utilisation des widgets créés à partir de code HTML et JavaScript: l'arborescence DOM à l'intérieur d'un widget n'est pas encapsulée du reste de la page. Ce manque d'encapsulation signifie que la feuille de style de votre document peut s'appliquer accidentellement à des parties du widget, que votre code JavaScript peut modifier accidentellement des parties à l'intérieur du widget, que vos ID peuvent chevaucher les ID qu'il contient, et ainsi de suite.

Les composants Web se composent de trois parties:

  1. Modèles
  2. Shadow DOM
  3. Éléments personnalisés

Shadow DOM résout le problème d'encapsulation de l'arborescence DOM. Les quatre parties des composants Web sont conçues pour fonctionner ensemble, mais vous pouvez également choisir celles des composants Web à utiliser. Ce tutoriel vous explique comment utiliser Shadow DOM.

Bonjour, Le Monde des Ombres

Avec Shadow DOM, les éléments peuvent être associés à un nouveau type de nœud. Ce nouveau type de nœud s'appelle une racine fantôme. Un élément auquel une racine fantôme est associée est appelé hôte fantôme. Le contenu d'un hôte fantôme n'est pas affiché, mais le contenu de la racine fantôme.

Par exemple, si votre balisage se présente comme suit:

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

alors au lieu de

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

votre page ressemble

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

En outre, si JavaScript sur la page demande quel est l'textContent du bouton, il ne sera pas mis en forme : こんちテ罱の世界!", mais à "Hello, world!", car la sous-arborescence DOM sous la racine fantôme est encapsulée.

Séparer le contenu d'une présentation

Voyons maintenant comment utiliser Shadow DOM pour séparer le contenu d'une présentation. Imaginons que nous ayons le tag de nom suivant:

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Voici le balisage. Voici ce que vous allez écrire aujourd'hui. Il n'utilise pas Shadow DOM:

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Comme l'arborescence DOM n'est pas encapsulée, la structure entière de la balise de nom est exposée au document. Si d'autres éléments de la page utilisent accidentellement les mêmes noms de classe pour le style ou l'écriture de script, cela va entraîner un mauvais moment.

On peut éviter de passer un mauvais moment.

Étape 1: Masquer les détails de la présentation

D'un point de vue sémantique, nous nous soucions probablement seulement des points suivants:

  • Il s'agit d'un tag de nom.
  • Le nom est "Bob".

Tout d'abord, nous écrivons un balisage plus proche de la véritable sémantique souhaitée:

<div id="nameTag">Bob</div>

Nous plaçons ensuite tous les styles et tags div utilisés pour la présentation dans un élément <template>:

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

À ce stade, "Bob" est la seule chose affichée. Étant donné que nous avons déplacé les éléments DOM de présentation dans un élément <template>, ils ne sont pas affichés, mais ils sont accessibles à partir de JavaScript. C'est ce que nous faisons maintenant pour remplir la racine fantôme:

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

Maintenant que nous avons configuré une racine fantôme, le tag de nom est à nouveau affiché. Si vous effectuez un clic droit sur le tag de nom et que vous inspectez l'élément, vous constatez qu'il s'agit d'un balisage sémantique adapté:

<div id="nameTag">Bob</div>

Cela démontre qu'en utilisant Shadow DOM, nous avons masqué les détails de la présentation du tag de nom dans le document. Les détails de la présentation sont encapsulés dans Shadow DOM.

Étape 2: Séparer le contenu de la présentation

Notre tag de nom masque désormais les détails de la présentation sur la page, mais cela ne sépare pas en réalité la présentation du contenu. En effet, bien que le contenu (le nom "Bob") figure sur la page, le nom affiché est celui que nous avons copié dans la racine fantôme. Si nous souhaitons modifier le nom du tag, nous devons le faire à deux endroits, ce qui pourrait entraîner une désynchronisation des données.

Les éléments HTML sont de type composition : vous pouvez placer un bouton dans une table, par exemple. Nous avons besoin ici d'une composition: le tag de nom doit être composé de l'arrière-plan rouge, du texte "Hi!" et du contenu du tag.

En tant qu'auteur du composant, vous définissez le fonctionnement de la composition avec votre widget à l'aide d'un nouvel élément appelé <content>. Cela crée un point d'insertion dans la présentation du widget, lequel sélectionne le contenu de l'hôte fantôme à afficher à ce stade.

Si nous remplaçons le balisage dans Shadow DOM comme suit:

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

Lorsque le tag de nom est affiché, le contenu de l'hôte fantôme est projeté à l'endroit où l'élément <content> apparaît.

La structure du document est désormais plus simple, car le nom ne se trouve qu'à un seul endroit : le document. Si votre page doit mettre à jour le nom de l'utilisateur, il vous suffit d'écrire:

document.querySelector('#nameTag').textContent = 'Shellie';

et c'est tout. Le rendu du tag de nom est automatiquement mis à jour par le navigateur, car nous projections le contenu du tag de nom en place avec <content>.

<div id="ex2b">

Le contenu et la présentation sont maintenant séparés. Le contenu se trouve dans le document, et la présentation dans le Shadow DOM. Elles sont automatiquement synchronisées par le navigateur au moment d'afficher un élément.

Étape 3: Bénéfices

En séparant le contenu et la présentation, nous pouvons simplifier le code qui manipule le contenu. Dans l'exemple du tag de nom, ce code ne doit traiter que d'une structure simple contenant un seul <div> au lieu de plusieurs.

Si nous modifions notre présentation, plus besoin de modifier le code.

Imaginons, par exemple, que nous souhaitions localiser notre tag associé à un nom. Comme il s'agit toujours d'un tag de nom, le contenu sémantique du document ne change pas:

<div id="nameTag">Bob</div>

Le code de configuration racine fantôme reste le même. Ce qui est placé dans la racine fantôme change:

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

Il s'agit d'une nette amélioration par rapport à la situation sur le Web aujourd'hui, car le code de mise à jour de votre nom peut dépendre de la structure simple et cohérente du composant. Votre code de mise à jour de nom n'a pas besoin de connaître la structure utilisée pour le rendu. Le nom apparaît en deuxième position en anglais (après "Hi! Mon nom l'est"), mais il est d'abord en japonais (avant "screenshot申screenshotす"). D'un point de vue sémantique, cette distinction est dénuée de sens du point de vue de la mise à jour du nom affiché, de sorte que le code de mise à jour du nom n'a pas besoin de connaître ce détail.

Crédit supplémentaire: projection avancée

Dans l'exemple ci-dessus, l'élément <content> sélectionne tout le contenu de l'hôte fantôme. En utilisant l'attribut select, vous pouvez contrôler ce qu'un élément de contenu projette. Vous pouvez également utiliser plusieurs éléments de contenu.

Par exemple, si l'un de vos documents contient les informations suivantes:

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

et une racine fantôme qui utilise des sélecteurs CSS pour sélectionner du contenu spécifique:

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

L'élément <div class="email"> est mis en correspondance avec les éléments <content select="div"> et <content select=".email">. Combien de fois l'adresse e-mail de Bob apparaît-elle et dans quelles couleurs ?

La réponse est que l'adresse e-mail de Bob apparaît une seule fois, et qu'elle est jaune.

En effet, comme le savent les personnes qui piratent Shadow DOM, construire l'arborescence de ce qui est réellement affiché à l'écran est comme une grande fête. L'élément de contenu est l'invitation qui permet au contenu du document d'entrer dans la session de rendu Shadow DOM en coulisses. Ces invitations sont distribuées dans l'ordre. Le destinataire d'une invitation dépend de la personne à laquelle elle est adressée (c'est-à-dire l'attribut select). Le contenu, une fois invité, accepte toujours l'invitation (qui ne l'aurait pas fait ?) et s'en va. Si une invitation ultérieure est à nouveau envoyée à cette adresse, il n'y a personne à la maison et elle n'arrivera pas à votre fête.

Dans l'exemple ci-dessus, <div class="email"> correspond à la fois au sélecteur div et au sélecteur .email, mais comme l'élément de contenu avec le sélecteur div apparaît plus tôt dans le document, <div class="email"> passe au groupe jaune, et personne n'est disponible pour rejoindre le groupe bleu. C'est peut-être pourquoi il est si bleu, même si la misère aime la compagnie, alors vous ne le savez jamais.

Si un élément n'est invité à aucune partie, il n'est pas rendu du tout. C'est ce qui est arrivé au texte "Hello, world" dans le tout premier exemple. Cela s'avère utile lorsque vous souhaitez obtenir un rendu radicalement différent: écrivez le modèle sémantique dans le document, c'est-à-dire ce qui est accessible aux scripts de la page, mais masquez-le pour l'affichage et connectez-le à un modèle de rendu très différent dans Shadow DOM à l'aide de JavaScript.

Par exemple, HTML dispose d'un sélecteur de date intéressant. Si vous écrivez <input type="date">, vous obtenez un agenda pop-up soigné. Mais que se passe-t-il si vous souhaitez permettre à l'utilisateur de choisir une plage de dates pour ses vacances sur l'île de dessert (comme avec des hamacs fabriqués à partir de lianes rouges). Vous configurez votre document de la manière suivante:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

mais créez un Shadow DOM qui utilise un tableau pour créer un calendrier lisible mettant en évidence la plage de dates, et ainsi de suite. Lorsque l'utilisateur clique sur les jours du calendrier, le composant met à jour l'état dans les entrées startDate et endDate. Lorsque l'utilisateur envoie le formulaire, les valeurs de ces éléments d'entrée sont envoyées.

Pourquoi ai-je inclus des étiquettes dans le document alors qu'elles ne s'affichent pas ? En effet, si un utilisateur consulte le formulaire avec un navigateur non compatible avec Shadow DOM, le formulaire reste utilisable, mais pas aussi élégant. L'utilisateur voit quelque chose comme:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

Vous passez Shadow DOM 101

Vous connaissez maintenant les bases de Shadow DOM. Vous avez réussi le test de niveau 101 de Shadow DOM. Vous pouvez en faire plus avec Shadow DOM. Par exemple, vous pouvez utiliser plusieurs ombres sur un même hôte Shadow, des ombres imbriquées pour l'encapsulation, ou concevoir votre page à l'aide des vues basées sur le modèle (MDV) et du Shadow DOM. Les composants Web sont bien plus que des Shadow DOM.

Nous expliquons cela dans des messages ultérieurs.