DOM shadow dichiarativo

Lo shadow DOM dichiarativo è una funzionalità standard della piattaforma web, supportata in Chrome a partire dalla versione 90. Tieni presente che le specifiche per questa funzionalità sono cambiate nel 2023 (inclusa la ridenominazione di shadowroot in shadowrootmode) e che le versioni standardizzate più aggiornate di tutte le parti della funzionalità sono disponibili nella versione 124 di Chrome.

Supporto dei browser

  • Chrome: 111.
  • Edge: 111.
  • Firefox: 123.
  • Safari: 16.4.

Origine

Shadow DOM è uno dei tre standard dei componenti web, completato da modelli HTML ed elementi personalizzati. Shadow DOM consente di applicare gli stili CSS a un sottoalbero DOM specifico e di isolare questo sottoalbero dal resto del documento. L'elemento <slot> offre un modo per controllare dove inserire gli elementi secondari di un elemento personalizzato all'interno del relativo albero ombra. La combinazione di queste caratteristiche consente di creare un sistema per la creazione di componenti autonomi e riutilizzabili che si integrano perfettamente con le applicazioni esistenti proprio come un elemento HTML integrato.

Finora, l'unico modo per utilizzare Shadow DOM era creare una radice shadow utilizzando JavaScript:

const host = document.getElementById('host');
const shadowRoot = host.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>';

Un'API imperativa come questa funziona bene per il rendering lato client: gli stessi moduli JavaScript che definiscono i nostri elementi personalizzati creano anche le relative directory shadow e ne impostano i contenuti. Tuttavia, molte applicazioni web devono eseguire il rendering dei contenuti lato server o in formato HTML statico in fase di creazione. Questo può essere un aspetto importante per offrire un'esperienza ragionevole ai visitatori che potrebbero non essere in grado di eseguire JavaScript.

Le giustificazioni per il rendering lato server (SSR) variano in base al progetto. Per rispettare le linee guida sull'accessibilità, alcuni siti web devono fornire codice HTML sottoposto a rendering completamente funzionante dal server, altri scelgono di offrire un'esperienza senza JavaScript di base come metodo per garantire buone prestazioni su connessioni o dispositivi lenti.

In passato, è stato difficile utilizzare Shadow DOM in combinazione con il rendering lato server perché non esisteva un modo integrato per esprimere le radici shadow nel codice HTML generato dal server. Esistono anche implicazioni sul rendimento quando si collegano le radici ombre agli elementi DOM che sono già stati visualizzati senza di esse. Ciò può causare uno spostamento del layout dopo il caricamento della pagina o mostrare temporaneamente un lampo di contenuti senza stile (&quot;FOUC&quot;) durante il caricamento degli stili dell&#39;elemento radice ombra.

Il DOM shadow dichiarativo (DSD) rimuove questa limitazione, portando Shadow DOM sul server.

Come creare una radice ombra dichiarativa

Una radice ombra dichiarativa è un elemento <template> con un attributo shadowrootmode:

<host-element>
  <template shadowrootmode="open">
    <slot></slot>
  </template>
  <h2>Light content</h2>
</host-element>

Un elemento modello con l'attributo shadowrootmode viene rilevato dall'analizzatore sintattico HTML e applicato immediatamente come radice ombra dell'elemento principale. Il caricamento del markup HTML puro dagli esempi di esempio precedenti genera la seguente struttura DOM:

<host-element>
  #shadow-root (open)
  <slot>
    ↳
    <h2>Light content</h2>
  </slot>
</host-element>

Questo esempio di codice segue le convenzioni del riquadro Elementi shadow di Chrome DevTools per la visualizzazione dei contenuti DOM shadow. Ad esempio, il carattere rappresenta i contenuti light DOM con slot.

Questo ci offre i vantaggi dell'incapsulamento e della proiezione degli slot di Shadow DOM in HTML statico. Non è necessario alcun codice JavaScript per produrre l'intero albero, inclusa la radice ombra.

Idratazione dei componenti

Lo Shadow DOM dichiarativo può essere utilizzato da solo per incapsulare gli stili o personalizzare il posizionamento figlio, ma è più efficace se utilizzato con Elementi personalizzati. Per i componenti creati utilizzando Elementi personalizzati viene eseguito automaticamente l'upgrade da HTML statico. Con l'introduzione di DOM ombreggiato dichiarativo, ora è possibile che un elemento personalizzato abbia un'origine ombreggiata prima dell'upgrade.

