Shadow DOM 301

Concepts avancés et API DOM

Cet article décrit 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 fantômes

Si vous organisez une fête, l'ambiance devient étouffante si tout le monde est encombré dans la même pièce. Vous souhaitez avoir la possibilité de répartir les groupes de personnes sur plusieurs salons. Les éléments hébergeant le Shadow DOM peuvent également effectuer cette opération. En d'autres termes, ils peuvent héberger plusieurs racines fantômes à la fois.

Voyons ce qui se passe si nous essayons d'associer plusieurs racines fantômes à 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>

Le résultat est "Root 2 FTW", malgré le fait que nous avions déjà associé un arbre fantôme. En effet, le dernier arbre fantôme ajouté à un hôte l'emporte. En ce qui concerne le rendu, il s'agit d'une pile LIFO. L'examen des outils de développement permet de vérifier ce comportement.

À quoi sert donc plusieurs ombres si seule la dernière est invitée à la partie du rendu ? Saisissez les points d'insertion de l'ombre.

Points d'insertion d'ombres

Les "points d'insertion d'ombres" (<shadow>) sont semblables aux points d'insertion normaux (<content>), car ce sont des espaces réservés. Cependant, au lieu d'être des espaces réservés pour le contenu d'un hôte, ils sont les hôtes d'autres arbres d'ombre. C'est Shadow DOM Inception !

Comme vous pouvez probablement l'imaginer, les choses se compliquent à mesure que vous percez un trou dans le terrier du lapin. Pour cette raison, la spécification indique très clairement ce qui se passe lorsque plusieurs éléments <shadow> sont en jeu:

Revenons à notre exemple d'origine. La première ombre root1 a été omise de la liste d'invitations. L'ajout d'un point d'insertion <shadow> permet de le réinitialiser:

<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>

Voici quelques points intéressants concernant cet exemple:

  1. "Root 2 FTW" s'affiche toujours au-dessus de "Root 1 FTW". Cela est dû à l'emplacement du point d'insertion <shadow>. Pour l'inverse, déplacez le point d'insertion: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';.
  2. Notez qu'il y a maintenant un point d'insertion <content> dans root1. Le nœud de texte "Light DOM" intervient alors pour le rendu.

Quel est le rendu à <shadow> ?

Il est parfois utile de connaître l'ancienne arborescence fantôme en cours d'affichage dans un <shadow>. Vous pouvez obtenir une référence à cette arborescence via .olderShadowRoot:

**root2.olderShadowRoot** === root1 //true

Obtenir la racine fantôme d'un hôte

Si un élément héberge le Shadow DOM, vous pouvez accéder à sa dernière racine fantôme à 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 personnes ne passent dans l'ombre, redéfinissez .shadowRoot sur une valeur nulle:

Object.defineProperty(host, 'shadowRoot', {
  get: function() { return null; },
  set: function(value) { }
});

C'est une astuce, mais ça marche. En fin de compte, il est important de se rappeler que, bien que incroyablement fantastique, Shadow DOM n'a pas été conçu pour être une fonctionnalité de sécurité. Ne vous y fiez pas pour isoler complètement le contenu.

Compiler un Shadow DOM en JavaScript

Si vous préférez compiler un DOM en JavaScript, 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 nouveau <span>.

Utiliser des points d'insertion

Les nœuds sélectionnés en dehors de l'élément hôte et "distribués" dans l'arborescence fantôme sont appelés "nœuds distribués par des roulements de tambour". Ils sont autorisés à traverser la limite de l'ombre lorsque les points d'insertion les invitent.

Ce qui est conceptuellement étrange 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 reprojetent simplement les nœuds de l'hôte dans l'arborescence fantôme. C'est une question de présentation ou de rendu: "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 Shadow DOM. Cela nous amène à une autre chose:

Element.getDistributedNodes()

Il n'est pas possible de balayer l'écran dans 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 dans lesquels un nœud est distribué en appelant sa méthode .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: Shadow DOM Visualizer

Il est difficile de comprendre la magie noire de Shadow DOM. Je me souviens avoir essayé de me pencher sur ce sujet pour la première fois.

Pour vous aider à visualiser le fonctionnement du rendu Shadow DOM, j'ai créé un outil à l'aide de d3.js. Les deux zones de balisage situées à gauche peuvent être modifiées. N'hésitez pas à coller votre propre balisage et à jouer pour voir comment les choses fonctionnent et comment les points d'insertion font glisser les nœuds hôtes dans l'arborescence fantôme.

Visualiseur Shadow DOM
Lancer Shadow DOM Visualizer

N'hésitez pas à l'essayer et à me dire ce que vous en pensez !

Modèle d'événement

Certains événements dépassent les limites de l'ombre, 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 reciblés pour donner l'impression qu'ils proviennent de l'élément hôte plutôt que d'éléments internes du Shadow DOM.

Lecture de l'action 1

  • Celle-ci est intéressante. Vous devriez voir un mouseout entre l'élément hôte (<div data-host>) et le 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 passez la souris plus en bas, puis à nouveau en jaune, une mouseout s'affiche sur le nœud bleu.

Lecture de l'action 2

  • Un élément mouseout apparaît sur l'hôte (à la toute fin). Normalement, les événements mouseout devraient se déclencher pour tous les blocs jaunes. Toutefois, dans ce cas, ces éléments sont internes au Shadow DOM et l'événement ne dépasse pas sa limite supérieure.

Lecture de l'action 3

  • Notez que lorsque vous cliquez sur l'entrée, focusin n'apparaît pas sur l'entrée, mais sur le nœud hôte lui-même. Il a été reciblé !

Événements toujours arrêtés

Les événements suivants ne dépassent jamais la limite de l'ombre:

  • abort
  • error
  • select
  • modifier
  • auto-infligée
  • reset
  • resize
  • scroll
  • sélectionnerstart

Conclusion

J'espère que vous serez d'accord pour dire que Shadow DOM est incroyablement puissant. Pour la première fois, nous obtenons une encapsulation appropriée sans avoir à ajouter des éléments <iframe> ou d'autres techniques plus anciennes.

Shadow DOM est certes un monstre complexe, mais mérite d'être ajouté à la plate-forme Web. Passez du temps avec elle. Apprenez. Posez des questions.

Pour en savoir plus, consultez l'article de présentation de Dominic Shadow DOM 101 et mon article Shadow DOM 201: CSS & Styling.