DOM 101 shadow

Dominic Cooney
Dominic Cooney

Introduzione

Web components offre una serie di standard all'avanguardia che:

  1. Consenti di creare widget
  2. ...che possono essere riutilizzati in modo affidabile
  3. ...e che non causano interruzioni se la versione successiva del componente modifica i dettagli di implementazione interni.

Questo significa che devi decidere quando e 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. È utile sfruttare le tue competenze in HTML e JavaScript per sviluppare un widget. Gli standard Web Componenti sono pensati per aiutarti.

Tuttavia, esiste un problema fondamentale che rende difficili da usare 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 il foglio di stile del documento potrebbe essere applicato accidentalmente alle parti all'interno del widget; il codice JavaScript potrebbe modificare accidentalmente le parti all'interno del widget; i tuoi ID potrebbero sovrapporsi a quelli all'interno del widget e così via.

I componenti web sono costituiti da tre parti:

  1. Modelli
  2. DOM shadow
  3. Elementi personalizzati

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

Hello, Shadow World

Con il DOM shadow, agli elementi è possibile associare un nuovo tipo di nodo. Questo nuovo tipo di nodo è chiamato radice shadow. Un elemento a cui è associata una radice shadow è chiamato shadow host. Il contenuto di un host shadow non viene visualizzato, al contrario dei contenuti della radice shadow.

Ad esempio, se avevi un markup come questo:

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

invece di

<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 l'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 qual è il valore textContent del pulsante, non si otterrà "世界!", ma "Hello, world!" perché il sottoalbero DOM sotto la radice ombra è incapsulato.

Separazione dei contenuti dalla presentazione

Ora vedremo l'uso dello Shadow DOM per separare i contenuti dalla presentazione. Supponiamo di avere questo tag:

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

Questo è il markup. Questo è quello che scriveresti oggi. Non usa 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 nome è esposta al documento. Se altri elementi sulla pagina usano accidentalmente gli stessi nomi di classe per lo stile o lo script, non possiamo avere problemi.

Possiamo evitare di incorrere in un brutto momento.

Passaggio 1: nascondi i dettagli della presentazione

Dal punto di vista semantico, probabilmente interessa solo che:

  • È un tag.
  • Il nome è "Bob".

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

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

Quindi mettiamo 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 che viene visualizzata. Poiché abbiamo spostato gli elementi DOM di presentazione all'interno di un elemento <template>, non ne viene eseguito il rendering, 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 impostato una radice shadow, il tag nome viene visualizzato di nuovo. Se facessi clic con il tasto destro del mouse sul tag nome e controlli l'elemento, scopri che si tratta di un markup semantico semplice:

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

Ciò dimostra che, utilizzando Shadow DOM, abbiamo nascosto i dettagli della presentazione del tag nome dal documento. I dettagli della presentazione sono incapsulati nel DOM Shadow.

Passaggio 2: separa i contenuti dalla presentazione

Il nostro tag nasconde i dettagli della presentazione nella pagina, ma in realtà non separa la presentazione dai contenuti, perché sebbene il contenuto (il nome "Bob") sia presente nella pagina, il nome visualizzato è quello copiato nella radice shadow. Se vogliamo cambiare il nome del tag, dobbiamo farlo in due punti e i tag potrebbero non essere sincronizzati.

Gli elementi HTML sono compositivi: ad esempio, puoi inserire un pulsante all'interno di una tabella. La composizione è ciò di cui abbiamo bisogno qui: il tag deve essere una composizione dello sfondo rosso, del testo "Ciao!" e del contenuto presente sul tag.

In qualità di autore del componente, definisci il funzionamento della composizione con il tuo widget utilizzando un nuovo elemento chiamato <content>. Viene creato un punto di inserimento nella presentazione del widget e il punto di inserzione seleziona i contenuti dall'host shadow per presentare in quel punto.

Se cambiamo 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, il contenuto dell'host shadow viene proiettato 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 deve aggiornare il nome dell'utente, basta scrivere:

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

e il gioco è fatto. Il rendering del tag viene aggiornato automaticamente dal browser perché stiamo proiettando il contenuto del tag in modo che sia presente con <content>.

<div id="ex2b">

Ora abbiamo raggiunto la separazione dei contenuti e della presentazione. I contenuti sono nel documento; la presentazione si trova nel DOM shadow. che vengono automaticamente sincronizzati dal browser al momento del rendering.