Un elemento personalizzato di cui viene eseguito l'upgrade da HTML e che include un'origine ombreggiata dichiarativa avrà già quest'origine ombreggiata collegata. Ciò significa che l'elemento avrà già una proprietà shadowRoot disponibile al momento dell'inizializzazione, senza che il codice ne crei una esplicitamente. È meglio controllare this.shadowRoot per verificare la presenza di eventuali root shadow esistenti nel costruttore dell'elemento. Se esiste già un valore, il codice HTML di questo componente include una radice ombra dichiarativa. Se il valore è nullo, nell'HTML non era presente alcuna radice shadow dichiarativa oppure il browser non supporta il DOM shadow dichiarativo.

<menu-toggle>
  <template shadowrootmode="open">
    <button>
      <slot></slot>
    </button>
  </template>
  Open Menu
</menu-toggle>
<script>
  class MenuToggle extends HTMLElement {
    constructor() {
      super();

      // Detect whether we have SSR content already:
      if (this.shadowRoot) {
        // A Declarative Shadow Root exists!
        // wire up event listeners, references, etc.:
        const button = this.shadowRoot.firstElementChild;
        button.addEventListener('click', toggle);
      } else {
        // A Declarative Shadow Root doesn't exist.
        // Create a new shadow root and populate it:
        const shadow = this.attachShadow({mode: 'open'});
        shadow.innerHTML = `<button><slot></slot></button>`;
        shadow.firstChild.addEventListener('click', toggle);
      }
    }
  }

  customElements.define('menu-toggle', MenuToggle);
</script>

Gli elementi personalizzati sono disponibili da un po' di tempo e finora non c'era motivo di verificare la presenza di un'origine ombreggiata esistente prima di crearne una utilizzando attachShadow(). Lo Shadow DOM dichiarativo include una piccola modifica che consente il funzionamento dei componenti esistenti nonostante ciò: la chiamata del metodo attachShadow() su un elemento con una radice shadow dichiarativa esistente non genererà un errore. Al contrario, l'elemento radice dell'ombra dichiarativa viene svuotato e restituito. Ciò consente ai componenti meno recenti non creati per il DOM dichiarativo di continuare a funzionare, poiché le radici dichiarative vengono conservate fino a quando non viene creata una sostituzione imperativa.

Per gli elementi personalizzati appena creati, una nuova proprietà ElementInternals.shadowRoot fornisce un modo esplicito per ottenere un riferimento alla radice ombra dichiarativa esistente di un elemento, sia aperta che chiusa. Questa opzione può essere utilizzata per verificare la presenza di qualsiasi radice ombra dichiarativa e utilizzarla, ma può essere utilizzata a partire da attachShadow() nei casi in cui non ne sia stata fornita una.

class MenuToggle extends HTMLElement {
  constructor() {
    super();

    const internals = this.attachInternals();

    // check for a Declarative Shadow Root:
    let shadow = internals.shadowRoot;

    if (!shadow) {
      // there wasn't one. create a new Shadow Root:
      shadow = this.attachShadow({
        mode: 'open'
      });
      shadow.innerHTML = `<button><slot></slot></button>`;
    }

    // in either case, wire up our event listener:
    shadow.firstChild.addEventListener('click', toggle);
  }
}

customElements.define('menu-toggle', MenuToggle);

Un'ombra per radice

Un elemento radice ombra dichiarativo è associato solo al relativo elemento principale. Ciò significa che le radici shadow sono sempre colocate con l'elemento associato. Questa decisione di progettazione garantisce che le radici shadow possano essere trasmesse in streaming come il resto di un documento HTML. È anche pratico per la creazione e la generazione, poiché l'aggiunta di un'origine ombreggiata a un elemento non richiede la gestione di un registry delle origini ombreggiate esistenti.

