Shadow DOM v1 - Componenti web autonomi

Shadow DOM consente agli sviluppatori web di creare DOM e CSS compartimentati per i componenti web

Riepilogo

Shadow DOM rimuove la fragilità della creazione di app web. Questa fragilità è dovuta alla natura globale di HTML, CSS e JS. Nel corso degli anni abbiamo inventato un numero esorbiente tools per aggirare i problemi. Ad esempio, quando utilizzi un nuovo ID o una nuova classe HTML, non saprai se sarà in conflitto con un nome esistente utilizzato dalla pagina. Crescono piccoli bug, la specificità dei CSS diventa un problema enorme (!important del tutto!), i selettori di stile crescono fuori controllo e le prestazioni possono risentirne. L'elenco continua.

Shadow DOM corregge CSS e DOM. Introducono stili con ambito alla piattaforma web. Senza strumenti o convenzioni di denominazione, puoi includere il codice CSS con markup, nascondere i dettagli di implementazione e creare componenti indipendenti in JavaScript vanilla.

Introduzione

Shadow DOM è uno dei tre standard dei componenti web: Modelli HTML, Shadow DOM e Elementi personalizzati. Le importazioni HTML in precedenza facevano parte dell'elenco, ma ora sono considerate deprecate.

Non è necessario creare i componenti web che utilizzano shadow DOM. Così facendo, potrai sfruttare i vantaggi offerti (ambito CSS, incapsulamento DOM, composizione) e creare elementi personalizzati riutilizzabili, che sono resilienti, altamente configurabili ed estremamente riutilizzabili. Se gli elementi personalizzati consentono di creare un nuovo codice HTML (con un'API JS), lo shadow DOM è il modo in cui fornisci i relativi HTML e CSS. Le due API si combinano per creare un componente con HTML, CSS e JavaScript autonomo.

Shadow DOM è progettato come strumento per la creazione di app basate su componenti. Pertanto, offre soluzioni per problemi comuni di sviluppo web:

  • DOM isolato: il DOM di un componente è autonomo (ad es. document.querySelector() non restituirà nodi nel DOM shadow del componente).
  • CSS con ambito: il CSS definito all'interno dello shadow DOM ha come ambito questo. Le regole di stile permettono di applicare gli stili di pagina.
  • Composizione: progetta un'API dichiarativa basata su markup per il componente.
  • Semplifica il CSS: il DOM con ambito significa che puoi utilizzare selettori CSS semplici, nomi di classi o ID più generici, senza preoccuparti di conflitti di denominazione.
  • Produttività: pensa alle app in blocchi di DOM anziché in un'unica pagina (globale) di grandi dimensioni.

Demo di fancy-tabs

In questo articolo, farò riferimento a un componente demo (<fancy-tabs>) e ai relativi snippet di codice. Se il tuo browser supporta le API, dovresti vedere una demo dal vivo appena sotto. In alternativa, consulta il codice sorgente completo su GitHub.

Visualizza il codice sorgente su GitHub

Che cos'è lo shadow DOM?

Background sul DOM

Il linguaggio HTML è alla base del web perché è facile da utilizzare. Dichiarando alcuni tag, potrai creare una pagina in pochi secondi che abbia sia presentazione che struttura. Tuttavia, di per sé il codice HTML non è molto utile. Gli esseri umani comprendono facilmente un linguaggio basato sul testo, ma le macchine hanno bisogno di qualcosa in più. Inserisci il modello oggetto documento (DOM).

Quando il browser carica una pagina web, carica molte cose interessanti. Una delle cose che fa è trasformare il codice HTML dell'autore in un documento online. In sostanza, per comprendere la struttura della pagina, il browser analizza l'HTML (stringhe di testo statiche) in un modello dati (oggetti/nodi). Il browser conserva la gerarchia HTML creando una struttura di questi nodi: il DOM. L'aspetto interessante del DOM è che è una rappresentazione live della pagina. A differenza dell'HTML statico che creiamo, i nodi prodotti dai browser contengono proprietà, metodi e soprattutto possono essere manipolati dai programmi. Ecco perché siamo in grado di creare elementi DOM direttamente utilizzando JavaScript:

const header = document.createElement('header');
const h1 = document.createElement('h1');
h1.textContent = 'Hello DOM';
header.appendChild(h1);
document.body.appendChild(header);

produce il seguente markup HTML:

<body>
    <header>
    <h1>Hello DOM</h1>
    </header>
</body>

Tutto bene. Poi che diamine è lo shadow DOM?

DOM... nell'ombra

Lo shadow DOM è semplicemente un DOM normale con due differenze: 1) come viene creato/utilizzato e 2) come si comporta in relazione al resto della pagina. Normalmente, crei nodi DOM e li aggiungi come elementi secondari di un altro elemento. Con shadow DOM, crei un albero DOM con ambito collegato all'elemento, ma separato dagli effettivi elementi secondari. Questo sottoalbero con ambito è chiamato albero delle ombre. L'elemento a cui è associato è il suo host shadow. Tutto ciò che aggiungi nell'ombra diventa locale per l'elemento host, incluso <style>. In questo modo lo shadow DOM ottiene l'ambito dello stile CSS.

