DOM 301 Shadow

Concetti avanzati e API DOM

In questo articolo vengono spiegate in modo più approfondito le cose straordinarie che puoi fare con Shadow DOM. Si basa sui concetti discussi in Shadow DOM 101 e Shadow DOM 201.

Utilizzo di più radici shadow

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

Vediamo cosa succede se proviamo a collegare 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>

Ciò che restituisce è "Root 2 FTW", nonostante abbiamo già collegato un albero delle ombre. Questo perché vince l'ultimo albero delle ombre aggiunto a un host. Per il rendering è uno stack LIFO. L'esame di DevTools consente di verificare questo comportamento.

Quindi a cosa servono più ombre se solo l'ultima viene invitata alla parte di rendering? Inserisci i punti di inserimento ombra.

Punti di inserimento ombra

I "punti di inserzione per le ombre" (<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 per altri alberi delle ombre. È Shadow DOM Inception!

Come puoi immaginare, le cose si complicano man mano che scavalchi la buca del coniglio. Per questo motivo, le specifiche sono molto chiare su cosa succede quando sono in gioco più elementi <shadow>:

Tornando al nostro esempio originale, la prima ombra root1 è stata lasciata fuori dall'elenco degli inviti. Se aggiungi un punto di inserzione <shadow>, verrà visualizzato di nuovo:

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

Ci sono un paio di cose interessanti su questo esempio:

  1. "Root 2 FTW" continua a essere visualizzato al di sopra di "Root 1 FTW". Ciò è dovuto al punto in cui abbiamo posizionato il punto di inserzione <shadow>. Se vuoi invertire, sposta il punto di inserimento: root2.innerHTML = '<shadow></shadow><div>Root 2 FTW</div>';.
  2. Nota che ora è presente un punto di inserimento <content> in root1. In questo modo il nodo di testo "Light DOM" verrà visualizzato per la corsa di rendering.

Cosa viene visualizzato in <shadow>?

A volte è utile sapere che l'albero ombra meno recente viene visualizzato in un <shadow>. Puoi ottenere un riferimento a tale albero tramite .olderShadowRoot:

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

Ottenere la radice shadow di un host

Se un elemento ospita un DOM shadow, puoi accedere alla 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 qualcuno entri nelle tue ombre, ridefinisci .shadowRoot in modo che sia nullo:

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

È un piccolo trucco, ma funziona. Alla fine, è importante ricordare che, sebbene incredibilmente fantastico, Shadow DOM non è stato progettato per essere una funzionalità di sicurezza. Non farvi affidamento per l'isolamento completo dei contenuti.

Creazione di un DOM shadow in JS

Se preferisci creare un DOM in JS, HTMLContentElement e HTMLShadowElement hanno un'interfaccia specifica.

<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 sto utilizzando select per estrarre <span> appena aggiunto.

Utilizzare i punti di inserzione

I nodi selezionati dall'elemento host e "distribuiti" nell'albero delle ombre sono chiamati... rullo di tamburi... nodi distribuiti. Possono attraversare il limite ombra quando i punti di inserzione li invitano.

La cosa concettualmente bizzarra dei punti di inserimento è che non muovono fisicamente il DOM. I nodi dell'host rimangono intatti. I punti di inserimento riproiettano semplicemente i nodi dell'host nell'albero delle ombre. Si tratta di un elemento 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 dello shadow DOM. Questo porta ad altre informazioni importanti:

Element.getDistributedNodes()

Non possiamo attraversare un <content>, ma l'API .getDistributedNodes() ci 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()

Analogamente a .getDistributedNodes(), puoi controllare in quali punti di inserzione è distribuito un nodo chiamando il suo .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 che è Shadow DOM è difficile. Ricordo di aver cercato di avvolgermi la testa per la prima volta.

Per capire meglio come funziona il rendering 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 provare a sperimentare per vedere come funzionano le cose e i punti di inserimento fanno scorrere i nodi host nell'albero delle ombre.

Visualizzatore DOM Shadow
Avvia il visualizzatore Shadow DOM

Provalo e fammi sapere cosa ne pensi.

Modello di evento

Alcuni eventi attraversano il limite d'ombra e altri no. Nei casi in cui gli eventi superano il confine, la destinazione dell'evento viene regolata per mantenere l'incapsulamento fornito dal limite superiore della radice ombra. In altre parole, gli eventi vengono reindirizzati in modo da sembrare che provengano dall'elemento host anziché da elementi interni al DOM Shadow.

Play Azione 1

  • Questa è interessante. Dovresti vedere un valore mouseout dall'elemento host (<div data-host>) al nodo blu. Anche se è un nodo distribuito, si trova ancora nell'host, non nello ShadowDOM. Se passi di nuovo verso il basso e diventa di nuovo giallo, viene generato un mouseout sul nodo blu.

Play Azione 2

  • Esiste un mouseout visualizzato sull'host (alla fine). Normalmente gli eventi mouseout vengono attivati per tutti i blocchi gialli. Tuttavia, in questo caso questi elementi sono interni al DOM Shadow e l'evento non viene visualizzato attraverso il limite superiore.

Play Azione 3

  • Tieni presente che quando fai clic sull'input, focusin non viene visualizzato nell'input, ma sul nodo host stesso. È stato colpito nuovamente!

Eventi sempre interrotti

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

  • abort
  • errore
  • seleziona
  • modifica
  • carico
  • reimpostazione
  • resize
  • scroll
  • selezionastart

Conclusione

Spero che sarai d'accordo sul fatto che Shadow DOM è incredibilmente potente. Per la prima volta in assoluto, abbiamo un incapsulamento corretto senza la necessità di <iframe> o altre tecniche meno recenti.

Lo shadow DOM è certamente un software complesso, ma è un bel metodo che vale la pena aggiungere alla piattaforma web. Trascorri un po' di tempo. Impara. Fate domande.

Per saperne di più, vedi l'articolo introduttivo di Dominic Shadow DOM 101 e l'articolo Shadow DOM 201: CSS & Styling.