Shadow DOM v1 - Componenti web autonomi

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

Riepilogo

Shadow DOM rimuove la fragilità della creazione di app web. La fragilità dipende dalla natura globale di HTML, CSS e JS. Nel corso degli anni abbiamo inventato un numero esorbitante di strumenti per aggirare i problemi. Ad esempio, quando utilizzi un nuovo ID/classe HTML, non è possibile sapere se entrerà in conflitto con un nome esistente utilizzato dalla pagina. Piccoli bug si insinuano, la specificità del CSS diventa un grosso problema (!important tutte le cose!), i selezionatori di stile diventano incontrollati e le prestazioni possono risentirne. L'elenco è ancora lungo.

Shadow DOM corregge CSS e DOM. Introduce gli stili basati su ambito nella piattaforma web. Senza strumenti o convenzioni di denominazione, puoi accorpare il CSS con il markup, nascondere i dettagli di implementazione e creare componenti autocontenuti in JavaScript standard.

Introduzione

Shadow DOM è uno dei tre standard Web Component: Modelli HTML, Shadow DOM e Elementi personalizzati. Le importazioni HTML faceva parte dell'elenco, ma ora sono considerate obsolete.

Non devi creare componenti web che utilizzano shadow DOM. Tuttavia, quando lo fai, puoi sfruttarne i vantaggi (scoping CSS, incapsulamento DOM, composizione) e creare riutilizzabili elementi personalizzati, che sono resilienti, altamente configurabili ed estremamente riutilizzabili. Se gli elementi personalizzati sono il modo per creare un nuovo HTML (con un'API JS), lo shadow DOM è il modo in cui fornisci il codice HTML e CSS. Le due API si combinano per creare un componente con HTML, CSS e JavaScript autocontenuti.

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

  • DOM isolato: il DOM di un componente è autonomo (ad es.document.querySelector() non restituirà nodi nello shadow DOM del componente).
  • CSS basato su ambito: il CSS definito all'interno del DOM ombra è basato su ambito. Le regole di stile non vengono visualizzate e gli stili di pagina non vengono applicati.
  • Composizione: progetta un'API dichiarativa basata su markup per il tuo componente.
  • Semplifica il CSS: il DOM basato sugli ambiti ti consente di utilizzare selettori CSS semplici, nomi di ID/classi più generici e di non preoccuparti dei conflitti di nomi.
  • Produttività: pensa alle app in blocchi di DOM anziché in una sola pagina grande (globale).

Demo di fancy-tabs

In questo articolo farò riferimento a un componente demo (<fancy-tabs>) e agli snippet di codice al suo interno. Se il tuo browser supporta le API, dovresti vedere una demo dal vivo subito di seguito. In caso contrario, consulta il codice sorgente completo su GitHub.

Visualizza il codice sorgente su GitHub

Che cos'è lo shadow DOM?

Informazioni preliminari sul DOM

L'HTML è alla base del web perché è facile da usare. Dichiarando alcuni tag, puoi creare una pagina in pochi secondi che abbia sia la presentazione sia la struttura. Tuttavia, da solo non è molto utile. Per gli esseri umani è facile comprendere un linguaggio basato su testo, ma le macchine hanno bisogno di qualcosa in più. Inserisci il Document Object Model o DOM.

Quando il browser carica una pagina web, esegue una serie di operazioni interessanti. Una delle sue funzionalità è 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 di dati (oggetti/nodi). Il browser preserva la gerarchia dell'HTML creando un albero di questi nodi: il DOM. La cosa interessante del DOM è che si tratta di una rappresentazione in tempo reale della tua pagina. A differenza del codice HTML statico che scriviamo, i nodi prodotti dal browser contengono proprietà, metodi e, soprattutto, possono essere manipolati dai programmi. Ecco perché possiamo 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);

genera il seguente markup HTML:

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

Tutto bene. Quindi che cos'è shadow DOM?

DOM… nell'ombra

Shadow DOM è un DOM normale con due differenze: 1) il modo in cui viene creato/utilizzato e 2) il modo in cui si comporta rispetto al resto della pagina. In genere, crei nodi DOM e li accondi come elementi secondari di un altro elemento. Con Shadow DOM, crei un albero DOM basato sugli ambiti collegato all'elemento, ma separato dai suoi elementi secondari effettivi. Questo sottoalbero con ambito è chiamato albero ombra. L'elemento a cui è associato è il suo host ombra. Tutto ciò che aggiungi nelle ombre diventa locale per l'elemento host, incluso <style>. In questo modo, il DOM ombra consente di definire l'ambito degli stili CSS.