Creazione dello shadow DOM in corso...

Una radice shadow è un frammento di documento collegato a un elemento "host". L'azione di collegare una radice shadow è il modo in cui l'elemento ottiene il proprio shadow DOM. Per creare lo shadow DOM per un elemento, chiama element.attachShadow():

const header = document.createElement('header');
const shadowRoot = header.attachShadow({mode: 'open'});
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'; // Could also use appendChild().

// header.shadowRoot === shadowRoot
// shadowRoot.host === header

Uso .innerHTML per riempire la radice shadow, ma potresti anche utilizzare altre API DOM. Questo è il web. Abbiamo l'imbarazzo della scelta.

La specifica definisce un elenco di elementi che non possono ospitare un albero delle ombre. Un elemento potrebbe essere nell'elenco per diversi motivi:

  • Il browser ospita già il proprio shadow DOM interno per l'elemento (<textarea>, <input>).
  • Non ha senso che l'elemento ospiti uno shadow DOM (<img>).

Ad esempio, queste azioni non funzionano:

    document.createElement('input').attachShadow({mode: 'open'});
    // Error. `<input>` cannot host shadow dom.

Creazione dello shadow DOM per un elemento personalizzato

Shadow DOM è particolarmente utile durante la creazione di elementi personalizzati. Utilizza lo shadow DOM per compartimentare il codice HTML, CSS e JS di un elemento, producendo così un "componente web".

Esempio: un elemento personalizzato collega lo shadow DOM a se stesso, incapsulando il relativo DOM/CSS:

// Use custom elements API v1 to register a new HTML tag and define its JS behavior
// using an ES6 class. Every instance of <fancy-tab> will have this same prototype.
customElements.define('fancy-tabs', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    // Attach a shadow root to <fancy-tabs>.
    const shadowRoot = this.attachShadow({mode: 'open'});
    shadowRoot.innerHTML = `
        <style>#tabs { ... }</style> <!-- styles are scoped to fancy-tabs! -->
        <div id="tabs">...</div>
        <div id="panels">...</div>
    `;
    }
    ...
});

Ci sono un paio di cose interessanti in corso qui. Il primo è che l'elemento personalizzato crea il proprio shadow DOM quando viene creata un'istanza di <fancy-tabs>. Puoi farlo nel constructor(). In secondo luogo, poiché stiamo creando una radice shadow, le regole CSS all'interno di <style> avranno come ambito <fancy-tabs>.

Composizione e spazi

La composizione è una delle caratteristiche meno comprese dello shadow DOM, ma probabilmente la più importante.

Nel mondo dello sviluppo web, la composizione è il modo in cui realizziamo app, dichiarativamente a partire dal codice HTML. Diversi componenti di base (<div>, <header>, <form>, <input>) si uniscono per formare le app. Alcuni di questi tag funzionano anche tra di loro. È per questo motivo che gli elementi nativi come <select>, <details>, <form> e <video> sono così flessibili. Ciascuno di questi tag accetta determinati HTML come elementi secondari e li utilizza in modo speciale. Ad esempio, <select> sa come eseguire il rendering di <option> e <optgroup> in widget a discesa e a selezione multipla. L'elemento <details> visualizza <summary> come una freccia espandibile. Persino <video> sa come comportarsi con determinati bambini: gli elementi <source> non vengono visualizzati, ma influiscono sul comportamento del video. Che magia!

Terminologia: DOM chiaro e DOM in ombra

La composizione shadow DOM introduce una serie di nuovi concetti fondamentali nello sviluppo web. Prima di addentrarci nelle informazioni, usiamo un po' di terminologia in modo da usare lo stesso gergo.

DOM leggero

Il markup scritto da un utente del tuo componente. Il DOM si trova al di fuori del DOM shadow del componente. Si tratta degli effettivi elementi secondari dell'elemento.

<better-button>
    <!-- the image and span are better-button's light DOM -->
    <img src="gear.svg" slot="icon">
    <span>Settings</span>
</better-button>

DOM shadow

Il DOM scritto da un autore del componente. Lo shadow DOM è locale del componente, ne definisce la struttura interna, il CSS con ambito e incapsula i dettagli dell'implementazione. Può anche definire come eseguire il rendering del markup creato dal consumatore del componente.

