DOM 101 shadow

Dominic Cooney
Dominic Cooney

Introduzione

I componenti web sono una serie 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.

Questo significa che devi decidere quando utilizzare HTML/JavaScript e quando usare 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 dei componenti web sono pensati per aiutarti a farlo.

Esiste però un problema fondamentale che rende difficili da usare i widget basati su HTML e JavaScript: l'albero DOM all'interno di un widget non è incapsulato dal resto della pagina. Questa mancanza di incapsulamento significa che il foglio di stile del documento potrebbe applicarsi per errore a parti all'interno del widget, che il codice JavaScript potrebbe modificare accidentalmente alcune parti all'interno del widget, che gli ID potrebbero sovrapporsi con gli 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 a cui è associata una radice shadow è chiamato host shadow. Il rendering dei contenuti di un host shadow non viene eseguito, mentre viene eseguito il rendering dei contenuti della radice shadow.

Ad esempio, se avessi un markup simile al seguente:

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

Inoltre, se JavaScript nella pagina chiede cos'è il textContent del pulsante, non risulterebbe "ねんちちチ、影の世界!", ma per "Hello, world!", perché il sottoalbero DOM sotto la radice ombra è 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".

Per prima cosa, scriviamo il 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 shadow 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 viene visualizzato il tag nome, i contenuti dell'host ombra vengono proiettati nel punto in cui viene visualizzato l'elemento <content>.

Ora la struttura del documento è più semplice perché il nome si trova in un solo posto: il documento. Se la pagina dovesse aggiornare il nome dell'utente, basta 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 raggiunto una separazione tra contenuti e presentazione. I contenuti sono nel documento, la presentazione è nel DOM shadow. Vengono automaticamente sincronizzati dal browser al momento del 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. È ancora un tag, quindi il contenuto semantico del documento non cambia:

<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 a entrambi gli elementi <content select="div"> e <content select=".email">. Quante volte viene visualizzato l'indirizzo email di Roberto 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 di contenuto è l'invito che consente ai contenuti del documento di partecipare alla festa di rendering Shadow DOM del backstage. 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 non viene 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 perché è 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. Ciò è utile quando vuoi ottenere un rendering radicalmente diverso: scrivi il modello semantico nel documento, ovvero ciò che è accessibile agli script nella pagina, ma nascondilo per scopi di rendering e collegalo a un modello di rendering molto diverso in Shadow DOM utilizzando JavaScript.

Ad esempio, HTML ha un bel selettore di date. Se scrivi <input type="date">, viene visualizzato un pratico calendario popup. E se volessi permettere all'utente di scegliere un intervallo di date per la vacanza sull'isola dei dolci (con amache realizzate con 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 creiamo Shadow DOM che usa una tabella per creare un calendario elegante che evidenzia 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>

Introduzione a You Pass Shadow DOM

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.