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 elimina la fragilità della creazione di app web. La fragilità deriva 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. Si insinuano bug impercettibili, la specificità CSS diventa un problema enorme (!important tutto!), i selettori di stile sfuggono al controllo e le prestazioni possono risentirne. solo per fare qualche esempio.

Shadow DOM corregge CSS e DOM. Introduce gli stili con ambito nella piattaforma web. Senza strumenti o convenzioni di denominazione, puoi unire CSS e markup, nascondere i dettagli di implementazione e creare componenti autonomi in JavaScript vanilla.

Introduzione

Shadow DOM è uno dei tre standard dei componenti web: modelli HTML, Shadow DOM ed elementi personalizzati. Le importazioni HTML facevano parte dell'elenco, ma ora sono considerate ritirate.

Non è necessario creare componenti web che utilizzano shadow DOM. Tuttavia, quando lo fai, sfrutti i suoi vantaggi (ambito CSS, incapsulamento DOM, composizione) e crei elementi personalizzati riutilizzabili, 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 relativo HTML e CSS. Le due API si combinano per creare un componente con HTML, CSS e JavaScript autonomi.

Shadow DOM è progettato come strumento per la creazione di app basate su componenti. Pertanto, offre soluzioni ai 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 con ambito: il CSS definito all'interno dello shadow DOM ha un ambito limitato. Le regole di stile non vengono applicate all'esterno e gli stili della pagina non vengono applicati all'interno.
  • Composizione: progetta un'API dichiarativa basata su markup per il tuo componente.
  • Semplifica il CSS: il DOM con ambito consente di utilizzare selettori CSS semplici, nomi di ID/classi più generici e di non preoccuparsi dei conflitti di denominazione.
  • Produttività: pensa alle app in blocchi di DOM anziché come un'unica grande pagina (globale).

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 qui sotto. In caso contrario, consulta il codice sorgente completo su GitHub.

Visualizza il codice sorgente su GitHub

Che cos'è lo shadow DOM?

Informazioni sul DOM

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

Quando il browser carica una pagina web, esegue una serie di operazioni interessanti. Una delle cose che fa è trasformare l'HTML dell'autore in un documento attivo. In pratica, per comprendere la struttura della pagina, il browser analizza l'HTML (stringhe di testo statiche) in un modello di dati (oggetti/nodi). Il browser conserva la gerarchia dell'HTML creando un albero di questi nodi: il DOM. La cosa interessante del DOM è che è una rappresentazione in tempo reale della tua pagina. A differenza dell'HTML statico che creiamo, 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 questo è positivo. Allora, che cos'è lo shadow DOM?

DOM… nell'ombra

Shadow DOM è un DOM normale con due differenze: 1) come viene creato/utilizzato e 2) come si comporta in relazione al resto della pagina. In genere, crei nodi DOM e li aggiungi come elementi secondari di un altro elemento. Con lo shadow DOM, crei un albero DOM con ambito associato all'elemento, ma separato dai suoi elementi secondari effettivi. Questo sottoalbero con ambito è chiamato shadow tree. L'elemento a cui è collegato è il suo shadow host. Tutto ciò che aggiungi nello shadow DOM diventa locale per l'elemento di hosting, incluso <style>. È così che lo shadow DOM ottiene l'ambito dello stile CSS.

Creazione dello shadow DOM

Una shadow root è un frammento di documento che viene collegato a un elemento "host". L'atto di collegare una shadow root è il modo in cui l'elemento ottiene il suo shadow DOM. Per creare uno 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

Utilizzo .innerHTML per riempire la shadow root, ma potresti anche utilizzare altre API DOM. Questo è il web. Abbiamo la possibilità di scegliere.

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

  • 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, questo non funziona:

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

Creazione di uno shadow DOM per un elemento personalizzato

Shadow DOM è particolarmente utile quando si creano elementi personalizzati. Utilizza shadow DOM per suddividere in compartimenti l'HTML, il CSS e il JS di un elemento, creando così un "componente web".