Il compromesso dell'associazione delle radici shadow con il relativo elemento principale è che non è possibile inizializzare più elementi dallo stesso elemento shadow dichiarativo <template>. Tuttavia, è improbabile che questo sia importante nella maggior parte dei casi in cui viene utilizzato il DOM shadow dichiarativo, poiché i contenuti di ogni radice ombra sono raramente identici. Sebbene il codice HTML visualizzato dal server contenga spesso strutture di elementi ripetute, i relativi contenuti sono generalmente diversi, ad esempio lievi variazioni nel testo o negli attributi. Poiché i contenuti di una radice ombra dichiarativa serializzata sono completamente statici, l'upgrade di più elementi da una singola radice ombra dichiarativa funzionerebbe solo se gli elementi risultavano identici. Infine, l'impatto di radici shadow simili ripetute sulle dimensioni di trasferimento di rete è relativamente ridotto a causa degli effetti della compressione.

In futuro potrebbe essere possibile rivedere le radici shadow condivise. Se il DOM supporta i modelli integrati, gli elementi shadow root dichiarativi potrebbero essere trattati come modelli che vengono istituite per costruire l'elemento shadow root di un determinato elemento. L'attuale design dichiarativo del DOM shadow consente di avere questa possibilità in futuro limitando l'associazione della radice ombra a un singolo elemento.

Lo streaming è fantastico

L'associazione delle radici ombre declarative direttamente all'elemento principale semplifica il processo di upgrade e di allacciamento a quell'elemento. Le radici ombre dichiarative vengono rilevate durante l'analisi HTML e collegate immediatamente quando viene rilevato il tag <template> di apertura. Il codice HTML analizzato all'interno di <template> viene analizzato direttamente nell'elemento radice ombra, quindi può essere "trasmesso in streaming": viene visualizzato man mano che viene ricevuto.

<div id="el">
  <script>
    el.shadowRoot; // null
  </script>

  <template shadowrootmode="open">
    <!-- shadow realm -->
  </template>

  <script>
    el.shadowRoot; // ShadowRoot
  </script>
</div>

Solo parser

Il DOM shadow dichiarativo è una funzionalità dell'interprete HTML. Ciò significa che un'origine animata dichiarativa verrà analizzata e collegata solo per i tag <template> con un attributo shadowrootmode presenti durante l'analisi HTML. In altre parole, le radici delle ombre dichiarative possono essere create durante l'analisi HTML iniziale:

<some-element>
  <template shadowrootmode="open">
    shadow root content for some-element
  </template>
</some-element>

L'impostazione dell'attributo shadowrootmode di un elemento <template> non ha alcun effetto e il modello rimane un normale elemento del modello:

const div = document.createElement('div');
const template = document.createElement('template');
template.setAttribute('shadowrootmode', 'open'); // this does nothing
div.appendChild(template);
div.shadowRoot; // null

Per evitare alcuni importanti aspetti di sicurezza, le radici ombreggiate dichiarative non possono essere create utilizzando API di analisi del frammento come innerHTML o insertAdjacentHTML(). L'unico modo per analizzare l'HTML con le radici ombra dichiarative applicate è utilizzare setHTMLUnsafe() o parseHTMLUnsafe():

<script>
  const html = `
    <div>
      <template shadowrootmode="open"></template>
    </div>
  `;
  const div = document.createElement('div');
  div.innerHTML = html; // No shadow root here
  div.setHTMLUnsafe(html); // Shadow roots included
  const newDocument = Document.parseHTMLUnsafe(html); // Also here
</script>

Rendering del server con stile

I fogli di stile in linea ed esterni sono completamente supportati all'interno delle radici ombreggiate dichiarative utilizzando i tag <style> e <link> standard:

<nineties-button>
  <template shadowrootmode="open">
    <style>
      button {
        color: seagreen;
      }
    </style>
    <link rel="stylesheet" href="/comicsans.css" />
    <button>
      <slot></slot>
    </button>
  </template>
  I'm Blue
</nineties-button>

Gli stili specificati in questo modo sono anche altamente ottimizzati: se lo stesso foglio di stile è presente in più origini ombre declarative, viene caricato e analizzato solo una volta. Il browser utilizza un singolo CSSStyleSheet di backup condiviso da tutte le root shadow, eliminando l'overhead della memoria duplicata.

I fogli di stile costruibili non sono supportati nel DOM dichiarativo shadow. Questo perché, al momento, non è possibile serializzare gli stili CSS componibili in HTML e non è possibile fare riferimento a questi stili durante il completamento di adoptedStyleSheets.

Come evitare il lampo di contenuti senza stile