#shadow-root
    <style>...</style>
    <slot name="icon"></slot>
    <span id="wrapper">
    <slot>Button</slot>
    </span>

Albero DOM appiattito

Il risultato della distribuzione da parte del browser del light DOM dell'utente nel tuo shadow DOM, che esegue il rendering del prodotto finale. L'albero appiattito è quello che vedi in DevTools e nella pagina.

<better-button>
    #shadow-root
    <style>...</style>
    <slot name="icon">
        <img src="gear.svg" slot="icon">
    </slot>
    <span id="wrapper">
        <slot>
        <span>Settings</span>
        </slot>
    </span>
</better-button>

L'elemento <slot>

Shadow DOM compone diversi alberi DOM utilizzando l'elemento <slot>. Le aree annuncio sono segnaposto all'interno del componente che gli utenti possono riempire con il proprio markup. Se definisci uno o più slot, inviti al markup esterno a eseguire il rendering nel DOM shadow del componente. Essenzialmente, stai dicendo "Esegui il rendering del markup dell'utente qui".

Gli elementi possono "superare" il limite shadow DOM quando un <slot> li invita. Questi elementi sono chiamati nodi distribuiti. Concettualmente, i nodi distribuiti possono sembrare un po' bizzarri. Gli slot non spostano fisicamente il DOM, ma lo visualizzano in un'altra posizione all'interno dello shadow DOM.

Un componente può definire zero o più slot nel suo DOM shadow. Gli slot possono essere vuoti o fornire contenuti di fallback. Se l'utente non fornisce contenuti light DOM, lo slot esegue il rendering dei suoi contenuti di riserva.

<!-- Default slot. If there's more than one default slot, the first is used. -->
<slot></slot>

<slot>fallback content</slot> <!-- default slot with fallback content -->

<slot> <!-- default slot entire DOM tree as fallback -->
    <h2>Title</h2>
    <summary>Description text</summary>
</slot>

Puoi anche creare aree con nome. Gli slot con nome sono aree specifiche nel DOM shadow a cui gli utenti fanno riferimento per nome.

Esempio: gli slot nel DOM shadow di <fancy-tabs>:

#shadow-root
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot> <!-- named slot -->
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>

Gli utenti dei componenti dichiarano <fancy-tabs> nel seguente modo:

<fancy-tabs>
    <button slot="title">Title</button>
    <button slot="title" selected>Title 2</button>
    <button slot="title">Title 3</button>
    <section>content panel 1</section>
    <section>content panel 2</section>
    <section>content panel 3</section>
</fancy-tabs>

<!-- Using <h2>'s and changing the ordering would also work! -->
<fancy-tabs>
    <h2 slot="title">Title</h2>
    <section>content panel 1</section>
    <h2 slot="title" selected>Title 2</h2>
    <section>content panel 2</section>
    <h2 slot="title">Title 3</h2>
    <section>content panel 3</section>
</fancy-tabs>

E se te lo stai chiedendo, l'albero appiattito ha un aspetto simile a questo:

<fancy-tabs>
    #shadow-root
    <div id="tabs">
        <slot id="tabsSlot" name="title">
        <button slot="title">Title</button>
        <button slot="title" selected>Title 2</button>
        <button slot="title">Title 3</button>
        </slot>
    </div>
    <div id="panels">
        <slot id="panelsSlot">
        <section>content panel 1</section>
        <section>content panel 2</section>
        <section>content panel 3</section>
        </slot>
    </div>
</fancy-tabs>

Tieni presente che il nostro componente è in grado di gestire diverse configurazioni, ma la struttura DOM appiattita rimane la stessa. Possiamo anche passare da <button> a <h2>. Questo componente è stato creato per gestire diversi tipi di elementi secondari... proprio come <select>.

Stili

Sono disponibili molte opzioni per applicare uno stile ai componenti web. Un componente che utilizza shadow DOM può avere uno stile stabilito dalla pagina principale, definire i propri stili o fornire hook (sotto forma di proprietà CSS personalizzate) per consentire agli utenti di eseguire l'override dei valori predefiniti.

Stili definiti dal componente

In pratica, la funzionalità più utile di shadow DOM è il CSS con ambito:

  • I selettori CSS della pagina esterna non vengono applicati al componente.
  • Gli stili definiti all'interno non svaniscono. L'ambito è ristretto all'elemento host.

I selettori CSS utilizzati all'interno dello shadow DOM vengono applicati localmente al componente. In pratica, questo significa che possiamo utilizzare nuovamente nomi comuni di ID/classi, senza preoccuparci dei conflitti in altre parti della pagina. I selettori CSS più semplici sono una best practice all'interno di Shadow DOM. Sono utili anche per le prestazioni.