Esempio: un elemento personalizzato collega 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 da notare. La prima è che l'elemento personalizzato crea il proprio shadow DOM quando viene creata un'istanza di <fancy-tabs>. Questa operazione viene eseguita nella constructor(). In secondo luogo, poiché stiamo creando una shadow root, le regole CSS all'interno di <style> verranno applicate a <fancy-tabs>.

Composizione e slot

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

Nel nostro mondo dello sviluppo web, la composizione è il modo in cui creiamo le app, in modo dichiarativo a partire dall'HTML. Diversi componenti di base (<div>, <header>, <form>, <input>) si uniscono per formare le app. Alcuni di questi tag possono anche essere utilizzati insieme. La composizione è il motivo per cui gli elementi nativi come <select>, <details>, <form> e <video> sono così flessibili. Ognuno di questi tag accetta determinati elementi HTML come elementi secondari e li utilizza in modo speciale. Ad esempio, <select> sa come eseguire il rendering di <option> e <optgroup> in widget di menu a discesa e selezione multipla. L'elemento <details> esegue il rendering di <summary> come una freccia espandibile. Persino <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 dello shadow DOM introduce una serie di nuovi concetti fondamentali nello sviluppo web. Prima di entrare nel dettaglio, standardizziamo alcuni termini in modo da parlare la stessa lingua.

Light DOM

Il markup scritto da un utente del tuo componente. Questo DOM si trova al di fuori dello shadow DOM 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>

Shadow DOM

Il DOM scritto dall'autore di un componente. Lo Shadow DOM è locale per il componente e definisce la sua struttura interna, il CSS con ambito e incapsula i dettagli di implementazione. Può anche definire come eseguire il rendering del markup creato dal consumer del tuo 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 nello shadow DOM, con 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>

L'elemento <slot>

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

Gli elementi possono "attraversare" il confine dello shadow DOM quando un <slot> li invita a farlo. 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 relativo shadow DOM. Gli slot possono essere vuoti o fornire contenuti di riserva. Se l'utente non fornisce contenuti light DOM, lo slot esegue il rendering dei 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 slot con nome. Gli slot con nome sono spazi specifici nello shadow DOM 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 dei componenti dichiarano <fancy-tabs> in questo 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>

Nota che il nostro componente è in grado di gestire diverse configurazioni, ma l'albero DOM appiattito rimane lo stesso. Possiamo anche passare da <button> a <h2>. Questo componente è stato creato per gestire diversi tipi di elementi secondari, proprio come fa <select>.

Stili

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

Stili definiti dai componenti

La funzionalità più utile dello shadow DOM è senza dubbio il CSS con ambito:

  • I selettori CSS della pagina esterna non si applicano all'interno del componente.
  • Gli stili definiti all'interno non vengono applicati all'esterno. Sono limitati all'elemento host.

I selettori CSS utilizzati all'interno dello shadow DOM si applicano localmente al componente. In pratica, questo significa che possiamo riutilizzare i 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 dello Shadow DOM. Inoltre, sono utili per il rendimento.

Esempio: gli stili definiti in una 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>

Anche i fogli di stile hanno come ambito lo shadow tree:

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

Ti sei mai chiesto come 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> è in grado di applicare uno stile diverso in base agli attributi che dichiari. Anche i componenti web possono applicare uno stile a se stessi utilizzando il selettore :host.

Esempio: un componente che applica lo stile a se stesso

<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. In altre parole, gli stili esterni hanno la priorità. In questo modo, gli utenti possono eseguire l'override dello stile di primo livello dall'esterno. Inoltre, :host funziona solo nel contesto di una shadow root, 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 incapsulare nel componente i comportamenti che reagiscono all'interazione dell'utente o per applicare uno stile ai 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 questo o uno dei suoi predecessori corrisponde a <selector>. Un utilizzo comune è la tematizzazione in base all'ambiente circostante di un componente. Ad esempio, molti utenti applicano i temi 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 i temi, ma un approccio ancora migliore è 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 badge con 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 agli elementi <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>

