Introduction
Les composants Web sont un ensemble de normes de pointe qui :
- Permettre de créer des widgets
- …qui peuvent être réutilisés de manière fiable
- … et qui ne casseront pas les pages si la prochaine version du composant modifie les détails d'implémentation internes.
Cela signifie-t-il que vous devez décider quand utiliser HTML/JavaScript et quand utiliser 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 logique d'exploiter vos compétences en HTML et JavaScript lors du développement d'un widget. Les normes des composants Web sont conçues pour vous aider à y parvenir.
Cependant, un problème fondamental rend les widgets créés en HTML et JavaScript difficiles à utiliser : l'arborescence DOM d'un widget n'est pas encapsulée par rapport au 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 du widget, que vos ID peuvent chevaucher des ID du widget, etc.
Les composants Web se composent de trois parties :
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 les parties des composants Web à utiliser. Ce tutoriel vous explique comment utiliser le Shadow DOM.
Hello, Shadow World
Avec Shadow DOM, les éléments peuvent être associés à un nouveau type de nœud. Ce nouveau type de nœud est appelé racine shadow. Un élément auquel une racine fantôme est associée est appelé hôte fantôme. Le contenu d'un hôte shadow n'est pas affiché. À la place, c'est le contenu de la racine shadow qui est affiché.
Par exemple, si vous aviez un balisage comme celui-ci :
<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>
De plus, si le code JavaScript de la page demande quel est le textContent du bouton, il n'obtiendra pas "こんにちは、影の世界!", mais "Hello, world!", car le sous-arbre DOM sous la racine fantôme est encapsulé.
Séparer le contenu de la présentation
Nous allons maintenant voir comment utiliser Shadow DOM pour séparer le contenu de la présentation. Imaginons que nous ayons ce tag de nom :
<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 écririez 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, toute la structure du tag 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 les scripts, nous allons avoir des problèmes.
Nous pouvons éviter de passer un mauvais moment.
Étape 1 : Masquez les détails de la présentation
Sémantiquement, nous ne nous soucions probablement que de ce qui suit :
- Il s'agit d'un tag de nom.
- Le nom est "Bob".
Tout d'abord, nous écrivons un balisage plus proche de la sémantique que nous voulons :
<div id="nameTag">Bob</div>
Ensuite, nous plaçons tous les styles et les éléments 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, seul "Bob" est affiché. Comme nous avons déplacé les éléments DOM de présentation dans un élément <template>, ils ne sont pas affichés, mais ils peuvent être accessibles depuis JavaScript. Nous allons maintenant le faire 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 shadow, le tag de nom s'affiche à nouveau. Si vous effectuez un clic droit sur le tag de nom et inspectez l'élément, vous verrez qu'il s'agit d'un balisage sémantique simple :
<div id="nameTag">Bob</div>
Cela montre qu'en utilisant le Shadow DOM, nous avons masqué les détails de présentation du tag de nom du document. Les détails de la présentation sont encapsulés dans le Shadow DOM.
Étape 2 : Séparez le contenu de la présentation
Notre balise de nom masque désormais les détails de présentation de la page, mais elle ne sépare pas réellement la présentation du contenu, car même si le contenu (le nom "Bob") se trouve sur la page, le nom affiché est celui que nous avons copié dans la racine fantôme. Si nous voulons modifier le nom sur le badge, nous devons le faire à deux endroits, et ils risquent de ne plus être synchronisés.
Les éléments HTML sont compositionnels : vous pouvez par exemple placer un bouton dans un tableau. C'est la composition dont nous avons besoin ici : le tag de nom doit être une composition de l'arrière-plan rouge, du texte "Hi!" et du contenu qui se trouve sur le tag de nom.
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, et le point d'insertion sélectionne le contenu de l'hôte fantôme à présenter à ce point.
Si nous modifions le balisage dans le 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'emplacement de l'élément <content>.
La structure du document est désormais plus simple, car le nom ne figure 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 voilà ! Le rendu du tag de nom est automatiquement mis à jour par le navigateur, car nous projetons le contenu du tag de nom à l'aide de <content>.
<div id="ex2b">
Nous avons maintenant séparé le contenu de la présentation. Le contenu se trouve dans le document, la présentation dans le Shadow DOM. Ils sont automatiquement synchronisés par le navigateur lorsqu'il est temps d'afficher quelque chose.
Étape 3 : Générer des 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 n'a besoin de traiter qu'une structure simple contenant un seul <div> au lieu de plusieurs.
Désormais, si nous modifions notre présentation, nous n'avons pas besoin de modifier le code.
Par exemple, imaginons que nous voulons localiser notre badge. Il s'agit toujours d'une balise de nom, de sorte que le contenu sémantique du document ne change pas :
<div id="nameTag">Bob</div>
Le code de configuration de la racine fantôme reste le même. Seul le contenu de la racine shadow 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 amélioration majeure par rapport à la situation actuelle sur le Web, car le code de mise à jour du nom peut dépendre de la structure du composant, qui est simple et cohérente. Le code de mise à jour du nom n'a pas besoin de connaître la structure utilisée pour l'affichage. Si nous examinons ce qui est affiché, le nom apparaît en deuxième position en anglais (après "Hi! "Je m'appelle"), mais en premier en japonais (avant "と申します"). Cette distinction n'a pas de sens sémantiquement du point de vue de la mise à jour du nom affiché. Le code de mise à jour du nom n'a donc pas besoin de connaître ce détail.
Bonus : projection avancée
Dans l'exemple ci-dessus, l'élément <content> sélectionne tout le contenu de l'hôte fantôme. L'attribut select vous permet de contrôler ce qu'un élément de contenu projette. Vous pouvez également utiliser plusieurs éléments de contenu.
Par exemple, si vous avez un document qui contient ceci :
<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 un 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"> correspond aux é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 fois, et qu'elle est jaune.
En effet, comme le savent les personnes qui travaillent sur Shadow DOM, la construction de l'arborescence de ce qui est réellement affiché à l'écran est un processus complexe. L'élément de contenu est l'invitation qui permet au contenu du document d'accéder au rendu du Shadow DOM en arrière-plan. Ces invitations sont envoyées dans l'ordre. Le destinataire d'une invitation dépend de la personne à qui elle est adressée (c'est-à-dire l'attribut select). Une fois invité, le contenu accepte toujours l'invitation (qui ne le ferait pas ?) et s'en va. Si une invitation ultérieure est envoyée à cette adresse, personne ne la reçoit et l'invité ne vient pas à votre fête.
Dans l'exemple ci-dessus, <div class="email"> correspond à la fois au sélecteur div et au sélecteur .email. Toutefois, comme l'élément de contenu avec le sélecteur div apparaît plus tôt dans le document, <div class="email"> va à la fête jaune, et personne n'est disponible pour aller à la fête bleue. (C'est peut-être pour cela qu'il est si bleu, mais comme on dit, malheur partagé n'est que demi-malheur, alors on ne sait jamais.)
Si un élément n'est invité à aucune fête, il n'est pas affiché du tout. C'est ce qui s'est passé avec le texte "Hello, world" dans le tout premier exemple. Cela est utile lorsque vous souhaitez obtenir un rendu radicalement différent : écrivez le modèle sémantique dans le document, qui est accessible aux scripts de la page, mais masquez-le à des fins de rendu et connectez-le à un modèle de rendu vraiment différent dans le Shadow DOM à l'aide de JavaScript.
Par exemple, HTML dispose d'un sélecteur de date pratique. Si vous écrivez <input
type="date">, un calendrier pop-up s'affiche. Mais que se passe-t-il si vous souhaitez permettre à l'utilisateur de choisir une plage de dates pour ses vacances sur l'île des desserts (vous savez… avec des hamacs faits de Red Vines) ? 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 élégant qui met en évidence la plage de dates, etc. Lorsque l'utilisateur clique sur les jours dans le 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 si elles ne sont pas affichées ? En effet, si un utilisateur consulte le formulaire avec un navigateur qui n'est pas compatible avec Shadow DOM, le formulaire reste utilisable, mais il est moins esthétique. L'utilisateur voit un message semblable à celui-ci :
<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 avez réussi le module "Shadow DOM 101"
Voilà pour les bases du Shadow DOM. Vous avez réussi le cours d'introduction ! Vous pouvez faire plus avec le Shadow DOM. Par exemple, vous pouvez utiliser plusieurs ombres sur un hôte d'ombre, ou des ombres imbriquées pour l'encapsulation, ou encore concevoir votre page à l'aide de vues basées sur un modèle (MDV) et du Shadow DOM. Et les composants Web ne se limitent pas au Shadow DOM.
Nous les expliquerons dans des articles ultérieurs.