Passaggio 3. Profitto

Separando i contenuti e la presentazione, possiamo semplificare il codice che manipola i contenuti: nell'esempio del tag nome, quel codice deve gestire solo una struttura semplice contenente un elemento <div> anziché diversi.

Ora, se modifichiamo la presentazione, non c'è bisogno di modificare il codice.

Ad esempio, supponiamo di voler localizzare il nostro tag. È ancora un tag, quindi il contenuto semantico del documento non cambia:

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

Il codice di configurazione radice shadow rimane invariato. Ciò che viene inserito nella radice shadow 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 notevole miglioramento rispetto alla situazione attuale del web, perché il codice di aggiornamento del nome può dipendere dalla struttura del componente, che è semplice e coerente. Il codice di aggiornamento dei nomi non deve necessariamente conoscere la struttura utilizzata per il rendering. Se consideriamo i contenuti visualizzati, il nome appare secondo in inglese (dopo "Ciao! "Il mio nome è"), ma prima in giapponese (prima di "と申ちす"). Questa distinzione è semanticamente priva di significato se si tratta di aggiornare il nome visualizzato, pertanto il codice di aggiornamento del nome non deve essere a conoscenza di questi dettagli.

Credito aggiuntivo: proiezione avanzata

Nell'esempio precedente, l'elemento <content> chery-sceglie tutti i contenuti dall'host shadow. Utilizzando l'attributo select, puoi controllare i progetti di un elemento di contenuti. Puoi anche utilizzare più elementi di contenuto.

Ad esempio, se hai un documento che contiene:

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

e una radice shadow 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 da 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 Roberto viene visualizzato una sola volta ed è giallo.

Il motivo è che, come sanno chi hacker su Shadow DOM, costruire l'albero di ciò che viene effettivamente visualizzato sullo schermo è come una festa enorme. L'elemento dei contenuti è l'invito che consente ai contenuti del documento di accedere al party di rendering Shadow DOM del backstage. Questi inviti vengono recapitati in ordine, a seconda di chi riceve l'invito (ovvero, l'attributo select). I contenuti, una volta invitati, accettano sempre l'invito (chi non lo fa?!) e li abbandonano. Se un invito successivo viene inviato di nuovo a quell'indirizzo, beh, nessuno è in casa e non arriva alla tua festa.

Nell'esempio precedente, <div class="email"> corrisponde sia al selettore div sia al selettore .email ma, poiché l'elemento dei contenuti con il selettore div è precedente nel documento, <div class="email"> va alla parte gialla e nessuno è disponibile per la parte blu. (Questo potrebbe essere il perché è così blu, anche se alla miseria ama la compagnia, quindi non lo sai mai.)

Se un elemento viene invitato nessuna parte, non viene sottoposto a rendering. Questo è ciò che è successo al testo "Hello World" nel primo esempio. Ciò è utile quando vuoi ottenere un rendering radicalmente diverso: scrivi il modello semantico nel documento, accessibile agli script nella pagina, ma nascondilo ai fini del rendering e collegalo a un modello di rendering davvero diverso in Shadow DOM utilizzando JavaScript.

Ad esempio, HTML ha un buon selettore della data. Se scrivi <input type="date">, si aprirà un calendario popup ordinato. E se volessi far scegliere all'utente un intervallo di date per la sua vacanza sull'isola del dessert (sai... con amache fatte con le viti rosse). Puoi configurare 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 create un DOM shadow che usa una tabella per creare un calendario fluido che evidenzia l'intervallo di date e così via. Quando l'utente fa clic sui giorni del calendario, il componente aggiorna lo stato negli input startDate ed endDate. Quando l'utente invia il modulo, vengono inviati i valori di questi elementi di input.

Perché ho incluso delle etichette nel documento se non devono essere 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ì bello. L'utente vede qualcosa del tipo:

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

Passi l'ombra DOM 101

Queste sono le basi di Shadow DOM: hai superato il DOM 101! Puoi fare di più con Shadow DOM. Ad esempio, puoi utilizzare più shadow su un singolo host shadow, oppure ombre nidificate per l'incapsulamento oppure puoi progettare la tua pagina utilizzando le viste basate su modelli (MDV) e Shadow DOM. I componenti web non sono soltanto Shadow DOM.

Li spiegheremo nei post successivi.