DOM 101 shadow

Dominic Cooney
Dominic Cooney

Introduzione

I componenti web sono un insieme di standard all'avanguardia che:

  1. Rendere possibile la creazione di widget
  2. …che possono essere riutilizzati in modo affidabile
  3. …e che non interrompe le pagine se la versione successiva del componente modifica i dettagli di implementazione interna.

Ciò significa che devi decidere quando utilizzare HTML/JavaScript e quando utilizzare i componenti web? No! HTML e JavaScript possono creare contenuti visivi interattivi. I widget sono elementi visivi interattivi. È sensato sfruttare le tue competenze in HTML e JavaScript quando sviluppi un widget. Gli standard Web Components sono progettati per aiutarti a farlo.

Tuttavia, esiste un problema fondamentale che rende difficili da utilizzare i widget creati con HTML e JavaScript: la struttura DOM all'interno di un widget non è incapsulata dal resto della pagina. Questa mancanza di incapsulamento significa che lo stile del documento potrebbe essere applicato accidentalmente a parti all'interno del widget; il codice JavaScript potrebbe modificare accidentalmente parti all'interno del widget; gli ID potrebbero sovrapporsi agli ID all'interno del widget; e così via.

Web Components è composto da tre parti:

  1. Modelli
  2. DOM Shadow
  3. Elementi personalizzati

Shadow DOM risolve il problema di incapsulamento dell'albero DOM. Le quattro parti di Web Components sono progettate per funzionare insieme, ma puoi anche scegliere quali parti utilizzare. Questo tutorial mostra come utilizzare Shadow DOM.

Ciao, Shadow World

Con Shadow DOM, agli elementi può essere associato un nuovo tipo di nodo. Questo nuovo tipo di nodo è chiamato elemento radice ombra. Un elemento con un'origine nascosta associata è chiamato elemento nascosto. I contenuti di un host ombra non vengono visualizzati, ma vengono visualizzati i contenuti della radice ombra.

Ad esempio, se hai un markup come questo:

<button>Hello, world!</button>
<script>
var host = document.querySelector('button');
var root = host.createShadowRoot();
root.textContent = 'こんにちは、影の世界!';
</script>

quindi anziché

<button id="ex1a">Hello, world!</button>
<script>
function remove(selector) {
  Array.prototype.forEach.call(
      document.querySelectorAll(selector),
      function (node) { node.parentNode.removeChild(node); });
}

if (!HTMLElement.prototype.createShadowRoot) {
  remove('#ex1a');
  document.write('<img src="SS1.png" alt="Screenshot of a button with \'Hello, world!\' on it.">');
}
</script>

La tua pagina ha il seguente aspetto

<button id="ex1b">Hello, world!</button>
<script>
(function () {
  if (!HTMLElement.prototype.createShadowRoot) {
    remove('#ex1b');
    document.write('<img src="SS2.png" alt="Screenshot of a button with \'Hello, shadow world!\' in Japanese on it.">');
    return;
  }
  var host = document.querySelector('#ex1b');
  var root = host.createShadowRoot();
  root.textContent = 'こんにちは、影の世界!';
})();
</script>

Non solo, se il codice JavaScript sulla pagina chiede qual è il valore textContent del pulsante, non riceverà "こんにちは、影の世界!", ma "Hello, world!" perché il sottoalbero DOM sotto l'elemento radice shadow è incapsulato.

Separare i contenuti dalla presentazione

Ora esamineremo l'utilizzo di Shadow DOM per separare i contenuti dalla presentazione. Supponiamo di avere questo tag di nome:

<style>
.ex2a.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.ex2a .boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.ex2a .name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="ex2a outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Ecco il markup. Ecco cosa scriveresti oggi. Non utilizza lo shadow DOM:

<style>
.outer {
  border: 2px solid brown;
  border-radius: 1em;
  background: red;
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
}
.boilerplate {
  color: white;
  font-family: sans-serif;
  padding: 0.5em;
}
.name {
  color: black;
  background: white;
  font-family: "Marker Felt", cursive;
  font-size: 45pt;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div>

Poiché l'albero DOM non è incapsulato, l'intera struttura del tag name è esposta al documento. Se altri elementi della pagina utilizzano accidentalmente gli stessi nomi di classe per gli stili o gli script, avremo dei problemi.

Possiamo evitare di passare un brutto momento.

Passaggio 1: nascondi i dettagli della presentazione

Dal punto di vista semantico, probabilmente ci interessa solo che:

  • È una targhetta con nome.
  • Il nome è "Bob".

Innanzitutto, scriviamo un markup più vicino alla vera semantica che vogliamo:

<div id="nameTag">Bob</div>

Poi inseriamo tutti gli stili e i div utilizzati per la presentazione in un elemento <template>:

<div id="nameTag">Bob</div>
<template id="nameTagTemplate">
<span class="unchanged"><style>
.outer {
  border: 2px solid brown;

  … same as above …

</style>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    Bob
  </div>
</div></span>
</template>

A questo punto, "Bob" è l'unica cosa visualizzata. Poiché abbiamo spostato gli elementi DOM della presentazione all'interno di un elemento <template>, non vengono visualizzati, ma è possibile accedervi da JavaScript. Lo facciamo ora per compilare la radice shadow:

<script>
var shadow = document.querySelector('#nameTag').createShadowRoot();
var template = document.querySelector('#nameTagTemplate');
var clone = document.importNode(template.content, true);
shadow.appendChild(clone);

Ora che abbiamo configurato un elemento radice ombra, il tag name viene visualizzato nuovamente. Se fai clic con il tasto destro del mouse sul tag nome e ispezioni l'elemento, vedrai che si tratta di un markup semantico:

<div id="nameTag">Bob</div>

Questo dimostra che, utilizzando Shadow DOM, abbiamo nascosto i dettagli di presentazione del tag name dal documento. I dettagli della presentazione sono incapsulati nello shadow DOM.

Passaggio 2: separa i contenuti dalla presentazione

Il nostro tag nome ora nasconde i dettagli della presentazione dalla pagina, ma non separa effettivamente la presentazione dai contenuti, perché anche se i contenuti (il nome "Bob") sono nella pagina, il nome visualizzato è quello che abbiamo copiato nell'elemento radice ombra. Se vogliamo cambiare il nome nel tag nome, dobbiamo farlo in due punti e potrebbero non essere sincronizzati.

Gli elementi HTML sono compositi: ad esempio, puoi inserire un pulsante all'interno di una tabella. La composizione è ciò che ci serve qui: il tag del nome deve essere una composizione dello sfondo rosso, del testo "Un saluto da Google" e dei contenuti presenti sul tag del nome.

Tu, l'autore del componente, definisci il funzionamento della composizione con il tuo widget utilizzando un nuovo elemento chiamato <content>. In questo modo viene creato un punto di inserimento nella presentazione del widget, che seleziona i contenuti dell'host ombra da presentare in quel punto.

Se modifichiamo il markup nel DOM ombra in questo modo:

<span class="unchanged"><template id="nameTagTemplate">
<style>
  …
</style></span>
<div class="outer">
  <div class="boilerplate">
    Hi! My name is
  </div>
  <div class="name">
    <content></content>
  </div>
</div>
<span class="unchanged"></template></span>

Quando il tag name viene visualizzato, i contenuti dell'host ombra vengono proiettati nello spazio in cui viene visualizzato l'elemento <content>.

Ora la struttura del documento è più semplice perché il nome si trova solo nel documento. Se la tua pagina deve aggiornare il nome dell'utente, devi solo scrivere:

document.querySelector('#nameTag').textContent = 'Shellie';

ed è tutto. Il rendering del tag name viene aggiornato automaticamente dal browser, perché proiettiamo i contenuti del tag name con <content>.

<div id="ex2b">

Ora abbiamo ottenuto la separazione dei contenuti e della presentazione. I contenuti sono nel documento, la presentazione è nel DOM shadow. Vengono sincronizzati automaticamente dal browser al momento di eseguire il rendering.

Passaggio 3: profitto

Separando i contenuti dalla presentazione, possiamo semplificare il codice che manipola i contenuti. Nell'esempio del tag name, il codice deve gestire solo una struttura semplice contenente un <div> anziché diversi.

Ora, se modifichiamo la presentazione, non dobbiamo modificare il codice.

Ad esempio, supponiamo di voler localizzare il tag del nome. Si tratta comunque di un tag nome, pertanto i contenuti semantici del documento non cambiano:

<div id="nameTag">Bob</div>

Il codice di configurazione dell'elemento radice ombra rimane invariato. Solo ciò che viene inserito nell'elemento radice dell'ombra cambia:

<template id="nameTagTemplate">
<style>
.outer {
  border: 2px solid pink;
  border-radius: 1em;
  background: url(sakura.jpg);
  font-size: 20pt;
  width: 12em;
  height: 7em;
  text-align: center;
  font-family: sans-serif;
  font-weight: bold;
}
.name {
  font-size: 45pt;
  font-weight: normal;
  margin-top: 0.8em;
  padding-top: 0.2em;
}
</style>
<div class="outer">
  <div class="name">
    <content></content>
  </div>
  と申します。
</div>
</template>

Si tratta di un grande miglioramento rispetto alla situazione attuale sul web, perché il codice di aggiornamento del nome può dipendere dalla struttura del componente, che è semplice e coerente. Il codice di aggiornamento del nome non deve conoscere la struttura utilizzata per il rendering. Se consideriamo ciò che viene visualizzato, il nome appare come secondo in inglese (dopo "Un saluto da Google. Mi chiamo"), ma prima in giapponese (prima di "と申します"). Questa distinzione non ha alcun significato semantico dal punto di vista dell'aggiornamento del nome visualizzato, quindi il codice di aggiornamento del nome non deve conoscere questo dettaglio.

Credito extra: proiezione avanzata

Nell'esempio precedente, l'elemento <content> seleziona tutti i contenuti dall'host ombra. Utilizzando l'attributo select, puoi controllare cosa viene visualizzato da un elemento di contenuto. Puoi anche utilizzare più elementi di contenuto.

Ad esempio, se hai un documento che contiene quanto segue:

<div id="nameTag">
  <div class="first">Bob</div>
  <div>B. Love</div>
  <div class="email">bob@</div>
</div>

e un elemento radice ombra che utilizza i selettori CSS per selezionare contenuti specifici:

<div style="background: purple; padding: 1em;">
  <div style="color: red;">
    <content **select=".first"**></content>
  </div>
  <div style="color: yellow;">
    <content **select="div"**></content>
  </div>
  <div style="color: blue;">
    <content **select=".email">**</content>
  </div>
</div>

L'elemento <div class="email"> corrisponde sia agli elementi <content select="div"> sia agli elementi <content select=".email">. Quante volte viene visualizzato l'indirizzo email di Bob e in quali colori?

La risposta è che l'indirizzo email di Bob viene visualizzato una volta ed è giallo.

Il motivo è che, come sanno gli hacker che si occupano di Shadow DOM, costruire l'albero di ciò che viene effettivamente visualizzato sullo schermo è come un grande party. L'elemento content è l'invito che consente di inserire i contenuti del documento nel party di rendering del DOM ombra dietro le quinte. Questi inviti vengono inviati in ordine; chi riceve un invita dipende a chi è indirizzato (ovvero all'attributo select). I contenuti, una volta invitati, accettano sempre l'invito (chi non lo farebbe?!) e vengono pubblicati. Se un invito successivo viene inviato di nuovo a quell'indirizzo, vuol dire che non c'è nessuno in casa e che non verrà alla tua festa.

Nell'esempio precedente, <div class="email"> corrisponde sia al selettore div sia al selettore .email, ma poiché l'elemento di contenuto con il selettore div compare prima nel documento, <div class="email"> viene assegnato al gruppo giallo e nessuno è disponibile per il gruppo blu. (Forse è per questo che è così blu, anche se la sofferenza ama la compagnia, quindi non si sa mai.)

Se un elemento non viene invitato a nessun gruppo, non viene visualizzato. È quello che è successo al testo "Hello, world" nel primo esempio. Questo è utile quando vuoi ottenere un rendering radicalmente diverso: scrivi il modello semantico nel documento, che è accessibile agli script nella pagina, ma nascondilo ai fini del rendering e collegalo a un modello di rendering molto diverso nel DOM ombra utilizzando JavaScript.

Ad esempio, HTML ha un bel selettore di date. Se scrivi <input type="date">, viene visualizzato un pratico calendario popup. Ma cosa succede se vuoi consentire all'utente di scegliere un intervallo di date per la sua vacanza sull'isola con dessert (sai, con amache fatte di Red Vines). Configura il documento nel seguente modo:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

ma crea un DOM ombra che utilizza una tabella per creare un calendario elegante che mette in evidenza l'intervallo di date e così via. Quando l'utente fa clic sui giorni nel calendario, il componente aggiorna lo stato negli input startDate e endDate. Quando l'utente invia il modulo, i valori di questi elementi di input vengono inviati.

Perché ho incluso le etichette nel documento se non verranno visualizzate? Il motivo è che se un utente visualizza il modulo con un browser che non supporta Shadow DOM, il modulo è comunque utilizzabile, ma non è così piacevole. L'utente vede qualcosa di simile a quanto segue:

<div class="dateRangePicker">
  <label for="start">Start:</label>
  <input type="date" name="startDate" id="start">
  <br>
  <label for="end">End:</label>
  <input type="date" name="endDate" id="end">
</div>

You Pass Shadow DOM 101

Queste sono le nozioni di base dello shadow DOM. Hai superato il corso introduttivo allo shadow DOM. Puoi fare di più con Shadow DOM, ad esempio puoi utilizzare più elementi shadow su un elemento host shadow o elementi shadow nidificati per l'incapsulamento oppure puoi progettare la tua pagina utilizzando le visualizzazioni basate su modelli (MDV) e Shadow DOM. Inoltre, i componenti web non sono solo shadow DOM.

Li spiegheremo nei post successivi.