Concepts avancés et API DOM
Cet article vous explique tout ce que vous pouvez faire avec Shadow DOM. Il s'appuie sur les concepts abordés dans les sections Shadow DOM 101 et Shadow DOM 201.
Utiliser plusieurs racines d'ombre
Si vous organisez une fête, il peut faire chaud si tout le monde est entassés dans la même pièce. Vous souhaitez pouvoir répartir des groupes de personnes dans plusieurs salles. Les éléments hébergeant le DOM d'ombre peuvent également le faire, c'est-à-dire qu'ils peuvent héberger plusieurs racines d'ombre à la fois.
Voyons ce qui se passe si nous essayons d'associer plusieurs racines d'ombre à un hôte :
<div id="example1">Light DOM</div>
<script>
var container = document.querySelector('#example1');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();
root1.innerHTML = '<div>Root 1 FTW</div>';
root2.innerHTML = '<div>Root 2 FTW</div>';
</script>
L'affichage est "Root 2 FTW", même si nous avions déjà associé un arbre d'ombre. En effet, la dernière arborescence fantôme ajoutée à un hôte l'emporte. Il s'agit d'une pile LIFO en ce qui concerne le rendu. L'examen des outils de développement confirme ce comportement.
Quel est l'intérêt d'utiliser plusieurs ombres si seule la dernière est invitée à la partie de rendu ? Saisissez les points d'insertion d'ombre.
Points d'insertion d'ombres
Les "points d'insertion d'ombre" (<shadow>
) sont semblables aux points d'insertion (<content>
) normaux en ce sens qu'ils sont des espaces réservés. Cependant, au lieu d'être des espaces réservés pour le contenu d'un hôte, ce sont des hôtes pour d'autres arborescences fantômes.
C'est Shadow DOM Inception !
Comme vous pouvez probablement l'imaginer, les choses se compliquent à mesure que vous avancez dans le terrier du lapin. Pour cette raison, la spécification est très claire sur ce qui se passe lorsque plusieurs éléments <shadow>
sont en jeu :
Pour revenir à notre exemple initial, le premier root1
d'ombre a été exclu de la liste d'invitations. Ajouter un point d'insertion <shadow>
le rétablit :
<div id="example2">Light DOM</div>
<script>
var container = document.querySelector('#example2');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();
root1.innerHTML = '<div>Root 1 FTW</div><content></content>';
**root2.innerHTML = '<div>Root 2 FTW</div><shadow></shadow>';**
</script>
Cet exemple présente quelques points intéressants :
- "Root 2 FTW" s'affiche toujours au-dessus de "Root 1 FTW". Cela est dû à l'emplacement du point d'insertion
<shadow>
. Si vous souhaitez l'inverser, déplacez le point d'insertion :root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';
. - Notez qu'un point d'insertion
<content>
est maintenant présent dans root1. Le nœud de texte "Light DOM" est ainsi inclus dans le rendu.
Qu'est-ce qui est affiché à <shadow>
?
Il est parfois utile de connaître l'ancien arbre d'ombre affiché à un <shadow>
. Vous pouvez obtenir une référence à cet arbre via .olderShadowRoot
:
**root2.olderShadowRoot** === root1 //true
Obtenir la racine de l'ombre d'un hôte
Si un élément héberge le Shadow DOM, vous pouvez accéder à sa racine fantôme la plus récente à l'aide de .shadowRoot
:
var root = host.createShadowRoot();
console.log(host.shadowRoot === root); // true
console.log(document.body.shadowRoot); // null
Si vous craignez que des utilisateurs ne traversent vos ombres, définissez .shadowRoot
sur "null" :
Object.defineProperty(host, 'shadowRoot', {
get: function() { return null; },
set: function(value) { }
});
Un peu d'astuce, mais ça marche. En fin de compte, il est important de se rappeler que, bien que le DOM fantôme soit incroyablement fantastique, il n'a pas été conçu comme une fonctionnalité de sécurité. Ne vous y fiez pas pour l'isolation complète du contenu.
Créer un Shadow DOM dans JS
Si vous préférez compiler le DOM en JS, HTMLContentElement
et HTMLShadowElement
disposent d'interfaces pour cela.
<div id="example3">
<span>Light DOM</span>
</div>
<script>
var container = document.querySelector('#example3');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();
var div = document.createElement('div');
div.textContent = 'Root 1 FTW';
root1.appendChild(div);
// HTMLContentElement
var content = document.createElement('content');
content.select = 'span'; // selects any spans the host node contains
root1.appendChild(content);
var div = document.createElement('div');
div.textContent = 'Root 2 FTW';
root2.appendChild(div);
// HTMLShadowElement
var shadow = document.createElement('shadow');
root2.appendChild(shadow);
</script>
Cet exemple est presque identique à celui de la section précédente.
La seule différence est que j'utilise maintenant select
pour extraire le <span>
nouvellement ajouté.
Utiliser des points d'insertion
Les nœuds sélectionnés dans l'élément hôte et "distribués" dans l'arborescence d'ombre sont appelés…roulement de tambour…nœuds distribués. Ils peuvent traverser la limite de l'ombre lorsque des points d'insertion les invitent.
Ce qui est conceptuellement bizarre avec les points d'insertion, c'est qu'ils ne déplacent pas physiquement le DOM. Les nœuds de l'hôte restent intacts. Les points d'insertion ne font que reprojeter les nœuds de l'hôte dans l'arborescence d'ombre. Il s'agit d'une présentation/d'un rendu: "Déplace ces nœuds ici" "Affichez ces nœuds à cet emplacement."
Exemple :
<div><h2>Light DOM</h2></div>
<script>
var root = document.querySelector('div').createShadowRoot();
root.innerHTML = '<content select="h2"></content>';
var h2 = document.querySelector('h2');
console.log(root.querySelector('content[select="h2"] h2')); // null;
console.log(root.querySelector('content').contains(h2)); // false
</script>
Et voilà, h2
n'est pas un enfant du DOM ombragé. Cela nous amène à un autre point :
Element.getDistributedNodes()
Nous ne pouvons pas traverser un <content>
, mais l'API .getDistributedNodes()
nous permet d'interroger les nœuds distribués à un point d'insertion :
<div id="example4">
<h2>Eric</h2>
<h2>Bidelman</h2>
<div>Digital Jedi</div>
<h4>footer text</h4>
</div>
<template id="sdom">
<header>
<content select="h2"></content>
</header>
<section>
<content select="div"></content>
</section>
<footer>
<content select="h4:first-of-type"></content>
</footer>
</template>
<script>
var container = document.querySelector('#example4');
var root = container.createShadowRoot();
var t = document.querySelector('#sdom');
var clone = document.importNode(t.content, true);
root.appendChild(clone);
var html = [];
[].forEach.call(root.querySelectorAll('content'), function(el) {
html.push(el.outerHTML + ': ');
var nodes = el.getDistributedNodes();
[].forEach.call(nodes, function(node) {
html.push(node.outerHTML);
});
html.push('\n');
});
</script>
Element.getDestinationInsertionPoints()
Comme pour .getDistributedNodes()
, vous pouvez vérifier les points d'insertion auxquels un nœud est distribué en appelant son .getDestinationInsertionPoints()
:
<div id="host">
<h2>Light DOM
</div>
<script>
var container = document.querySelector('div');
var root1 = container.createShadowRoot();
var root2 = container.createShadowRoot();
root1.innerHTML = '<content select="h2"></content>';
root2.innerHTML = '<shadow></shadow>';
var h2 = document.querySelector('#host h2');
var insertionPoints = h2.getDestinationInsertionPoints();
[].forEach.call(insertionPoints, function(contentEl) {
console.log(contentEl);
});
</script>
Outil : Visualiseur Shadow DOM
Comprendre la magie noire qu'est le Shadow DOM est difficile. Je me souviens d'avoir essayé de me tourner la tête pour la première fois.
Pour vous aider à visualiser le fonctionnement du rendu Shadow DOM, j'ai créé un outil utilisant d3.js. Les deux boîtes de balisage sur la gauche sont modifiables. N'hésitez pas à coller votre propre balisage et à observer le fonctionnement des éléments. Les points d'insertion font tourner les nœuds hôtes dans l'arborescence fantôme.
Essayez-la et dites-moi ce que vous en pensez.
Modèle d'événement
Certains événements dépassent la limite des ombres et d'autres non. Dans les cas où des événements dépassent la limite, la cible de l'événement est ajustée afin de maintenir l'encapsulation fournie par la limite supérieure de la racine fantôme. Autrement dit, les événements sont redirigés pour donner l'impression qu'ils proviennent de l'élément hôte plutôt que des éléments internes du Shadow DOM.
Jouer à l'action 1
- Cette question est intéressante. Vous devriez voir un
mouseout
de l'élément hôte (<div data-host>
) au nœud bleu. Même s'il s'agit d'un nœud distribué, il se trouve toujours dans l'hôte, et non dans le ShadowDOM. Si vous déplacez le pointeur de la souris vers le bas dans la zone jaune, unmouseout
s'affiche à nouveau sur le nœud bleu.
Jouer à l'action 2
- Un
mouseout
apparaît sur l'hôte (tout à la fin). Normalement, les événementsmouseout
se déclenchent pour tous les blocs jaunes. Toutefois, dans ce cas, ces éléments sont internes au Shadow DOM, et l'événement ne passe pas par sa limite supérieure.
Exécuter l'action 3
- Notez que lorsque vous cliquez sur l'entrée, le
focusin
n'apparaît pas sur l'entrée, mais sur le nœud hôte lui-même. Elle a été re ciblée.
Événements toujours arrêtés
Les événements suivants ne traversent jamais la limite de l'ombre :
- abort
- erreur
- sélectionner
- modifier
- auto-infligée
- réinitialiser
- resize
- scroll
- selectstart
Conclusion
J'espère que vous serez d'accord pour dire que le Shadow DOM est incroyablement puissant. Pour la première fois, nous disposons d'une encapsulation appropriée sans le fardeau supplémentaire des <iframe>
ou d'autres techniques plus anciennes.
Le Shadow DOM est certes un animal complexe, mais il mérite d'être ajouté à la plate-forme Web. Passez du temps avec elle. Apprenez-le. Posez des questions.
Pour en savoir plus, consultez l'article d'introduction de Dominic Shadow DOM 101 et mon article Shadow DOM 201: CSS & Styling.