Un potenziale problema nei browser che non supportano ancora il DOM dichiarativo è quello di evitare il "lampo di contenuti senza stile" (FOUC), in cui vengono mostrati i contenuti non elaborati per gli elementi personalizzati di cui non è stato ancora eseguito l'upgrade. Prima del DOM shadow dichiarativo, una tecnica comune per evitare il FOUC era applicare una regola di stile display:none agli elementi personalizzati che non sono stati ancora caricati, poiché non è stato collegato e compilato il relativo elemento shadow root. In questo modo, i contenuti non vengono visualizzati finché non sono "pronti":

<style>
  x-foo:not(:defined) > * {
    display: none;
  }
</style>

Con l'introduzione del DOM ombra dichiarativo, gli elementi personalizzati possono essere visualizzati o creati in HTML in modo che i relativi contenuti ombra siano in posizione e pronti prima del caricamento dell'implementazione del componente lato client:

<x-foo>
  <template shadowrootmode="open">
    <style>h2 { color: blue; }</style>
    <h2>shadow content</h2>
  </template>
</x-foo>

In questo caso, la regola "FOUC" display:none impedirebbe la visualizzazione dei contenuti della radice shadow dichiarativa. Tuttavia, la rimozione di questa regola causerebbe la visualizzazione di contenuti errati o senza stile nei browser senza supporto del DOM shadow dichiarativo finché il polyfill del DOM shadow dichiarativo non viene caricato e converte il modello di root shadow in un root shadow reale.

Fortunatamente, questo problema può essere risolto in CSS modificando la regola di stile FOUC. Nei browser che supportano il DOM shadow dichiarativo, l'elemento <template shadowrootmode> viene convertito immediatamente in un'origine shadow, senza lasciare elementi <template> nella struttura DOM. I browser che non supportano il DOM dichiarativo shadow conservano l'elemento <template>, che possiamo utilizzare per impedire il FOUC:

<style>
  x-foo:not(:defined) > template[shadowrootmode] ~ *  {
    display: none;
  }
</style>

Anziché nascondere l'elemento personalizzato non ancora definito, la regola "FOUC" aggiornata nasconde i relativi elementi secondari quando seguono un elemento <template shadowrootmode>. Una volta definito l'elemento personalizzato, la regola non corrisponde più. La regola viene ignorata nei browser che supportano il DOM ombra dichiarativo perché l'elemento secondario <template shadowrootmode> viene rimosso durante l'analisi del codice HTML.

Rilevamento delle funzionalità e supporto del browser

Il DOM shadow dichiarativo è disponibile da Chrome 90 ed Edge 91, ma utilizzava un attributo non standard precedente chiamato shadowroot anziché l'attributo standardizzato shadowrootmode. L'attributo shadowrootmode e il comportamento di streaming più recenti sono disponibili in Chrome 111 ed Edge 111.

Essendo una nuova API della piattaforma web, il dichiarativo Shadow DOM non dispone ancora di un supporto diffuso in tutti i browser. Il supporto del browser può essere rilevato controllando l'esistenza di una proprietà shadowRootMode sul prototipo di HTMLTemplateElement:

function supportsDeclarativeShadowDOM() {
  return HTMLTemplateElement.prototype.hasOwnProperty('shadowRootMode');
}

Polyfill

Creare un polyfill semplificato per il DOM dichiarativo di shadow è relativamente semplice, dal momento che un polyfill non deve replicare perfettamente la semantica dei tempi o le caratteristiche solo dell'analizzatore sintattico che interessano l'implementazione del browser. Per eseguire il polyfill di Declarative Shadow DOM, possiamo eseguire la scansione del DOM per trovare tutti gli elementi <template shadowrootmode>, quindi convertirli in radici shadow collegate nell'elemento principale. Questo processo può essere eseguito una volta che il documento è pronto o attivato da eventi più specifici come i cicli di vita degli elementi personalizzati.

(function attachShadowRoots(root) {
  root.querySelectorAll("template[shadowrootmode]").forEach(template => {
    const mode = template.getAttribute("shadowrootmode");
    const shadowRoot = template.parentNode.attachShadow({ mode });

    shadowRoot.appendChild(template.content);
    template.remove();
    attachShadowRoots(shadowRoot);
  });
})(document);

Per approfondire