Esempio: gli stili definiti in una radice shadow sono locali

#shadow-root
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        ...
    }
    #tabs {
        display: inline-flex;
        ...
    }
    </style>
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

L'ambito dei fogli di stile è limitato anche all'albero delle ombre:

#shadow-root
    <link rel="stylesheet" href="styles.css">
    <div id="tabs">
    ...
    </div>
    <div id="panels">
    ...
    </div>

Ti sei mai chiesto in che modo l'elemento <select> esegue il rendering di un widget a selezione multipla (anziché di un menu a discesa) quando aggiungi l'attributo multiple:

<select multiple>
  <option>Do</option>
  <option selected>Re</option>
  <option>Mi</option>
  <option>Fa</option>
  <option>So</option>
</select>

<select> può personalizzare lo stile di sé in base agli attributi che dichiari. Anche i componenti web possono personalizzare lo stile utilizzando il selettore :host.

Esempio: lo stile di un componente

<style>
:host {
    display: block; /* by default, custom elements are display: inline */
    contain: content; /* CSS containment FTW. */
}
</style>

Un problema con :host è che le regole nella pagina principale hanno una specificità maggiore rispetto alle regole :host definite nell'elemento. Vale a dire che gli stili esterni vincono. Ciò consente agli utenti di ignorare lo stile di primo livello dall'esterno. Inoltre, :host funziona solo nel contesto di una radice shadow, quindi non puoi utilizzarlo al di fuori dello shadow DOM.

La forma funzionale di :host(<selector>) consente di scegliere come target l'host se corrisponde a un <selector>. Questo è un ottimo modo per il componente per incapsulare comportamenti che reagiscono all'interazione dell'utente, stato o stile dei nodi interni in base all'host.

<style>
:host {
    opacity: 0.4;
    will-change: opacity;
    transition: opacity 300ms ease-in-out;
}
:host(:hover) {
    opacity: 1;
}
:host([disabled]) { /* style when host has disabled attribute. */
    background: grey;
    pointer-events: none;
    opacity: 0.4;
}
:host(.blue) {
    color: blue; /* color host when it has class="blue" */
}
:host(.pink) > #tabs {
    color: pink; /* color internal #tabs node when host has class="pink". */
}
</style>

Stili basati sul contesto

:host-context(<selector>) corrisponde al componente se questo o uno dei suoi predecessori corrisponde a <selector>. Un uso comune di questa funzione è l'uso di temi basati su ciò che circonda un componente. Ad esempio, molte persone applicano un tema applicando un corso a <html> o <body>:

<body class="darktheme">
    <fancy-tabs>
    ...
    </fancy-tabs>
</body>

:host-context(.darktheme) definisce lo stile di <fancy-tabs> quando è un discendente di .darktheme:

:host-context(.darktheme) {
    color: white;
    background: black;
}

:host-context() può essere utile per la tematizzazione, ma un approccio ancora migliore consiste nel creare hook di stile utilizzando le proprietà personalizzate CSS.

Stile dei nodi distribuiti

::slotted(<compound-selector>) corrisponde ai nodi distribuiti in un <slot>.

Supponiamo di aver creato un componente del badge del nome:

<name-badge>
    <h2>Eric Bidelman</h2>
    <span class="title">
    Digital Jedi, <span class="company">Google</span>
    </span>
</name-badge>

Lo shadow DOM del componente può applicare uno stile a <h2> e .title dell'utente:

<style>
::slotted(h2) {
    margin: 0;
    font-weight: 300;
    color: red;
}
::slotted(.title) {
    color: orange;
}
/* DOESN'T WORK (can only select top-level nodes).
::slotted(.company),
::slotted(.title .company) {
    text-transform: uppercase;
}
*/
</style>
<slot></slot>

Se ricordi da prima, i valori <slot> non spostano il DOM leggero dell'utente. Quando i nodi sono distribuiti in un elemento <slot>, <slot> esegue il rendering del loro DOM, ma i nodi rimangono fisicamente all'interno. Gli stili applicati prima della distribuzione continuano a essere applicati dopo la distribuzione. Tuttavia, quando il DOM leggero viene distribuito, può assumere stili aggiuntivi (quelli definiti dallo shadow DOM).