Creazione di DOM ombra

Un elemento principale ombra è un frammento di documento che viene collegato a un elemento "host". L'atto di attaccare un'origine shadow è il modo in cui l'elemento acquisisce il proprio shadow DOM. Per creare un DOM ombra 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

Sto utilizzando .innerHTML per compilare l'elemento shadow root, ma puoi anche utilizzare altre API DOM. Questo è il web. Abbiamo scelta.

La specifica definisce un elenco di elementi che non possono ospitare un albero ombra. Esistono diversi motivi per cui un elemento potrebbe essere presente nell'elenco:

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

Ad esempio, non funziona:

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

Creazione di shadow DOM per un elemento personalizzato

Shadow DOM è particolarmente utile per la creazione di elementi personalizzati. Utilizza shadow DOM per suddividere in compartimenti HTML, CSS e JS di un elemento, producendo così un "componente web".

Esempio: un elemento personalizzato collega a se stesso lo shadow DOM, 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 aspetti interessanti. La prima è che l'elemento personalizzato crea il proprio DOM ombra quando viene creata un'istanza di <fancy-tabs>. Questa operazione viene eseguita in constructor(). In secondo luogo, poiché stiamo creando un elemento radice ombra, le regole CSS all'interno di <style> avranno come ambito <fancy-tabs>.

Composizione e slot

La composizione è una delle funzionalità meno comprese dello shadow DOM, ma è senza dubbio la più importante.

Nel nostro mondo di sviluppo web, la composizione è il modo in cui costruiamo le app, in modo dichiarativo da HTML. Diversi componenti di base (<div>, <header>, <form>, <input>) si combinano per formare le app. Alcuni di questi tag funzionano persino tra di loro. La composizione è il motivo per cui gli elementi nativi come <select>, <details>, <form> e <video> sono così flessibili. Ognuno di questi tag accetta determinato codice HTML come elementi secondari e lo utilizza in modo speciale. Ad esempio, <select> sa come visualizzare <option> e <optgroup> in menu a discesa e widget di selezione multipla. L'elemento <details> mostra <summary> come freccia espandibile. Anche <video> sa come gestire determinati elementi secondari: gli elementi <source> non vengono visualizzati, ma influiscono sul comportamento del video. Che magia!

Terminologia: light DOM e shadow DOM

La composizione DOM ombra introduce una serie di nuovi elementi fondamentali nello sviluppo web. Prima di entrare nel dettaglio, standardizziamo un po' di terminologia in modo da parlare tutti la stessa lingua.

DOM Light

Il markup scritto da un utente del componente. Questo DOM si trova al di fuori del DOM shadow del componente. Si tratta degli elementi secondari effettivi 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 dall'autore di un componente. Shadow DOM è locale per il componente e ne definisce la struttura interna, il CSS basato sugli ambiti e incapsula i dettagli di implementazione. Inoltre, può definire come eseguire il rendering del markup creato dall'utente consumer del componente.

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

Albero DOM appiattito

Il risultato del browser che distribuisce il light DOM dell'utente nel tuo shadow DOM, eseguendo il rendering del prodotto finale. L'albero appiattito è ciò che vedi in DevTools e ciò che viene visualizzato 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>

Elemento <slot>

Shadow DOM compone diversi alberi DOM utilizzando l'elemento <slot>. Gli slot sono segnaposto all'interno del componente che gli utenti possono compilare con il proprio markup. Se definisci uno o più slot, inviti il markup esterno a essere visualizzato nel DOM ombra del componente. In sostanza, stai dicendo "Esegui il rendering del markup dell'utente qui".

Gli elementi possono "attraversare" il confine del DOM ombra quando un <slot> li invita. Questi elementi sono chiamati nodi distribuiti. A livello concettuale, 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 proprio DOM ombra. Gli slot possono essere vuoti o fornire contenuti di riserva. Se l'utente non fornisce contenuti light DOM, lo slot ne mostra quelli 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 spazi con nome. Gli slot con nome sono spazi vuoti specifici nel DOM ombra a cui gli utenti fanno riferimento per nome.

Esempio: gli slot nello shadow DOM 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 del componente dichiarano <fancy-tabs> come segue:

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

Se ti stai chiedendo come appare l'albero appiattito, ecco un esempio:

<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 configurazioni diverse, ma la struttura DOM appiattita rimane invariata. Possiamo anche passare da <button> a <h2>. Questo componente è stato creato per gestire diversi tipi di bambini, proprio come fa <select>.

Stili

