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. La fragilità deriva dalla natura globale di HTML, CSS e JS. Nel corso degli anni abbiamo ha inventato un numero esorbitante di strumenti per eludere i problemi. Ad esempio, quando utilizzi un nuovo ID/classe HTML, non si può sapere se è in conflitto con un nome esistente utilizzato dalla pagina. Sottili insetti si insinuano La specificità del CSS diventa un grosso problema (!important tutto!), lo stile selettori diventano fuori controllo e le prestazioni possono risentirne. Elenco continua.

Shadow DOM corregge CSS e DOM. Introduce gli stili con ambito sul web completamente gestita. Senza strumenti o convenzioni di denominazione, puoi raggruppare CSS con , nascondere i dettagli di implementazione e creare contenuti indipendenti in JavaScript vanilla.

Introduzione

Shadow DOM è uno dei tre standard dei componenti web: Modelli HTML, Shadow DOM e Elementi personalizzati: Importazioni HTML faceva parte dell'elenco, ma ora sono considerate ritirato.

Non è necessario creare componenti web che utilizzano shadow DOM. Ma se lo fai, sfruttarne i vantaggi (ambito CSS, incapsulamento del DOM, composizione) e crea contenuti riutilizzabili elementi personalizzati, resilienti, altamente configurabili ed estremamente riutilizzabili. Se personalizzato consentono di creare un nuovo HTML (con un'API JS), lo shadow DOM è e al modo in cui fornisci HTML e CSS. Le due API si combinano per creare un componente con HTML, CSS e JavaScript indipendenti.

Shadow DOM è progettato come strumento per creare app basate su componenti. Pertanto, offre soluzioni a 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 del DOM shadow è limitato all'ambito. Regole di stile non trapelano e gli stili di pagina non si distinguono.
  • Composizione: progetta un'API dichiarativa basata su markup per il tuo componente.
  • Semplifica i CSS: il DOM con ambito consente di utilizzare semplici selettori CSS, nomi generici di ID/classe senza preoccuparsi di conflitti di denominazione.
  • Produttività: considera le app in blocchi di DOM anziché in blocchi di grandi dimensioni (globale).
di Gemini Advanced.

Demo di fancy-tabs

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

Visualizza il codice sorgente su GitHub

Che cos'è lo shadow DOM?

Informazioni generali sul DOM

HTML è alla base del Web perché è facile da utilizzare. Se dichiari alcuni tag, possono creare in pochi secondi una pagina che abbia sia presentazione che struttura. Tuttavia, HTML non è poi così utile. È facile per le persone comprendere un testo- ma le macchine hanno bisogno di qualcosa di più. Inserisci l'oggetto del documento o DOM.

Quando il browser carica una pagina web, fa una serie di cose interessanti. Uno di trasforma il codice HTML dell'autore in un documento online. In sostanza, per comprendere la struttura della pagina, il browser analizza l'HTML (statico stringhe di testo) in un modello di dati (oggetti/nodi). Il browser conserva Gerarchia dell'HTML creando una struttura di questi nodi: il DOM. La cosa interessante sul DOM è che si tratta di una rappresentazione in tempo reale della tua pagina. A differenza delle immagini L'HTML che creiamo, i nodi prodotti dal browser contengono proprietà, metodi e best di tutto... possono essere manipolati dai programmi! Ecco perché possiamo creare un 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>

Va tutto bene. Poi Che cos'è shadow DOM?

DOM... nell'ombra

Shadow DOM è semplicemente un normale DOM con due differenze: 1) come viene creato/usato e 2) Come si comporta in relazione al resto della pagina. Di solito, il DOM viene creato nodi e aggiungerli come secondari di un altro elemento. Con shadow DOM, crea una struttura DOM con ambito collegato all'elemento, ma separata dai suoi figli reali. Questo sottoalbero con ambito è chiamato albero ombra. L'elemento a cui è associato è l'host shadow. Tutto ciò che aggiungi nell'ombra diventa locale all'elemento host, tra cui <style>. Ecco come shadow DOM raggiunge la definizione dell'ambito dello stile CSS.