Un altro esempio più approfondito tratto da <fancy-tabs>:

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
    <style>
    #panels {
        box-shadow: 0 2px 2px rgba(0, 0, 0, .3);
        background: white;
        border-radius: 3px;
        padding: 16px;
        height: 250px;
        overflow: auto;
    }
    #tabs {
        display: inline-flex;
        -webkit-user-select: none;
        user-select: none;
    }
    #tabsSlot::slotted(*) {
        font: 400 16px/22px 'Roboto';
        padding: 16px 8px;
        ...
    }
    #tabsSlot::slotted([aria-selected="true"]) {
        font-weight: 600;
        background: white;
        box-shadow: none;
    }
    #panelsSlot::slotted([aria-hidden="true"]) {
        display: none;
    }
    </style>
    <div id="tabs">
    <slot id="tabsSlot" name="title"></slot>
    </div>
    <div id="panels">
    <slot id="panelsSlot"></slot>
    </div>
`;

In questo esempio sono presenti due aree: un'area denominata per i titoli delle schede e un'area per i contenuti del riquadro delle schede. Quando l'utente seleziona una scheda, ne mostriamo il riquadro in grassetto. Per farlo, seleziona i nodi distribuiti che hanno l'attributo selected. Il codice JS dell'elemento personalizzato (non mostrato qui) aggiunge questo attributo al momento corretto.

Stilizzare un componente dall'esterno

Esistono un paio di modi per applicare uno stile a un componente dall'esterno. Il modo più semplice è utilizzare il nome del tag come selettore:

fancy-tabs {
    width: 500px;
    color: red; /* Note: inheritable CSS properties pierce the shadow DOM boundary. */
}
fancy-tabs:hover {
    box-shadow: 0 3px 3px #ccc;
}

Gli stili esterni prevalgono sempre sugli stili definiti in shadow DOM. Ad esempio, se l'utente scrive il selettore fancy-tabs { width: 500px; }, avrà la precedenza sulla regola del componente: :host { width: 650px;}.

Finora, l'applicazione dello stile al componente è utile solo per aiutarti. Ma cosa succede se vuoi stilare gli elementi interni di un componente? Per farlo, abbiamo bisogno delle proprietà CSS personalizzate.

Creazione di hook di stile utilizzando le proprietà personalizzate CSS

Gli utenti possono modificare gli stili interni se l'autore del componente fornisce hook di stile utilizzando le proprietà CSS personalizzate. Concettualmente, l'idea è simile a <slot>. Crei "segnaposto di stile" che gli utenti possono sostituire.

Esempio: <fancy-tabs> consente agli utenti di sostituire il colore di sfondo:

<!-- main page -->
<style>
    fancy-tabs {
    margin-bottom: 32px;
    --fancy-tabs-bg: black;
    }
</style>
<fancy-tabs background>...</fancy-tabs>

All'interno del DOM shadow:

:host([background]) {
    background: var(--fancy-tabs-bg, #9E9E9E);
    border-radius: 10px;
    padding: 10px;
}

In questo caso, il componente utilizzerà black come valore di sfondo poiché l'utente l'ha fornito. In caso contrario, il valore predefinito sarà #9E9E9E.

Argomenti avanzati

Creazione di radici shadow chiuse (dovrebbe evitare)

Esiste un altro tipo di shadow DOM, chiamato modalità "chiusa". Quando crei un albero delle ombre chiuso, JavaScript non potrà accedere al DOM interno del componente. Il funzionamento di elementi nativi come <video> è simile al funzionamento di elementi nativi. JavaScript non può accedere allo shadow DOM di <video> perché il browser lo implementa utilizzando una radice shadow in modalità chiusa.

Esempio: creazione di un albero delle ombre chiuso:

const div = document.createElement('div');
const shadowRoot = div.attachShadow({mode: 'closed'}); // close shadow tree
// div.shadowRoot === null
// shadowRoot.host === div

La modalità chiusa influisce anche su altre API:

  • Element.assignedSlot / TextNode.assignedSlot restituisce null
  • Event.composedPath() per gli eventi associati a elementi all'interno dello shadow DOM, restituisce []

Ecco un riepilogo del perché non dovresti mai creare componenti web con {mode: 'closed'}:

  1. Senso artificiale di sicurezza. Non esiste nulla che impedisca a un utente malintenzionato di compromettere Element.prototype.attachShadow.

  2. La modalità chiusa impedisce al codice elemento personalizzato di accedere al suo shadow DOM. È un errore. Dovrai però archiviare un riferimento per usarlo in un secondo momento se vuoi usare elementi come querySelector(). Questa funzionalità consente di sventare completamente lo scopo originale della modalità chiusa.

        customElements.define('x-element', class extends HTMLElement {
        constructor() {
        super(); // always call super() first in the constructor.
        this._shadowRoot = this.attachShadow({mode: 'closed'});
        this._shadowRoot.innerHTML = '<div class="wrapper"></div>';
        }
        connectedCallback() {
        // When creating closed shadow trees, you'll need to stash the shadow root
        // for later if you want to use it again. Kinda pointless.
        const wrapper = this._shadowRoot.querySelector('.wrapper');
        }
        ...
    });
    
  3. La modalità chiusa rende il componente meno flessibile per gli utenti finali. Durante la creazione dei componenti web, arriverà un momento in cui ti dimentichi di aggiungere una funzionalità. Un'opzione di configurazione. Un caso d'uso che l'utente vuole. Un esempio comune è l'oblio di includere hook adeguati per lo stile dei nodi interni. Con la modalità chiusa, gli utenti non hanno modo di sostituire i valori predefiniti e modificare gli stili. La possibilità di accedere ai componenti interni del componente è estremamente utile. Infine, gli utenti creano un fork del componente, ne trovano un altro o ne creano uno proprio se non fanno ciò che desiderano :(

Utilizzo degli slot in JS

L'API shadow DOM fornisce utilità per lavorare con slot e nodi distribuiti. Sono utili quando si crea un elemento personalizzato.

evento slotchange

L'evento slotchange viene attivato quando i nodi distribuiti di uno slot cambiano. Ad esempio, se l'utente aggiunge/rimuove i bambini dal DOM leggero.

const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
    console.log('light dom children changed!');
});

Per monitorare altri tipi di modifiche al DOM leggero, puoi configurare un MutationObserver nel costruttore dell'elemento.

Quali elementi vengono visualizzati in uno slot?

A volte è utile sapere quali elementi sono associati a un'area annuncio. Richiama slot.assignedNodes() per trovare gli elementi di cui viene eseguito il rendering dell'area annuncio. L'opzione {flatten: true} restituirà anche i contenuti di riserva di uno slot (se non vengono distribuiti nodi).

Supponiamo, ad esempio, che il tuo shadow DOM abbia il seguente aspetto:

<slot><b>fallback content</b></slot>
UtilizzoCallIl risultato
<my-component>testo componente</my-component> slot.assignedNodes(); [component text]
<my-component></my-component> slot.assignedNodes(); []
<my-component></my-component> slot.assignedNodes({flatten: true}); [<b>fallback content</b>]

A quale area è assegnato un elemento?

È possibile anche rispondere alla domanda inversa. element.assignedSlot indica a quale degli slot del componente è assegnato il tuo elemento.

Il modello di eventi DOM shadow

Quando un evento esce dallo shadow DOM, il target viene regolato per mantenere l'incapsulamento fornito dallo shadow DOM. In altre parole, gli eventi vengono reindirizzati in modo da sembrare che provengano dal componente anziché dagli elementi interni dello shadow DOM. Alcuni eventi non si propagano nemmeno dallo shadow DOM.

Gli eventi che attraversano il confine dell'ombra sono:

  • Eventi di interesse: blur, focus, focusin, focusout
  • Eventi del mouse: click, dblclick, mousedown, mouseenter, mousemove e così via.
  • Eventi ruota: wheel
  • Eventi di input: beforeinput, input
  • Eventi tastiera: keydown, keyup
  • Eventi di composizione: compositionstart, compositionupdate, compositionend
  • DragEvent: dragstart, drag, dragend, drop e così via.

Suggerimenti

Se l'albero delle ombre è aperto, la chiamata a event.composedPath() restituirà un array di nodi attraversati dall'evento.

Utilizzo di eventi personalizzati

Gli eventi DOM personalizzati che vengono attivati sui nodi interni in un albero shadow non escono dal confine shadow, a meno che l'evento non venga creato utilizzando il flag composed: true:

// Inside <fancy-tab> custom element class definition:
selectTab() {
    const tabs = this.shadowRoot.querySelector('#tabs');
    tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}

Se il criterio composed: false (predefinito), i consumatori non potranno ascoltare l'evento al di fuori della radice shadow.

<fancy-tabs></fancy-tabs>
<script>
    const tabs = document.querySelector('fancy-tabs');
    tabs.addEventListener('tab-select', e => {
    // won't fire if `tab-select` wasn't created with `composed: true`.
    });
</script>

Gestione dello stato attivo

Se ricordi dal modello di eventi shadow DOM, gli eventi attivati all'interno dello shadow DOM vengono regolati in modo che risultino come provenienti dall'elemento di hosting. Ad esempio, supponiamo di fare clic su un <input> all'interno di una radice shadow:

<x-focus>
    #shadow-root
    <input type="text" placeholder="Input inside shadow dom">

L'evento focus sembrerà provenire da <x-focus>, non da <input>. Analogamente, document.activeElement sarà <x-focus>. Se la root shadow è stata creata con mode:'open' (vedi la modalità chiusa), potrai accedere anche al nodo interno che ha impostato lo stato attivo:

document.activeElement.shadowRoot.activeElement // only works with open mode.

Se sono presenti più livelli di shadow DOM (ad esempio un elemento personalizzato all'interno di un altro elemento personalizzato), devi esaminare in modo ricorsivo le radici shadow per trovare il activeElement:

function deepActiveElement() {
    let a = document.activeElement;
    while (a && a.shadowRoot && a.shadowRoot.activeElement) {
    a = a.shadowRoot.activeElement;
    }
    return a;
}

Un'altra opzione per lo stato attivo è l'opzione delegatesFocus: true, che espande il comportamento dello stato attivo degli elementi in un albero di ombreggiatura:

  • Se fai clic su un nodo all'interno dello shadow DOM, che non è un'area attivabile, la prima area attivabile viene evidenziata.
  • Quando un nodo all'interno dello shadow DOM diventa attivo, :focus viene applicato all'host oltre all'elemento attivo.

Esempio: in che modo delegatesFocus: true modifica il comportamento dello stato attivo

<style>
    :focus {
    outline: 2px solid red;
    }
</style>

<x-focus></x-focus>

<script>
customElements.define('x-focus', class extends HTMLElement {
    constructor() {
    super(); // always call super() first in the constructor.

    const root = this.attachShadow({mode: 'open', delegatesFocus: true});
    root.innerHTML = `
        <style>
        :host {
            display: flex;
            border: 1px dotted black;
            padding: 16px;
        }
        :focus {
            outline: 2px solid blue;
        }
        </style>
        <div>Clickable Shadow DOM text</div>
        <input type="text" placeholder="Input inside shadow dom">`;

    // Know the focused element inside shadow DOM:
    this.addEventListener('focus', function(e) {
        console.log('Active element (inside shadow dom):',
                    this.shadowRoot.activeElement);
    });
    }
});
</script>

Risultato

delegatiFocus: comportamento reale.

Sopra è visualizzato il risultato quando lo stato attivo è <x-focus> (clic dell'utente, scheda in focus() e così via), Se viene fatto clic su "Testo DOM shadowable cliccabile", viene attivato lo stato attivo dell'elemento <input> interno (incluso autofocus).

Se dovessi impostare delegatesFocus: false, ecco cosa vedresti invece:

delegatiFocus: false e l&#39;input interno è attivo.
delegatesFocus: false e <input> interno sono incentrati.
delegatosFocus: false e x-focus guadagna lo stato attivo (ad esempio, ha tabindex=&#39;0&#39;).
delegatesFocus: false e <x-focus> ottengono lo stato attivo (ad esempio, ha tabindex="0").
delegatosFocus: false e &quot;Clickable Shadow DOM text&quot; (Testo del DOM Clickable Shadow) viene selezionato (o viene fatto clic su un&#39;altra area vuota all&#39;interno del DOM shadow dell&#39;elemento).
Viene fatto clic su delegatesFocus: false e "Clickable Shadow DOM text" (o su un'altra area vuota all'interno del DOM shadow dell'elemento).

Suggerimenti utili

Nel corso degli anni ho imparato una o due cose sulla creazione di componenti web. Penso che troverai utili alcuni di questi suggerimenti per la creazione dei componenti e il debug dello shadow DOM.

Utilizza il contenimento CSS

Solitamente, il layout/stile/paint di un componente web è piuttosto indipendente. Usa il contenimento CSS in :host per una vittoria perfetta:

<style>
:host {
    display: block;
    contain: content; /* Boom. CSS containment FTW. */
}
</style>

Reimpostazione degli stili ereditabili

Gli stili ereditabili (background, color, font, line-height e così via) continuano a ereditare in shadow DOM. In altre parole, penetrano al limite del DOM shadow per impostazione predefinita. Se vuoi iniziare con un nuovo slate, usa all: initial; per reimpostare gli stili ereditabili sul valore iniziale quando superano il limite dell'ombra.

<style>
    div {
    padding: 10px;
    background: red;
    font-size: 25px;
    text-transform: uppercase;
    color: white;
    }
</style>

<div>
    <p>I'm outside the element (big/white)</p>
    <my-element>Light DOM content is also affected.</my-element>
    <p>I'm outside the element (big/white)</p>
</div>

<script>
const el = document.querySelector('my-element');
el.attachShadow({mode: 'open'}).innerHTML = `
    <style>
    :host {
        all: initial; /* 1st rule so subsequent properties are reset. */
        display: block;
        background: white;
    }
    </style>
    <p>my-element: all CSS properties are reset to their
        initial value using <code>all: initial</code>.</p>
    <slot></slot>
