DOM 301 Shadow

Concetti avanzati e API DOM

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

Utilizzo di più origini shadow

Se organizzi una festa, l'atmosfera diventa pesante se tutti sono ammassati nella stessa stanza. Vuoi avere la possibilità di distribuire gruppi di persone in più stanze. Anche gli elementi che ospitano il DOM ombra possono farlo, ovvero possono ospitare più di un elemento ombra alla volta.

Vediamo cosa succede se proviamo ad associare più radici shadow 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 visualizzato "Root 2 FTW", nonostante avessimo già allegato un albero ombra. Questo accade perché l'ultimo albero ombra aggiunto a un host ha la precedenza. Si tratta di una pila LIFO per quanto riguarda il rendering. L'esame di DevTools conferma questo comportamento.

Quindi, a cosa serve utilizzare più ombre se solo l'ultima è invitata al rendering? Inserisci i punti di inserimento dell'ombra.

Punti di inserzione in ombra

I "punti di inserzione in ombra" (<shadow>) sono simili ai normali punti di inserzione (<content>) in quanto sono segnaposto. Tuttavia, anziché essere segnaposto per i contenuti di un host, sono host di altri alberi delle ombre. È l'Inception del DOM Shadow.

Come puoi immaginare, le cose si complicano man mano che scendi più in profondità. Per questo motivo, le specifiche sono molto chiare su cosa succede quando sono presenti più elementi <shadow>:

Tornando al nostro esempio originale, il primo utente ombra root1 non è stato incluso nell'elenco di invitati. Aggiungendo un punto di inserzione <shadow>, il testo viene ripristinato:

<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 due aspetti interessanti:

  1. "Root 2 FTW" viene ancora visualizzato sopra "Root 1 FTW". Questo accade perché abbiamo posizionato il punto di inserzione <shadow>. Se vuoi l'effetto contrario, sposta il punto di inserzione: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';.
  2. Ora noterai che in root1 è presente un punto di inserzione <content>. In questo modo, il nodo di testo "DOM leggero" viene incluso nel rendering.

Cosa viene visualizzato in <shadow>?

A volte è utile conoscere l'albero delle ombre precedente visualizzato in <shadow>. Puoi ottenere un riferimento a quell'albero tramite .olderShadowRoot:

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

Ottenere l'origine ombra di un host

Se un elemento ospita Shadow DOM, puoi accedere al suo elemento radice shadow 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 entrino nelle tue ombre, ridefinisci .shadowRoot come null:

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

Un po' di hacking, ma funziona. Infine, è importante ricordare che, anche se è incredibilmente fantastico, Shadow DOM non è stato progettato per essere una funzionalità di sicurezza. Non fare affidamento su questa opzione per un isolamento completo dei contenuti.

Creazione di shadow DOM in JS

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

<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 nella sezione precedente. L'unica differenza è che ora utilizzo select per estrarre il nuovo <span>.

Utilizzo dei punti di inserzione

I nodi selezionati dall'elemento host e "distribuiti" nell'albero ombra si chiamano…rullino di tamburi…nodi distribuiti. Possono attraversare il confine dell'ombra quando i punti di inserzione li invitano.

Ciò che è concettualmente strano sui punti di inserzione è che non muovono fisicamente il DOM. I nodi dell'host rimangono invariati. I punti di inserzione si limitano a riprodurre i nodi dall'host nell'albero ombra. È un problema 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. h2 non è un elemento secondario del DOM ombra. Questo ci porta a un altro suggerimento:

Element.getDistributedNodes()

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

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

Come per .getDistributedNodes(), puoi controllare i punti di inserimento in cui è 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: visualizzatore DOM shadow

Comprendere la magia nera che è lo shadow DOM è difficile. Ricordo di aver provato a capirlo per la prima volta.

Per aiutarti a visualizzare il funzionamento del rendering del DOM ombra, ho creato uno strumento utilizzando d3.js. Entrambe le caselle di markup sul lato sinistro sono modificabili. Non esitare a incollare il tuo markup e a fare esperimenti per vedere come funzionano le cose e come i punti di inserzione svuotano i nodi host nell'albero ombra.

Visualizzatore DOM shadow
Avvia lo strumento di visualizzazione del DOM ombra

Provalo e fammi sapere cosa ne pensi.

Modello evento

Alcuni eventi attraversano il confine dell'ombra, 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 dell'elemento radice ombra. In altre parole, gli eventi vengono scelti come target in modo da sembrare provenienti dall'elemento host anziché dagli elementi interni dello shadow DOM.

Play Action 1

  • Questa è interessante. Dovresti vedere un mouseout dall'elemento host (<div data-host>) al nodo blu. Anche se è un node distribuito, si trova ancora nell'host, non nello ShadowDOM. Se passi il mouse più in basso nel giallo, viene visualizzato nuovamente un mouseout sul nodo blu.

Riproduci Azione 2

  • Esiste un mouseout visualizzato sull'host (alla fine). Normalmente, vedresti attivare gli eventi mouseout per tutti i blocchi gialli. Tuttavia, in questo caso questi elementi sono interni allo shadow DOM e l'evento non viene visualizzato tramite il relativo confine superiore.

Gioca a Azione 3

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

Eventi sempre fermi

I seguenti eventi non superano mai il confine dell'ombra:

  • abort
  • errore
  • seleziona
  • modifica
  • load
  • reimposta
  • resize
  • scroll
  • selectstart

Conclusione

Spero che tu concordi sul fatto che Shadow DOM è incredibilmente potente. Per la prima volta, abbiamo un'encapsulazione corretta senza il bagaglio extra dei <iframe> o di altre tecniche meno recenti.

Shadow DOM è certamente un elemento complesso, ma vale la pena aggiungerlo alla piattaforma web. Dedicaci un po' di tempo. Impara. Fate domande.

Per saperne di più, consulta l'articolo introduttivo di Dominic Shadow DOM 101 e il mio articolo Shadow DOM 201: CSS e stili.