Esistono molte opzioni per definire lo stile dei componenti web. Un componente che utilizza il DOM nascosto può essere impostato in base allo stile della pagina principale, definire i propri stili o fornire hook (sotto forma di proprietà CSS personalizzate) per consentire agli utenti di ignorare i valori predefiniti.

Stili definiti dai componenti

La funzionalità più utile di shadow DOM è senza dubbio il CSS basato sugli ambiti:

  • I selettori CSS della pagina esterna non vengono applicati all'interno del componente.
  • Gli stili definiti all'interno non si estendono all'esterno. Hanno ambito limitato all'elemento host.

I selettori CSS utilizzati all'interno del DOM ombra si applicano localmente al componente. In pratica, significa che possiamo utilizzare di nuovo nomi di ID/classi comuni, senza preoccuparci di conflitti in altre parti della pagina. I selettori CSS più semplici sono una best practice all'interno di Shadow DOM. Sono inoltre utili per il rendimento.

Esempio: gli stili definiti in un elemento shadow root 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>

I fogli di stile hanno anche un ambito nell'albero ombra:

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

Ti sei mai chiesto come l'elemento <select> mostri un widget di selezione multipla (anziché 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> è in grado di stilizzarsi in modo diverso in base agli attributi dichiarati. Anche i componenti web possono applicare uno stile a se stessi utilizzando il selettore :host.

Esempio: uno stile del componente stesso

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

Un aspetto da tenere presente per :host è che le regole nella pagina principale hanno una specificità superiore rispetto alle regole :host definite nell'elemento. In altre parole, gli stili esterni vincono. In questo modo, gli utenti possono eseguire l'override degli stili di primo livello dall'esterno. Inoltre, :host funziona solo nel contesto di un elemento shadow root, quindi non puoi utilizzarlo al di fuori del DOM shadow.

La forma funzionale di :host(<selector>) consente di scegliere come target l'organizzatore se corrisponde a un <selector>. Questo è un ottimo modo per il componente per incapsulare i comportamenti che reagiscono all'interazione dell'utente o allo stato o allo 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>

Stile basato sul contesto

:host-context(<selector>) corrisponde al componente se esso o uno dei suoi antenati corrisponde a <selector>. Un utilizzo comune è la scelta di temi in base all'ambiente di un componente. Ad esempio, molte persone applicano un tema applicando una classe a <html> o <body>:

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

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

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

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

Stilizzazione dei nodi distribuiti

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

Supponiamo di aver creato un componente badge con nome:

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

Il DOM ombra del componente può applicare stili 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, i <slot> non spostano il DOM leggero dell'utente. Quando i nodi sono distribuiti in un <slot>, quest'ultimo esegue il rendering del loro DOM, ma i nodi rimangono fisicamente invariati.<slot> Gli stili applicati prima della distribuzione continuano a essere applicati anche dopo la distribuzione. Tuttavia, quando il light DOM viene distribuito, può assumere stili aggiuntivi (quelli definiti dallo shadow DOM).

Un altro esempio più approfondito di <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 slot: uno denominato per i titoli delle schede e uno per i contenuti del riquadro della scheda. Quando l'utente seleziona una scheda, la selezione viene messa in grassetto e viene visualizzato il relativo riquadro. Ciò viene fatto selezionando i nodi distribuiti che hanno l'attributo selected. Il codice JS dell'elemento personalizzato (non mostrato qui) aggiunge questo attributo al momento giusto.

Applicazione di stili a un componente dall'esterno

Esistono due modi per applicare lo 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 hanno sempre la precedenza sugli stili definiti nello shadow DOM. Ad esempio, se l'utente scrive il selettore fancy-tabs { width: 500px; }, questo sostituirà la regola del componente: :host { width: 650px;}.

Lo stile del componente stesso può fare solo così tanto. Ma cosa succede se vuoi impostare lo stile degli elementi interni di un componente? Per farlo, abbiamo bisogno di proprietà personalizzate CSS.

Creazione di hook di stile utilizzando le proprietà CSS personalizzate

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>. Creerai "sostituzioni di stile" che gli utenti potranno sostituire.

Esempio: <fancy-tabs> consente agli utenti di ignorare 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 relativo shadow DOM:

: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é fornito dall'utente. In caso contrario, il valore predefinito è #9E9E9E.

Argomenti avanzati

Creazione di radici shadow chiuse (da evitare)

Esiste un'altra versione del DOM ombra chiamata modalità "chiusa". Quando crei un albero ombra chiuso, JavaScript esterno non potrà accedere al DOM interno del componente. È simile al funzionamento degli elementi nativi come <video>. JavaScript non può accedere al DOM ombra di <video> perché il browser lo implementa utilizzando un'origine ombra in modalità chiusa.