Come ricorderai, gli elementi <slot> non spostano il DOM light dell'utente. Quando i nodi vengono distribuiti in uno <slot>, lo <slot> esegue il rendering del loro DOM, ma i nodi rimangono fisicamente nella stessa posizione. Gli stili applicati prima della distribuzione continuano a essere applicati anche dopo. 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 slot con nome per i titoli delle schede e uno slot per i contenuti del riquadro delle schede. Quando l'utente seleziona una scheda, la selezione viene visualizzata in grassetto e viene mostrato il relativo riquadro. A questo scopo, seleziona i nodi distribuiti che hanno l'attributo selected. Il codice JS dell'elemento personalizzato (non mostrato qui) aggiunge questo attributo al momento giusto.

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 hanno sempre la precedenza su quelli definiti nello shadow DOM. Ad esempio, se l'utente scrive il selettore fancy-tabs { width: 500px; }, questo avrà la precedenza sulla regola del componente: :host { width: 650px;}.

Lo stile del componente stesso non è sufficiente. Ma cosa succede se vuoi applicare uno stile agli elementi interni di un componente? Per farlo, abbiamo bisogno delle proprietà personalizzate CSS.

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à personalizzate CSS. Concettualmente, l'idea è simile a <slot>. Crei "segnaposto di stile" che gli utenti possono sostituire.

Esempio: <fancy-tabs> consente agli utenti di eseguire l'override del 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é è stato fornito dall'utente. In caso contrario, il valore predefinito è #9E9E9E.

Argomenti avanzati

Creazione di shadow root chiusi (da evitare)

Esiste un'altra variante del DOM shadow, chiamata modalità "chiusa". Quando crei un albero shadow chiuso, JavaScript esterno non potrà accedere al DOM interno del tuo componente. Questo è simile al funzionamento di elementi nativi come <video>. JavaScript non può accedere allo shadow DOM di <video> perché il browser lo implementa utilizzando una shadow root in modalità chiusa.

