Introduction
Web Components est 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 cassera pas les pages si la prochaine version 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 quand utiliser des 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 lorsque vous développez un widget. Les normes Web Components sont conçues pour vous y aider.
Cependant, un problème fondamental rend les widgets créés à partir de HTML et de 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 votre feuille de style de document peut s'appliquer accidentellement à des parties du widget, que votre code JavaScript peut modifier accidentellement des parties du widget, que vos ID peuvent se chevaucher avec ceux du widget, etc.
Web Components se compose de trois parties:
Le Shadow DOM résout le problème d'encapsulation de l'arborescence DOM. Les quatre composants Web sont conçus pour fonctionner ensemble, mais vous pouvez également choisir les composants Web à utiliser. Ce tutoriel vous explique comment utiliser le DOM fantôme.
Bonjour, Shadow World
Avec Shadow DOM, les éléments peuvent se voir attribuer un nouveau type de nœud. Ce nouveau type de nœud est appelé racine d'ombre. Un élément auquel une racine d'ombre est associée est appelé hôte d'ombre. Le contenu d'un hôte fantôme n'est pas affiché. C'est le contenu de la racine fantôme qui est affiché à la place.
Par exemple, si vous aviez un balisage 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 se présente comme suit :
<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 JavaScript sur la page demande quel est le textContent
du bouton, il ne recevra pas "こんにちは、影の世界!", mais "Hello, world!", car le sous-arbre DOM sous le nœud racine de l'ombre 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 disposions de cette étiquette:
<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 écrivez 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>
L'arborescence DOM n'étant pas encapsulée, l'ensemble de la structure de la balise de nom est exposé au document. Si d'autres éléments de la page utilisent accidentellement les mêmes noms de classe pour le style ou le script, nous allons avoir des problèmes.
Nous pouvons éviter de passer un mauvais moment.
Étape 1: Masquer les détails de la présentation
Sémantiquement, nous ne nous soucions probablement que de:
- Il s'agit d'une étiquette nominative.
- Le nom est "Bob".
Tout d'abord, nous écrivons du balisage plus proche de la sémantique réelle souhaitée:
<div id="nameTag">Bob</div>
Nous plaçons ensuite tous les styles et divs 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 peuvent être accessibles à partir de JavaScript. Nous le faisons maintenant pour renseigner la racine de l'ombre:
<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 d'ombre, la balise de nom est à nouveau affichée. Si vous effectuez un clic droit sur la balise de nom et inspectez l'élément, vous constaterez qu'il s'agit d'une balise sémantique très pratique:
<div id="nameTag">Bob</div>
Cela montre qu'en utilisant le Shadow DOM, nous avons masqué les détails de présentation de la balise de nom du document. Les détails de la présentation sont encapsulés dans le Shadow DOM.
Étape 2: Séparer le contenu de la présentation
Notre étiquette de nom masque désormais les détails de la présentation de la page, mais elle ne sépare pas réellement la présentation du contenu. En effet, bien que le contenu (le nom "Bob") se trouve sur la page, le nom qui s'affiche est celui que nous avons copié dans la racine d'ombre. Si nous voulons modifier le nom sur la carte, nous devons le faire à deux endroits, et ils risquent de se désynchroniser.
Les éléments HTML sont des éléments de composition. Vous pouvez par exemple placer un bouton dans un tableau. La composition est ce dont nous avons besoin ici: la carte de visite doit être une composition de l'arrière-plan rouge, du texte "Bonjour" et du contenu figurant sur la carte de visite.
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 stade.
Si nous remplaçons le balisage du Shadow DOM par le code suivant:
<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 la balise de nom est affichée, le contenu de l'hôte fantôme est projeté à l'emplacement 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 voilà ! Le rendu de la carte de visite est automatiquement mis à jour par le navigateur, car nous projetons le contenu de la carte de visite à 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, et la présentation dans le Shadow DOM. Elles sont automatiquement synchronisées par le navigateur au moment de l'affichage.
Étape 3: Profit
En séparant le contenu et la présentation, nous pouvons simplifier le code qui manipule le contenu. Dans l'exemple de la carte de visite, ce code ne doit gérer qu'une structure simple contenant un <div>
au lieu de plusieurs.
Si nous modifions notre présentation, nous n'avons pas besoin de modifier le code.
Par exemple, supposons que nous souhaitions localiser notre badge. Il s'agit toujours d'une balise de nom. Le contenu sémantique du document ne change donc pas:
<div id="nameTag">Bob</div>
Le code de configuration de la racine d'ombre reste le même. Ce qui est mis dans la racine d'ombre 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 considérable par rapport à la situation actuelle sur le Web, car votre code de modification de nom peut dépendre de la structure du composant, qui est simple et cohérente. Votre code de mise à jour du nom n'a pas besoin de connaître la structure utilisée pour le rendu. Si nous examinons ce qui est affiché, le nom apparaît en deuxième position en anglais (après "Bonjour ! "Je m'appelle"), mais d'abord en japonais (avant "と申します"). Cette distinction n'a aucune signification sémantique 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 votre document contient les éléments suivants:
<div id="nameTag">
<div class="first">Bob</div>
<div>B. Love</div>
<div class="email">bob@</div>
</div>
et une racine d'ombre 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">
est mis en correspondance par 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 est jaune.
En effet, comme le savent les personnes qui piratent le Shadow DOM, la construction de 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 d'intégrer le contenu du document dans la partie de rendu du Shadow DOM en coulisses. Ces invitations sont envoyées dans l'ordre. Le destinataire dépend de l'attribut select
. Une fois invité, le contenu accepte toujours l'invitation (qui ne le ferait pas ?), et c'est parti. Si une invitation ultérieure est envoyée à cette adresse, personne n'est chez lui et elle ne sera pas reçue.
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">
est attribué à la fête jaune et personne n'est disponible pour la fête bleue. (C'est peut-être pourquoi il est si bleu, bien que la misère aime la compagnie, donc on ne sait jamais.)
Si un élément n'est invité à aucune partie, il n'est pas du tout affiché. C'est ce qui est arrivé au 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 associez-le à un modèle de rendu vraiment différent dans Shadow DOM à l'aide de JavaScript.
Par exemple, HTML propose 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 laisser l'utilisateur choisir une plage de dates pour ses vacances sur une île sucrées (avec des hamacs en vigne rouge, par exemple) ? Vous configurez votre document comme suit:
<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 une table pour créer un calendrier élégant qui met en évidence la plage de dates, etc. 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 libellés dans le document s'ils ne seront pas affichés ? En effet, si un utilisateur consulte le formulaire avec un navigateur qui n'est pas compatible avec Shadow DOM, le formulaire reste utilisable, mais pas aussi joli. L'utilisateur voit quelque chose comme ceci:
<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 cours de base sur le Shadow DOM
Vous avez maintenant les bases du Shadow DOM en poche. Vous pouvez faire plus avec Shadow DOM. Par exemple, vous pouvez utiliser plusieurs ombres sur un hôte d'ombre, ou des ombres imbriquées pour l'encapsulation, ou concevoir votre page à l'aide de vues basées sur le modèle (MDV) et de Shadow DOM. Et les composants Web ne se limitent pas à Shadow DOM.
Nous les expliquerons dans de prochains articles.