Esempio: creazione di un albero ombreggiato chiuso:

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

Anche altre API sono interessate dalla modalità chiusa:

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

Ecco un riepilogo dei motivi per cui non dovresti mai creare componenti web con {mode: 'closed'}:

  1. Senso di sicurezza artificiale. Non c'è nulla che impedisca a un malintenzionato di eseguire il hijacking di Element.prototype.attachShadow.

  2. La modalità chiusa impedisce al codice dell'elemento personalizzato di accedere al proprio DOM ombra. È un errore completo. Dovrai invece conservare un riferimento per utilizzarlo in un secondo momento se vuoi usare elementi come querySelector(). In questo modo, viene completamente aggirato 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. Man mano che sviluppi componenti web, arriverà un momento in cui dimenticherai di aggiungere una funzionalità. Un'opzione di configurazione. Un caso d'uso che l'utente vuole. Un esempio comune è dimenticare di includere hook di stile adeguati per i nodi interni. In modalità chiusa, gli utenti non possono sostituire i valori predefiniti e modificare gli stili. La possibilità di accedere alle parti interne del componente è molto utile. Alla fine, gli utenti eseguiranno il fork del tuo componente, ne troveranno un altro o ne creeranno uno proprio se non fa ciò che vogliono :(

Utilizzare gli slot in JS

L'API shadow DOM fornisce utilità per lavorare con gli slot e i nodi distribuiti. Sono utili quando crei 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 elementi secondari 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 impostare un MutationObserver nel costruttore dell'elemento.

Quali elementi vengono visualizzati in uno slot?

A volte è utile sapere quali elementi sono associati a uno slot. Chiama slot.assignedNodes() per trovare gli elementi visualizzati dallo slot. L'opzione {flatten: true} restituirà anche i contenuti di riserva di uno slot (se non vengono distribuiti nodi).

Ad esempio, supponiamo che il tuo DOM ombra sia il seguente:

<slot><b>fallback content</b></slot>
UtilizzoChiamaRisultato
<my-component>component text</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 slot è assegnato un elemento?

È anche possibile rispondere alla domanda inversa. element.assignedSlot indica a quale delle posizioni del componente è assegnato l'elemento.

Il modello di eventi Shadow DOM

Quando un evento risale dallo shadow DOM, il relativo target viene modificato per mantenere l'incapsulamento fornito dallo shadow DOM. In altre parole, gli eventi vengono scelti come target in modo da sembrare provenienti dal componente anziché da elementi interni all'elemento DOM shadow. Alcuni eventi non si propagano nemmeno al di fuori del DOM ombra.

Gli eventi che superano il confine dell'ombra sono:

  • Eventi di messa a fuoco: 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 ombra è aperto, la chiamata a event.composedPath() restituirà un array di nodi attraversati dall'evento.

Utilizzo di eventi personalizzati

Gli eventi DOM personalizzati attivati sui nodi interni di un albero shadow non risalgono al di fuori del confine dell'ombra, 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 composed: false (valore predefinito), i consumatori non potranno ascoltare l'evento al di fuori dell'elemento shadow root.

<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 della messa a fuoco

Se ricordi il modello di eventi del DOM ombra, gli eventi attivati all'interno del DOM ombra vengono modificati in modo da sembrare provenienti dall'elemento host. Ad esempio, supponiamo che tu faccia clic su un <input> all'interno di un elemento radice ombra:

<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 l'elemento radice nascosto è stato creato con mode:'open' (vedi modalità chiusa), potrai anche accedere al nodo interno che ha acquisito lo stato attivo:

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

Se sono presenti più livelli di DOM ombra (ad esempio un elemento personalizzato all'interno di un altro elemento personalizzato), devi eseguire ricerche ricorsive nelle radici shadow per trovare 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 è delegatesFocus: true, che espande il comportamento di stato attivo degli elementi all'interno di un albero ombra:

  • Se fai clic su un nodo all'interno del DOM ombra e il nodo non è un'area attivabile, viene attivata la prima area attivabile.
  • Quando un nodo all'interno di shadow DOM acquisisce lo stato attivo, :focus viene applicato all'elemento host oltre che all'elemento attivo.

Esempio: in che modo delegatesFocus: true modifica il comportamento dell'attenzione

<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

delegatesFocus: comportamento true.

Sopra è riportato il risultato quando <x-focus> è attivo (clic dell'utente, tabulazione, focus() e così via). Viene fatto clic su "Testo shadow DOM cliccabile" o viene attivato il controllo interno <input> (incluso autofocus).

Se imposti delegatesFocus: false, vedrai invece quanto segue:

delegatesFocus: false e l&#39;input interno è attivo.
delegatesFocus: false e <input> interno sono a fuoco.
delegatesFocus: false e x-focus
    acquisisce lo stato attivo (ad es. ha tabindex=&#39;0&#39;).
delegatesFocus: false e <x-focus> acquisisce lo stato attivo (ad es. ha tabindex="0").
delegatesFocus: false e viene eseguito un clic su &quot;Testo DOM shadow cliccabile&quot; (o su un&#39;altra area vuota all&#39;interno del DOM shadow dell&#39;elemento).
delegatesFocus: false e viene fatto clic su "Testo DOM Shadow cliccabile" (o su un'altra area vuota all'interno del DOM Shadow dell'elemento).

Suggerimenti utili

Nel corso degli anni ho imparato un po' di cose sulla creazione di componenti web. Penso che alcuni di questi suggerimenti ti saranno utili per la creazione di componenti e per il debugging del DOM ombra.

Utilizzare il contenimento CSS

In genere, il layout/lo stile/la pittura di un componente web è abbastanza autonomo. Utilizza il contenimento CSS in :host per migliorare le prestazioni:

<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 essere ereditati nel DOM shadow. ovvero, attraversano il confine del DOM ombra per impostazione predefinita. Se vuoi iniziare da capo, utilizza all: initial; per reimpostare gli stili ereditabili sul loro valore iniziale quando superano il confine 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>

Trovare tutti gli elementi personalizzati utilizzati da una pagina

A volte è utile trovare gli elementi personalizzati utilizzati nella pagina. Per farlo, devi esaminare in modo ricorsivo il DOM ombra 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>

Invece di compilare un'origine ombreggiata utilizzando .innerHTML, possiamo utilizzare un <template> dichiarativo. I modelli sono un segnaposto ideale per dichiarare la struttura di un componente web.

Guarda l'esempio in "Elementi personalizzati: creazione di componenti web riutilizzabili".

Cronologia e supporto dei browser

Se segui i componenti web negli ultimi due anni, saprai che Chrome 35+/Opera supportano da tempo una versione precedente del DOM ombra. Blink continuerà a supportare entrambe le versioni in parallelo per un certo tempo. La specifica v0 forniva un metodo diverso per creare un elemento radice ombra (element.createShadowRoot anziché element.attachShadow della v1). La chiamata al metodo precedente continua a creare un elemento radice ombra con la semantica v0, pertanto il codice v0 esistente non verrà interrotto.

Se ti interessa la vecchia specifica v0, consulta gli articoli di html5rocks: 1, 2, 3. È disponibile anche un ottimo confronto delle differenze tra Shadow DOM v0 e v1.

Supporto browser

La versione 1 di DOM ombra è inclusa in Chrome 53 (stato), Opera 40, Safari 10 e Firefox 63. Edge ha iniziato lo sviluppo.

Per rilevare il DOM shadow, controlla l'esistenza di attachShadow:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Polyfill

Fino a quando il supporto del browser non sarà ampiamente disponibile, i polyfill shadydom e shadycss ti forniscono la funzionalità v1. Shady DOM imita l'ambito DOM di Shadow DOM e i polyfill shadycss proprietà CSS personalizzate e l'ambito dello stile 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!
}

Visita la pagina https://github.com/webcomponents/shadycss#usage per istruzioni su come eseguire lo shim/l'ambito degli stili.

Conclusione

Per la prima volta, abbiamo un'API primitiva che esegue un'adeguata delimitazione CSS e DOM e ha una composizione vera e propria. Se combinato con altre API dei componenti web come gli elementi personalizzati, il DOM ombra offre un modo per creare componenti completamente incapsulati senza utilizzare hack o vecchie strutture come i <iframe>.

Non fraintendermi. Shadow DOM è sicuramente un argomento complesso. Ma è un sistema che vale la pena imparare. Dedicaci un po' di tempo. Impara e fai domande.

Per approfondire

Domande frequenti

Posso utilizzare Shadow DOM 1.0 oggi stesso?

Con un polyfill, sì. Consulta la sezione Supporto dei browser.

Quali funzionalità di sicurezza fornisce lo shadow DOM?

Shadow DOM non è una funzionalità di sicurezza. È uno strumento leggero per definire l'ambito del CSS e nascondere le strutture DOM nel componente. Se vuoi un confine di sicurezza reale, utilizza un <iframe>.

Un componente web deve utilizzare shadow DOM?

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

Qual è la differenza tra radici shadow aperte e chiuse?

Consulta Radici shadow chiuse.