Shadow DOM 301

Concepts avancés et API DOM

Cet article présente d'autres choses incroyables que vous pouvez faire avec le Shadow DOM. Il s'appuie sur les concepts abordés dans Shadow DOM 101 et Shadow DOM 201.

Utiliser plusieurs racines fantômes

Si vous organisez une fête, l'atmosphère devient étouffante si tout le monde est entassé dans la même pièce. Vous souhaitez pouvoir répartir des groupes de personnes dans plusieurs salles. Les éléments hébergeant le Shadow DOM peuvent également le faire, c'est-à-dire qu'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 rendu est "Root 2 FTW", même si nous avions déjà associé un shadow tree. En effet, le dernier arbre fantôme ajouté à un hôte l'emporte. Il s'agit d'une pile LIFO en ce qui concerne l'affichage. L'examen des outils de développement confirme ce comportement.

Alors, à quoi sert d'utiliser plusieurs ombres si seule la dernière est invitée à la fête du rendu ? Saisissez des points d'insertion fantômes.

Points d'insertion fantômes

Les "points d'insertion cachés" (<shadow>) sont semblables aux points d'insertion normaux (<content>) dans la mesure où il s'agit d'espaces réservés. Cependant, au lieu d'être des espaces réservés pour le contenu d'un hôte, ils hébergent d'autres arborescences fantômes. C'est l'Inception du Shadow DOM !

Comme vous pouvez l'imaginer, les choses se compliquent à mesure que vous creusez le sujet. C'est pourquoi la spécification est très claire sur ce qui se passe lorsque plusieurs éléments <shadow> sont en jeu :

Si nous reprenons notre exemple initial, le premier shadow root1 n'a pas été inclus dans la liste d'invitation. L'ajout d'un point d'insertion <shadow> permet de le restaurer :

<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 à noter 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>. Si vous souhaitez l'inverse, déplacez le point d'insertion : root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';.
  2. Notez qu'il existe désormais un point d'insertion <content> 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 shadow tree affiché au niveau d'un <shadow>. Vous pouvez obtenir une référence à cette arborescence via .olderShadowRoot :

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

Obtenir la racine shadow d'un hôte

Si un élément héberge un 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 personnes ne pénètrent dans vos ombres, redéfinissez .shadowRoot sur "null" :

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

C'est un peu du bricolage, mais ça fonctionne. En fin de compte, il est important de se rappeler que, bien qu'il soit incroyablement fantastique, le Shadow DOM n'a pas été conçu pour être une fonctionnalité de sécurité. Ne vous fiez pas à cette fonctionnalité pour isoler complètement le 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 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> que je viens d'ajouter.

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 shadow sont appelés… roulement de tambour… nœuds distribués. Ils sont autorisés à franchir la limite du shadow DOM lorsque des 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 shadow. Il s'agit d'un problème de présentation/rendu : "Déplacer ces nœuds ici" "Afficher 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 à un autre point :

Element.getDistributedNodes()

Nous ne pouvons pas parcourir un <content>, mais l'API .getDistributedNodes() nous permet d'interroger les nœuds distribués au niveau d'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 dans quels points d'insertion 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 : Shadow DOM Visualizer

Il est difficile de comprendre la magie noire qu'est le Shadow DOM. Je me souviens de la première fois que j'ai essayé de comprendre ce concept.

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

Visualiseur de Shadow DOM
Lancer le visualiseur de Shadow DOM

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

Modèle d'événement

Certains événements franchissent la limite du shadow DOM, d'autres non. Lorsque les événements franchissent 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. En d'autres termes, 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 au Shadow DOM.

Play Action 1

  • Celle-ci 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 la souris plus bas dans la zone jaune, un événement mouseout est à nouveau déclenché sur le nœud bleu.

Play Action 2

  • Un mouseout apparaît sur l'hôte (à la toute fin). Normalement, vous devriez voir les événements mouseout 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 se propage pas au-delà de sa limite supérieure.

Play 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é redirigé !

Événements qui sont toujours arrêtés

Les événements suivants ne franchissent jamais la limite du shadow DOM :

  • abort
  • erreur
  • sélectionner
  • modifier
  • cargaison
  • réinitialiser
  • resize
  • scroll
  • selectstart

Conclusion

J'espère que vous conviendrez que le Shadow DOM est incroyablement puissant. Pour la première fois, nous disposons d'une encapsulation appropriée sans le bagage supplémentaire des <iframe> ou d'autres techniques plus anciennes.

Le Shadow DOM est certainement une bête complexe, mais c'est une bête qui mérite d'être ajoutée à la plate-forme Web. Prenez le temps de l'explorer. Apprenez Posez des questions.

Pour en savoir plus, consultez l'article d'introduction de Dominic, Shadow DOM 101, et mon article Shadow DOM 201 : CSS et styles.