Esempio: creazione di uno shadow tree 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 shadow, restituisce []

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

  1. Falso senso di sicurezza. Niente impedisce a un malintenzionato di compromettere Element.prototype.attachShadow.

  2. La modalità chiusa impedisce al codice dell'elemento personalizzato di accedere al proprio shadow DOM. È un fallimento totale. Dovrai invece memorizzare un riferimento per un secondo momento se vuoi utilizzare elementi come querySelector(). Questo vanifica 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 di componenti web, arriverà il momento in cui dimenticherai di aggiungere una funzionalità. Un'opzione di configurazione. Un caso d'uso desiderato dall'utente. Un esempio comune è dimenticare di includere hook di stile adeguati per i nodi interni. Con la modalità chiusa, gli utenti non possono ignorare le impostazioni predefinite e modificare gli stili. Poter accedere agli elementi interni del componente è molto utile. In definitiva, gli utenti eseguiranno il fork del tuo componente, ne troveranno un altro o ne creeranno uno proprio se non fa ciò che vogliono :(

Utilizzo degli slot in JS

L'API shadow DOM fornisce utilità per lavorare con slot e nodi distribuiti. Questi sono utili per la creazione di 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 light.

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

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

Quali elementi vengono visualizzati in un'area annuncio?

A volte è utile sapere quali elementi sono associati a uno slot. Chiama slot.assignedNodes() per scoprire quali elementi vengono 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 lo shadow DOM sia simile al 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 ti indica a quale slot del componente è assegnato l'elemento.

Il modello di eventi Shadow DOM

Quando un evento viene propagato dallo shadow DOM, il suo target viene modificato per mantenere l'incapsulamento fornito dallo shadow DOM. In altre parole, gli eventi vengono reindirizzati in modo che sembrino provenire dal componente anziché dagli elementi interni dello shadow DOM. Alcuni eventi non si propagano nemmeno al di fuori dello shadow DOM.

Gli eventi che attraversano il confine dello shadow DOM sono:

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

Suggerimenti

Se lo shadow tree è 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 uno shadow tree non vengono propagati oltre il confine dello 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 composed: false (valore predefinito), i consumer non potranno ascoltare l'evento all'esterno 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 del focus

Come ricorderai dal modello di eventi del DOM shadow, gli eventi attivati all'interno del DOM shadow vengono modificati in modo che sembrino provenire dall'elemento di hosting. Ad esempio, supponiamo che tu faccia clic su un <input> all'interno di una shadow root:

<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 shadow root è stata creata con mode:'open' (consulta la sezione Modalità chiusa), potrai anche accedere al nodo interno che ha ricevuto 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 eseguire il drill down in modo ricorsivo nelle shadow root 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 è l'opzione delegatesFocus: true, che espande il comportamento dello stato attivo degli elementi all'interno di uno shadow tree:

  • Se fai clic su un nodo all'interno dello shadow DOM e il nodo non è un'area attivabile, viene attivata la prima area attivabile.
  • Quando un nodo all'interno dello shadow DOM riceve lo stato attivo, :focus si applica all'host oltre che all'elemento con lo stato attivo.

Esempio: come 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

Comportamento delegatesFocus: true.

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

Se imposti delegatesFocus: false, ecco cosa vedrai:

delegatesFocus: false e l'input interno è attivo.
delegatesFocus: false e l'elemento <input> interno è attivo.
delegatesFocus: false e x-focus
    acquisisce lo stato attivo (ad es. ha tabindex=&quot;0&quot;).
delegatesFocus: false e <x-focus> ricevono lo stato attivo (ad es. hanno tabindex="0").
delegatesFocus: false e viene fatto clic su &quot;Testo Shadow DOM cliccabile&quot; (o su un'altra area vuota all'interno dello Shadow DOM dell'elemento).
delegatesFocus: false e "Clickable Shadow DOM text" vengono selezionati (o viene selezionata un'altra area vuota all'interno del DOM Shadow dell'elemento).

Suggerimenti utili

Nel corso degli anni ho imparato un paio di cose sulla creazione di componenti web. Penso che troverai utili alcuni di questi suggerimenti per la creazione di componenti e il debug di shadow DOM.

Utilizza il contenimento CSS

In genere, il layout, lo stile e l'aspetto di un componente web sono abbastanza autonomi. 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 nello shadow DOM. In altre parole, per impostazione predefinita, superano il confine dello shadow DOM. Se vuoi iniziare da zero, utilizza all: initial; per reimpostare gli stili ereditabili sul valore iniziale quando superano il confine dello shadow DOM.

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

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

Consulta l'esempio in "Custom elements: building reusable web components".

Cronologia e supporto del browser

Se hai seguito i componenti web negli ultimi due anni, saprai che Chrome 35+/Opera hanno fornito una versione precedente di Shadow DOM per un po' di tempo. Blink continuerà a supportare entrambe le versioni in parallelo per un po' di tempo. La specifica v0 forniva un metodo diverso per creare una shadow root (element.createShadowRoot anziché element.attachShadow della v1). La chiamata al metodo precedente continua a creare una shadow root con la semantica v0, quindi il codice v0 esistente non verrà interrotto.

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

Supporto browser

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

Per rilevare le funzionalità dello shadow DOM, 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 lo scoping DOM di Shadow DOM e shadycss esegue il polyfill delle proprietà personalizzate CSS e dello scoping 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!
}

Consulta la pagina https://github.com/webcomponents/shadycss#usage per istruzioni su come applicare lo shim/lo scope agli stili.

Conclusione

Per la prima volta in assoluto, abbiamo una primitiva API che esegue lo scoping CSS e DOM corretto e ha una composizione reale. In combinazione con altre API dei componenti web, come gli elementi personalizzati, lo shadow DOM consente di creare componenti veramente incapsulati senza espedienti o l'utilizzo di elementi obsoleti come gli <iframe>.

Non fraintendermi. Shadow DOM è sicuramente una bestia complessa. Ma è una bestia che vale la pena imparare a domare. Dedica un po' di tempo a questa attività. Imparalo e fai domande.

Per approfondire

Domande frequenti

Posso utilizzare Shadow DOM v1 oggi?

Con un polyfill, sì. Consulta 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 del CSS e nascondere gli alberi DOM nel componente. Se vuoi un vero e proprio perimetro di sicurezza, 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 sfruttare funzionalità come l'ambito CSS, l'incapsulamento DOM e la composizione.

Qual è la differenza tra shadow root aperti e chiusi?

Consulta Shadow root chiusi.