Shadow DOM 301

Concepts avancés et API DOM

Cet article présente d'autres fonctionnalités incroyables que vous pouvez utiliser avec Shadow DOM. Il s'appuie sur les concepts abordés dans 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, le dernier arbre d'ombre ajouté à 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 donc l'intérêt d'utiliser plusieurs ombres si seule la dernière est invitée à la fête de rendu ? Saisissez les points d'insertion d'ombre.

Points d'insertion d'ombre

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. Toutefois, au lieu d'être des espaces réservés pour le contenu d'un hôte, ils hébergent d'autres arbres d'ombre. C'est l'Inception du Shadow DOM !

Comme vous pouvez probablement l'imaginer, les choses se compliquent à mesure que vous avancez dans le terrier du lapin. C'est pourquoi 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:

  1. "Root 2 FTW" s'affiche toujours au-dessus de "Root 1 FTW". Cela est dû à l'emplacement du point d'insertion <shadow>. Pour inverser l'ordre, déplacez le point d'insertion: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';.
  2. 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) { }
});

C'est un peu un bidouillage, mais ça fonctionne. 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 appuyez pas sur elle pour une isolation complète du contenu.

Créer un Shadow DOM en JS

Si vous préférez créer un DOM en JS, HTMLContentElement et HTMLShadowElement disposent d'interfaces à cet effet.

<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 sont autorisés à franchir la limite de l'ombre lorsque les points d'insertion les y 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'un élément de présentation/rendu: "Déplacez 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 dans lesquels 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 comprendre cela pour la première fois.

Pour vous aider à visualiser le fonctionnement du rendu du DOM fantôme, j'ai créé un outil à l'aide de d3.js. Les deux boîtes de balisage sur la gauche sont modifiables. N'hésitez pas à coller votre propre balisage et à jouer avec pour voir comment les choses fonctionnent et comment les points d'insertion mélangent les nœuds hôtes dans l'arborescence d'ombre.

Visualiseur Shadow DOM
Lancer le visualiseur Shadow DOM

Essayez-la et dites-moi ce que vous en pensez.

Modèle d'événement

Certains événements traversent la limite de l'ombre, d'autres non. Dans les cas où les événements traversent 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 d'ombre. 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.

Lire 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, un mouseout s'affiche à nouveau sur le nœud bleu.

Lire l'action 2

  • Un mouseout apparaît sur l'hôte (tout à la fin). Normalement, les événements mouseout se déclenchent pour tous les blocs jaunes. Toutefois, dans ce cas, ces éléments sont internes au DOM fantôme et l'événement ne remonte pas au-delà de 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.