DOM 101 shadow

Dominic Cooney
Dominic Cooney

Introduzione

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

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

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

Tuttavia, esiste un problema fondamentale che rende difficile l'utilizzo dei widget creati con 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 essere applicato accidentalmente a parti all'interno del widget, il codice JavaScript potrebbe modificare accidentalmente parti all'interno del widget, i tuoi ID potrebbero sovrapporsi agli ID all'interno del widget e così via.

I componenti web sono costituiti da tre parti:

  1. Modelli
  2. Shadow DOM
  3. Elementi personalizzati

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

Hello, Shadow World

Con Shadow DOM, agli elementi può essere associato un nuovo tipo di nodo. Questo nuovo tipo di nodo è chiamato shadow root. Un elemento a cui è associata una shadow root è chiamato shadow host. I contenuti di uno shadow host non vengono visualizzati; vengono invece visualizzati i contenuti della shadow root.

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>

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

<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 il codice JavaScript nella pagina chiede qual è il valore di textContent del pulsante, non riceverà "こんにちは、影の世界!", ma "Hello, world!" perché il sottoalbero DOM sotto la shadow root è incapsulato.

Separazione dei contenuti dalla presentazione

Ora vedremo come utilizzare Shadow DOM per separare i contenuti dalla presentazione. Supponiamo di avere questo badge con il 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. Questo è ciò che scriveresti oggi. Non utilizza 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 del nome è esposta al documento. Se altri elementi della pagina utilizzano accidentalmente gli stessi nomi di classe per lo stile o lo scripting, avremo dei problemi.

Possiamo evitare di passare un brutto momento.

Passaggio 1: nascondi i dettagli della presentazione

Semanticamente, probabilmente ci interessa solo che:

  • È un badge con il nome.
  • Il nome è "Bob".

Innanzitutto, scriviamo un markup più vicino alla semantica reale 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'unico elemento visualizzato. Poiché abbiamo spostato gli elementi DOM di presentazione all'interno di un elemento <template>, non viene eseguito il rendering, ma è possibile accedervi da JavaScript. Ora lo facciamo per compilare la shadow root:

<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 una shadow root, il tag del nome viene nuovamente visualizzato. Se fai clic con il tasto destro del mouse sul tag del nome e ispezioni l'elemento, vedrai che si tratta di un markup semantico semplice:

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

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

Passaggio 2: separa i contenuti dalla presentazione

Il nostro tag <name> ora nasconde i dettagli di presentazione dalla pagina, ma non separa effettivamente la presentazione dai contenuti, perché, sebbene i contenuti (il nome "Bob") siano nella pagina, il nome visualizzato è quello che abbiamo copiato nello shadow root. Se vogliamo cambiare il nome sul badge, dobbiamo farlo in due punti e potrebbero non essere sincronizzati.

Gli elementi HTML sono compositivi: ad esempio, puoi inserire un pulsante all'interno di una tabella. In questo caso, abbiamo bisogno della composizione: il badge deve essere una composizione dello sfondo rosso, del testo "Ciao!" e dei contenuti presenti sul badge.

In qualità di 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 dall'host shadow da presentare in quel punto.

Se modifichiamo il markup nello Shadow DOM 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 eseguito il rendering del tag name, i contenuti dello shadow host 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 deve aggiornare il nome dell'utente, basta scrivere:

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

e il gioco è fatto. Il rendering del badge con il nome viene aggiornato automaticamente dal browser, perché stiamo proiettando i contenuti del badge con il nome nella posizione con <content>.

<div id="ex2b">

Ora abbiamo ottenuto la separazione dei contenuti e della presentazione. I contenuti si trovano nel documento, mentre la presentazione si trova nello Shadow DOM. Vengono mantenuti automaticamente sincronizzati dal browser quando è il momento di eseguire il rendering di un elemento.

Passaggio 3: profitto

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

Ora, se cambiamo la nostra presentazione, non dobbiamo modificare il codice.

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

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

Il codice di configurazione della shadow root rimane invariato. Cambia solo ciò che viene inserito nello shadow root:

<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 per secondo in inglese (dopo "Hi! My name is"), ma prima in giapponese (prima di "と申します"). Questa distinzione è semanticamente priva di significato dal punto di vista dell'aggiornamento del nome visualizzato, quindi il codice di aggiornamento del nome non deve conoscere questo dettaglio.

Aggiungi un tocco in più: proiezione avanzata

Nell'esempio precedente, l'elemento <content> seleziona tutti i contenuti dall'host shadow. Utilizzando l'attributo select, puoi controllare ciò che viene proiettato 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 una shadow root 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 all'elemento <content select="div"> sia all'elemento <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 le persone che lavorano con Shadow DOM, la creazione dell'albero di ciò che viene effettivamente visualizzato sullo schermo è come una grande festa. L'elemento content è l'invito che consente ai contenuti del documento di accedere alla festa di rendering dello Shadow DOM dietro le quinte. Questi inviti vengono recapitati in ordine; chi riceve un invito dipende dalla persona a cui è indirizzato (ovvero l'attributo select). Una volta invitati, i contenuti accettano sempre l'invito (chi non lo farebbe?!) e partono. Se un invito successivo viene inviato di nuovo a quell'indirizzo, beh, non c'è nessuno in casa e non arriva alla tua festa.

Nell'esempio precedente, <div class="email"> corrisponde sia al selettore div che al selettore .email, ma poiché l'elemento dei contenuti con il selettore div si trova prima nel documento, <div class="email"> va alla festa gialla e nessuno è disponibile per andare alla festa blu. (Forse è per questo che è così triste, anche se mal comune mezzo gaudio, quindi non si sa mai).

Se un elemento non è invitato a nessuna festa, non viene visualizzato affatto. Questo è ciò che è successo al testo "Hello, world" nel primissimo esempio. Questo è utile quando vuoi ottenere un rendering radicalmente diverso: scrivi il modello semantico nel documento, che è 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 buon selettore della data. Se scrivi <input type="date">, viene visualizzato un calendario popup. Ma cosa succede se vuoi consentire all'utente di scegliere un intervallo di date per la sua vacanza su un'isola deserta (sai… con amache fatte di liquirizia rossa)? Puoi configurare il documento in questo 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 creano uno Shadow DOM che utilizza 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 ed endDate; quando l'utente invia il modulo, vengono inviati i valori di questi elementi di input.

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, anche se non è altrettanto bello. L'utente visualizza un messaggio simile al seguente:

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

Hai superato l'attività Shadow DOM 101

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

Li spieghiamo nei post successivi.