Creazione del DOM shadow

Una root shadow è un frammento di documento che viene collegato a un elemento "host". L'atto di collegare una radice shadow è il modo in cui l'elemento ottiene il suo DOM shadow. A crea shadow DOM per un elemento, richiama 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 radice ombra, ma puoi anche usare un altro DOM su quelle di livello inferiore. Questo è il web. Abbiamo la possibilità di scegliere.

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

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

Ad esempio, questa operazione non funziona:

    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 elementi personalizzati. Utilizza lo shadow DOM per suddividere in compartimenti i codici HTML, CSS e JS di un elemento, in modo da producendo un "componente web".

Esempio: un elemento personalizzato collega lo shadow DOM a se stesso, che incapsula il proprio 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 fare qui. La prima è che elemento personalizzato crea il proprio DOM shadow quando un'istanza di <fancy-tabs> viene creato. Puoi farlo in constructor(). In secondo luogo, poiché stiamo creando una root shadow, le regole CSS all'interno di <style> avranno come ambito <fancy-tabs>.

Composizione e spazi

La composizione è una delle caratteristiche meno comprensibili dello shadow DOM, probabilmente il più importante.

Nel mondo dello sviluppo web, la composizione è il modo in cui creiamo le app, dichiarativamente fuori dal codice HTML. Componenti di base diversi (<div>, <header>, <form> e <input>) si uniscono per formare le app. Alcuni di questi tag funzionano una con l'altra. La composizione è il motivo per cui elementi nativi come <select>, <details>, <form> e <video> sono così flessibili. Ciascuno di questi tag accetta alcuni HTML da bambini e con questi contenuti fa qualcosa di speciale. Ad esempio: <select> sa come eseguire il rendering di <option> e <optgroup> in un menu a discesa e widget a selezione multipla. L'elemento <details> visualizza <summary> come un freccia espandibile. Persino <video> sa come trattare con alcuni bambini: Gli elementi <source> non vengono visualizzati, ma influiscono sul comportamento del video. Che magia!

Terminologia: light DOM e shadow DOM

La composizione DOM shadow introduce una serie di nuovi concetti fondamentali sul web sviluppo del prodotto. Prima di addentrarci nelle erbe infestanti, standardizziamo alcuni la terminologia adottata, quindi parliamo dello stesso gergo.

DOM leggero

Il markup scritto da un utente del tuo componente. Questo DOM risiede al di fuori del DOM shadow del componente. Sono gli 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 da un autore del componente. Il DOM shadow è locale del componente e ne definisce la struttura interna e il CSS con ambito e incapsula la tua implementazione i dettagli. Può anche definire la modalità di 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 del browser che ha distribuito il Light DOM dell'utente nella tua ombra DOM, con il rendering del prodotto finale. L'albero appiattito è quello che vedi alla fine nei DevTools e sul rendering della 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>

<slot> elemento

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

Gli elementi possono essere "incrociati" il confine DOM shadow quando <slot> invita in cui vengono inseriti. Questi elementi sono chiamati nodi distribuiti. Concettualmente, nodi distribuiti può sembrare un po' strano. Gli slot non spostano fisicamente il DOM; loro eseguirne il rendering in un'altra posizione all'interno del DOM shadow.

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

<!-- 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. Le aree con nome sono fori specifici della shadow DOM a cui gli utenti fanno riferimento per nome.

Esempio. Le aree 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 questo aspetto:

<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 bidimensionale rimane lo stesso. Possiamo anche passare da <button> a <h2>. Questo componente è stato creato per gestire diversi tipi di elementi secondari... come fa <select>!

Stili

