DOM 301 Shadow

Concetti avanzati e API DOM

Questo articolo illustra altre cose straordinarie che puoi fare con Shadow DOM. Si basa sui concetti discussi in Shadow DOM 101 e Shadow DOM 201.

Utilizzo di più shadow root

Se stai organizzando una festa, l'ambiente diventa soffocante se tutti sono stipati nella stessa stanza. Vuoi avere la possibilità di distribuire gruppi di persone in più stanze. Anche gli elementi che ospitano lo Shadow DOM possono farlo, ovvero possono ospitare più di una shadow root alla volta.

Vediamo cosa succede se proviamo ad associare più shadow root a un host:

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

Viene eseguito il rendering di "Root 2 FTW", nonostante il fatto che avessimo già collegato uno shadow tree. Questo perché l'ultimo shadow tree aggiunto a un host ha la precedenza. Si tratta di uno stack LIFO per quanto riguarda il rendering. L'esame di DevTools verifica questo comportamento.

Quindi, qual è lo scopo di utilizzare più shadow se solo l'ultima viene invitata alla festa del rendering? Inserisci i punti di inserimento shadow.

Punti di inserimento shadow

I "punti di inserimento shadow" (<shadow>) sono simili ai normali punti di inserimento (<content>) in quanto sono segnaposto. Tuttavia, anziché essere segnaposto per i contenuti di un host, sono host per altri shadow tree. È l'Inception dello Shadow DOM.

Come puoi immaginare, le cose si complicano man mano che approfondisci l'argomento. Per questo motivo, la specifica è molto chiara su cosa succede quando sono in gioco più elementi <shadow>:

Tornando al nostro esempio originale, la prima ombra root1 è stata esclusa dall'elenco degli invitati. L'aggiunta di un punto di inserimento <shadow> lo ripristina:

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

Questo esempio presenta un paio di aspetti interessanti:

  1. "Root 2 FTW" viene comunque visualizzato sopra "Root 1 FTW". Questo perché abbiamo posizionato il punto di inserimento <shadow>. Se vuoi l'opposto, sposta il punto di inserimento: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';.
  2. Nota che ora c'è un punto di inserimento <content> in root1. In questo modo, il nodo di testo "Light DOM" viene incluso nel rendering.

Che cosa viene visualizzato in <shadow>?

A volte è utile conoscere lo shadow tree precedente di cui viene eseguito il rendering in un <shadow>. Puoi ottenere un riferimento a quell'albero tramite .olderShadowRoot:

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

Ottenere la shadow root di un host

Se un elemento ospita Shadow DOM, puoi accedere alla sua shadow root più recente utilizzando .shadowRoot:

var root = host.createShadowRoot();
console.log(host.shadowRoot === root); // true
console.log(document.body.shadowRoot); // null

Se temi che le persone possano entrare nelle tue shadow, ridefinisci .shadowRoot come null:

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

Un po' un hack, ma funziona. In definitiva, è importante ricordare che, sebbene sia incredibilmente fantastico, Shadow DOM non è stato progettato per essere una funzionalità di sicurezza. Non fare affidamento su questa funzionalità per l'isolamento completo dei contenuti.

Creazione di shadow DOM in JS

Se preferisci creare il DOM in JS, HTMLContentElement e HTMLShadowElement hanno interfacce per farlo.

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

Questo esempio è quasi identico a quello della sezione precedente. L'unica differenza è che ora utilizzo select per estrarre il <span> appena aggiunto.

Utilizzo dei punti di inserimento

I nodi selezionati dall'elemento host e "distribuiti" nello shadow tree sono chiamati… rullo di tamburi… nodi distribuiti. Possono attraversare il confine dello shadow DOM quando i punti di inserimento li invitano a farlo.

La particolarità concettuale dei punti di inserimento è che non spostano fisicamente il DOM. I nodi dell'host rimangono intatti. I punti di inserimento si limitano a riproiettare i nodi dall'host allo shadow tree. È una questione di presentazione/rendering: "Sposta questi nodi qui" "Esegui il rendering di questi nodi in questa posizione".

Ad esempio:

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

Ecco fatto. L'elemento h2 non è un elemento secondario dello shadow DOM. Questo ci porta a un altro aspetto:

Element.getDistributedNodes()

Non possiamo attraversare un <content>, ma l'API .getDistributedNodes() ci consente di eseguire query sui nodi distribuiti in un punto di inserimento:

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

Analogamente a .getDistributedNodes(), puoi controllare in quali punti di inserimento è distribuito un nodo chiamando il relativo .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>

Strumento: Shadow DOM Visualizer

Comprendere la magia nera di Shadow DOM è difficile. Ricordo di aver cercato di capirlo per la prima volta.

Per aiutarti a visualizzare il funzionamento del rendering dello Shadow DOM, ho creato uno strumento utilizzando d3.js. Entrambe le caselle di markup sul lato sinistro sono modificabili. Puoi incollare il tuo markup e fare delle prove per vedere come funzionano le cose e come i punti di inserimento scambiano i nodi host nello shadow tree.

Visualizzatore DOM shadow
Avvia Shadow DOM Visualizer

Provalo e fammi sapere cosa ne pensi.

Modello di evento

Alcuni eventi attraversano il confine dello shadow DOM, altri no. Nei casi in cui gli eventi superano il confine, il target dell'evento viene modificato per mantenere l'incapsulamento fornito dal confine superiore della shadow root. In altre parole, gli eventi vengono reindirizzati in modo che sembrino provenire dall'elemento host anziché dagli elementi interni allo Shadow DOM.

Play Action 1

  • Questo è interessante. Dovresti vedere un mouseout dall'elemento host (<div data-host>) al nodo blu. Anche se è un nodo distribuito, si trova comunque nell'host, non nello ShadowDOM. Spostando il mouse più in basso nel giallo, si attiva di nuovo un mouseout sul nodo blu.

Play Action 2

  • C'è un mouseout che appare sull'host (alla fine). Normalmente, vedresti l'attivazione di eventi mouseout per tutti i blocchi gialli. Tuttavia, in questo caso questi elementi sono interni allo Shadow DOM e l'evento non si propaga attraverso il suo limite superiore.

Play Action 3

  • Tieni presente che quando fai clic sull'input, focusin non viene visualizzato sull'input, ma sul nodo host stesso. È stato modificato il targeting.

Eventi sempre interrotti

I seguenti eventi non superano mai il confine dello shadow DOM:

  • abort
  • errore
  • seleziona
  • modifica
  • cardiaco
  • reimposta
  • ridimensiona
  • scroll
  • selectstart

Conclusione

Spero che concorderai sul fatto che Shadow DOM è incredibilmente potente. Per la prima volta in assoluto, abbiamo un incapsulamento adeguato senza il bagaglio aggiuntivo di <iframe> o altre tecniche precedenti.

Shadow DOM è sicuramente una bestia complessa, ma è una bestia che vale la pena aggiungere alla piattaforma web. Dedica un po' di tempo a questo aspetto. Impara. Fate domande.

Se vuoi saperne di più, consulta l'articolo introduttivo di Dominic Shadow DOM 101 e il mio articolo Shadow DOM 201: CSS & Styling.