`;
</script>

Individuazione di tutti gli elementi personalizzati utilizzati da una pagina

A volte è utile trovare gli elementi personalizzati utilizzati nella pagina. Per farlo, devi attraversare in modo ricorsivo lo shadow DOM di tutti gli elementi utilizzati nella pagina.

const allCustomElements = [];

function isCustomElement(el) {
    const isAttr = el.getAttribute('is');
    // Check for <super-button> and <button is="super-button">.
    return el.localName.includes('-') || isAttr && isAttr.includes('-');
}

function findAllCustomElements(nodes) {
    for (let i = 0, el; el = nodes[i]; ++i) {
    if (isCustomElement(el)) {
        allCustomElements.push(el);
    }
    // If the element has shadow DOM, dig deeper.
    if (el.shadowRoot) {
        findAllCustomElements(el.shadowRoot.querySelectorAll('*'));
    }
    }
}

findAllCustomElements(document.querySelectorAll('*'));

Creazione di elementi da un <template>

Anziché compilare una radice shadow utilizzando .innerHTML, possiamo utilizzare una <template> dichiarativa. I modelli sono un segnaposto ideale per dichiarare la struttura di un componente web.

Vedi l'esempio nella sezione "Elementi personalizzati: creazione di componenti web riutilizzabili".

Supporto di cronologia e browser

Se hai seguito i componenti web da un paio d'anni, sarai che da un po' di tempo in Chrome 35 e versioni successive/Opera viene fornita una versione meno recente di shadow DOM. Blink continuerà a supportare entrambe le versioni in parallelo per qualche tempo. La specifica v0 ha fornito un metodo diverso per creare una radice shadow (element.createShadowRoot anziché element.attachShadow di v1). La chiamata del metodo precedente continua a creare una radice shadow con semantica v0, in modo che il codice v0 esistente non presenti errori.

Se ti interessano le vecchie specifiche v0, consulta gli articoli html5rocks: 1, 2, 3. C'è anche un ottimo confronto delle differenze tra shadow DOM v0 e v1.

Supporto del browser

Shadow DOM v1 viene fornito in Chrome 53 (stato), Opera 40, Safari 10 e Firefox 63. Edge ha avviato lo sviluppo.

Per rilevare lo shadow DOM da parte della funzionalità, verifica l'esistenza di attachShadow:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Polyfill

Fino a quando il supporto dei browser non sarà ampiamente disponibile, i polyfill shadydom e shadycss ti offrono la funzionalità v1. Shady DOM imita l'ambito DOM di Shadow DOM e polyfill shadycss le proprietà personalizzate CSS e lo stile che definisce l'ambito fornito dall'API nativa.

Installa i polyfill:

bower install --save webcomponents/shadydom
bower install --save webcomponents/shadycss

Utilizza i polyfill:

function loadScript(src) {
    return new Promise(function(resolve, reject) {
    const script = document.createElement('script');
    script.async = true;
    script.src = src;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
    });
}

// Lazy load the polyfill if necessary.
if (!supportsShadowDOMV1) {
    loadScript('/bower_components/shadydom/shadydom.min.js')
    .then(e => loadScript('/bower_components/shadycss/shadycss.min.js'))
    .then(e => {
        // Polyfills loaded.
    });
} else {
    // Native shadow dom v1 support. Go to go!
}

Consulta la pagina https://github.com/webcomponents/shadycss#usage per istruzioni su come applicare lo zoom o l'ambito degli stili.

Conclusione

Per la prima volta in assoluto, abbiamo una primitiva API che esegue correttamente l'ambito CSS, la definizione dell'ambito DOM e ha una vera composizione. Combinato con altre API dei componenti web, come gli elementi personalizzati, shadow DOM offre un modo per creare componenti realmente incapsulati senza compromissioni o usare elementi vecchi come gli <iframe>.

Non fraintendetemi. Lo Shadow DOM è di sicuro un mostro complesso! Ma è una bestia che vale la pena imparare. Trascorri un po' di tempo. Imparala e fai domande.

Per approfondire

Domande frequenti

Posso usare Shadow DOM v1 oggi?

Con un polyfill, sì. Vedi Supporto dei browser.

Quali funzionalità di sicurezza offre lo shadow DOM?

Shadow DOM non è una funzionalità di sicurezza. È uno strumento leggero per definire l'ambito delle risorse CSS e nascondere gli alberi DOM nei componenti. Se vuoi un vero confine di sicurezza, utilizza un <iframe>.

Un componente web deve utilizzare shadow DOM?

No, Non è necessario creare componenti web che utilizzano shadow DOM. Tuttavia, creare elementi personalizzati che utilizzano Shadow DOM consente di sfruttare funzionalità come l'ambito CSS, l'incapsulamento del DOM e la composizione.

Qual è la differenza tra le radici shadow aperte e chiuse?

Vedi Radici shadow chiuse.