Esistono molte opzioni per applicare uno stile ai componenti web. Un componente che utilizza shadow Il DOM può essere definito dalla pagina principale, definire i propri stili o fornire ganci (in 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 è il CSS con ambito:

  • I selettori CSS della pagina esterna non vengono applicati all'interno del componente.
  • Gli stili definiti all'interno non vengono sbiaditi. L'ambito è l'elemento host.

I selettori CSS utilizzati all'interno dello shadow DOM vengono applicati localmente al componente. Nella Ciò significa che possiamo usare di nuovo i nomi comuni di ID/classe, senza preoccuparci sui conflitti in altri punti della pagina. I selettori CSS più semplici sono una best practice all'interno di Shadow DOM. Inoltre, sono utili per le prestazioni.

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

Inoltre, i fogli di stile hanno come ambito l'albero ombra:

#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 (invece 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ò avere uno stile diverso in base agli attributi che dichiarano. Anche i componenti web possono personalizzare gli stili utilizzando l'elemento :host selettore.

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 di :host regole definite nell'elemento. Vale a dire che prevalgono gli stili esterni. Questo consente agli utenti di sostituire dall'esterno lo stile di primo livello. Inoltre, :host funziona solo nel contesto di una radice ombra, quindi non puoi utilizzarla al di fuori shadow DOM.

La forma funzionale di :host(<selector>) ti consente di scegliere come target l'host se corrisponde a <selector>. Si tratta di un ottimo modo per consentire al componente di incapsulare comportamenti che reagiscono all'interazione dell'utente oppure stabiliscono o definiscono i nodi interni in base sull'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 opzione è la creazione di temi basati sul l'ambiente circostante. Ad esempio, molte persone sviluppano i temi applicando una classe a <html> o <body>:

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

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

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

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

Assegnazione di uno stile ai nodi distribuiti

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

Supponiamo di aver creato un componente per il badge del nome:

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

Il DOM shadow del componente può definire lo stile di <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 in precedenza, i <slot> non spostano il DOM leggero dell'utente. Quando nodi sono distribuiti in un <slot>, <slot> esegue il rendering del DOM, ma i nodi restano fisicamente. Gli stili applicati prima della distribuzione continuano su applicabili dopo la distribuzione. Tuttavia, quando il DOM leggero è distribuito, può applicare stili aggiuntivi (quelli definiti dallo shadow DOM).

Ecco un altro esempio più approfondito 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: uno denominato per i titoli delle schede e un spazio per i contenuti del riquadro delle schede. Quando l'utente seleziona una scheda, la relativa selezione viene formattata in grassetto. e rivelarne il riquadro. Per farlo, devi selezionare i nodi distribuiti Attributo selected. Il codice JS dell'elemento personalizzato (non mostrato qui) aggiunge che nel momento corretto.

Definizione dello stile di un componente dall'esterno

Esistono due modi per applicare uno stile a un componente dall'esterno. Il modo più semplice è quello di 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 su quelli definiti in shadow DOM. Ad esempio: se l'utente scrive il selettore fancy-tabs { width: 500px; }, prevale regola del componente: :host { width: 650px;}.

L'applicazione dello stile del componente stesso ti consente di raggiungere solo il punto. Ma cosa succede se definire l'interno di un componente? Per farlo, abbiamo bisogno di CSS proprietà.

Creare 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 dei "segnaposto dello stile" che gli utenti possono ignorare.

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

Argomenti avanzati

Creazione di root shadow chiuse (dovrebbe evitare)

C'è un altro tipo di DOM ombra chiamato "chiuso" . Quando crei un albero ombra chiuso, l'esterno di JavaScript non potrà accedere al DOM interno del componente. Si tratta di un funzionamento simile a quello degli elementi nativi come <video>. 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 ombra chiuso:

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

La modalità chiusa interessa anche altre API:

  • Resi: null su Element.assignedSlot / TextNode.assignedSlot
  • Event.composedPath() per gli eventi associati a elementi all'interno dell'ombra DOM, restituisce []
di Gemini Advanced.

Ecco un riepilogo del motivo per cui non si dovrebbero mai creare componenti web con {mode: 'closed'}:

  1. Senso artificiale di sicurezza. Nulla può impedire a un aggressore compromissione di Element.prototype.attachShadow.

  2. La modalità chiusa impedisce al codice dell'elemento personalizzato di accedere al suo shadow DOM. Si tratta di un errore completo. Dovrai però accantonare un riferimento per un secondo momento se vuoi utilizzare elementi come querySelector(). Questo è completamente sconfigge 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 tuo componente meno flessibile per gli utenti finali. Man mano che per creare componenti web, arriva un momento in cui dimentichi di aggiungere funzionalità. Un'opzione di configurazione. Un caso d'uso desiderato dall'utente. Un comune un esempio è dimenticarsi di includere hook di stile adeguati per i nodi interni. Con la modalità chiusa, gli utenti non possono in alcun modo sostituire i valori predefiniti stili. Poter accedere ai componenti interni del componente è di grande aiuto. Infine, gli utenti forkeranno il tuo componente, ne troveranno un altro o creeranno se non fa quello che vuole :(

Utilizzo degli slot in JS

L'API shadow DOM fornisce utilità per lavorare con slot e nodi. Questi aspetti sono utili durante la creazione di un elemento personalizzato.

evento slotchange

L'evento slotchange viene attivato quando i nodi distribuiti di uno slot cambiano. Per ad esempio se l'utente aggiunge/rimuove figli 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?

A volte è utile sapere quali elementi sono associati a un'area. Chiama slot.assignedNodes() per trovare gli elementi che vengono visualizzati nell'area. La L'opzione {flatten: true} restituirà anche i contenuti di fallback di un'area (se non esistono nodi vengono distribuiti).

Ad esempio, supponiamo che il tuo DOM shadow sia simile al seguente:

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

A quale area è assegnato un elemento?

È anche possibile rispondere alla domanda inversa. element.assignedSlot dice indica a quali aree componenti è assegnato il tuo elemento.

Modello di eventi DOM Shadow

Quando un evento compare dallo shadow DOM, la sua destinazione viene regolata in modo da mantenere dell'incapsulamento fornito dallo shadow DOM. Vale a dire, il retargeting degli eventi in modo in modo che provengano dal componente piuttosto che da elementi interni shadow DOM. Alcuni eventi non si propagano nemmeno fuori dal DOM shadow.

Gli eventi che superano il confine ombra sono:

  • Eventi importanti: blur, focus, focusin e 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 in un albero ombra non fumetto fuori dal confine ombra, a meno che l'evento non venga creato utilizzando il composed: true flag:

// 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 (impostazione predefinita), i consumatori non potranno ascoltare l'evento al di fuori della radice ombra.

<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 dell'attenzione

Se richiami dal modello di eventi di shadow DOM, gli eventi che vengono attivati all'interno dello shadow DOM vengono regolati in modo da sembrare che provengano dall'elemento host. Ad esempio, supponiamo che tu faccia clic su <input> all'interno di una radice ombra:

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

L'evento focus sembrerà che provenga da <x-focus>, non da <input>. Analogamente, il valore di document.activeElement corrisponderà a <x-focus>. Se la radice ombra è stata creata con mode:'open' (vedi modalità chiusa), verrà visualizzata anche ad accedere al nodo interno su cui si è concentrato:

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

Se sono in gioco più livelli di shadow DOM (ad esempio, un elemento personalizzato un altro elemento personalizzato), devi eseguire in modo ricorsivo i dettagli delle radici ombra per trova 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 le comportamento di messa a fuoco di un elemento all'interno di un albero ombra:

  • Se fai clic su un nodo all'interno di shadow DOM e il nodo non è un'area attivabile, viene evidenziata la prima area attivabile.
  • Quando un nodo all'interno del DOM shadow acquisisce lo stato attivo, :focus si applica all'host in oltre all'elemento 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

delegatisFocus: vero comportamento.

Sopra è riportato il risultato quando viene attivato l'elemento <x-focus> (clic dell'utente, scheda, focus() e così via), "Testo DOM ombra cliccabile" un clic o la finestra <input> è attivo (incluso autofocus).

Se impostassi delegatesFocus: false, ecco cosa vedresti invece:

delegatisFocus: falso e l&#39;input interno è focalizzato.
. delegatesFocus: false e <input> interno è attivo.
di Gemini Advanced.
.
. delegatisFocus: false e x-focus
    ottiene l&#39;attenzione (ad es. ha tabindex=&#39;0&quot;).
delegatesFocus: false e <x-focus> ottiene l'attenzione (ad es. ha tabindex="0").
. delegatisFocus: false e &quot;Testo DOM ombra cliccabile&quot; sono
    si fa clic su un&#39;altra area vuota all&#39;interno del DOM shadow dell&#39;elemento.
delegatesFocus: false e "Testo DOM ombra cliccabile" sono si fa clic su un'altra area vuota all'interno del DOM shadow dell'elemento.

Suggerimenti utili

Nel corso degli anni ho imparato qualcosa sulla creazione di componenti web. IO troverete utili alcuni di questi suggerimenti per creare componenti e il debug dello shadow DOM.

Utilizza il contenimento CSS

In genere, il layout, lo stile e la colorazione di un componente web sono indipendenti. Utilizza le funzionalità di Contenimento CSS in :host per un rendimento vincite:

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

Reimpostazione degli stili ereditabili

Continua gli stili ereditabili (background, color, font, line-height e così via) da ereditare nel DOM shadow. In altre parole, penetrano i confini del DOM ombra predefinito. Se vuoi iniziare con una nuova slate, usa all: initial; per reimpostare gli stili ereditabili al loro valore iniziale quando superano il confine d'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 elementi personalizzati utilizzati nella pagina. Per farlo, devono attraversare in modo ricorsivo lo shadow DOM di tutti gli elementi usati sulla 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 usare una <template>. I modelli sono un segnaposto ideale per dichiarare la struttura dei un componente web.

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

Storia e Supporto del browser

Se hai seguito i componenti web negli ultimi due anni, sappi che per Chrome 35 e versioni successive/Opera è stata fornita una versione precedente dello shadow DOM per per un po' di tempo. Blink continuerà a supportare entrambe le versioni in parallelo per alcuni nel tempo. La specifica v0 ha fornito un metodo diverso per creare una radice shadow (element.createShadowRoot anziché element.attachShadow della versione 1). Chiamata a il metodo precedente continua a creare una radice shadow con la semantica v0, per cui il codice non si interromperà.

Se ti interessa la vecchia specifica v0, dai un'occhiata a html5rocks articoli: 1, 2, 3. C'è anche un ottimo confronto delle differenze tra shadow DOM v0 e v1.

Supporto browser

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

Per rilevare la funzionalità shadow DOM, verifica l'esistenza di attachShadow:

const supportsShadowDOMV1 = !!HTMLElement.prototype.attachShadow;

Polyfill

Finché il supporto dei browser non sarà disponibile su larga scala, shadydom e I polyfill shadycss offrono la versione 1 funzionalità. Il DOM ombreggiato imita l'ambito del DOM dei polyfill shadow DOM e shadycss Proprietà personalizzate CSS e definizione dell'ambito di 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 applicare lo spessore/l'ambito ai tuoi stili.

Conclusione

Per la prima volta in assoluto, abbiamo una primitiva dell'API che esegue una corretta definizione dell'ambito CSS, dell'ambito del DOM e ha una vera composizione. Combinata con altre API dei componenti web come gli elementi personalizzati, shadow DOM fornisce un modo per creare contenuti componenti senza attacchi informatici o l'utilizzo di bagagli meno recenti come <iframe>.

Non fraintendermi. Il DOM delle Ombre è sicuramente una bestia complessa! Ma è una bestia che vale la pena imparare. Trascorri un po' di tempo con questo strumento. Impara e fai domande!

Per approfondire

Domande frequenti

Posso utilizzare 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 dei CSS e nascondere gli alberi DOM nel componente. Se vuoi un vero confine di sicurezza, usa 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 significa che sfruttare funzionalità come l'ambito CSS, l'incapsulamento del DOM e la composizione.

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

Vedi Radici shadow chiuse.