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).
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.
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
suElement.assignedSlot
/TextNode.assignedSlot
Event.composedPath()
per gli eventi associati a elementi all'interno dell'ombra DOM, restituisce []
Ecco un riepilogo del motivo per cui non si dovrebbero mai creare componenti web con
{mode: 'closed'}
:
Senso artificiale di sicurezza. Nulla può impedire a un aggressore compromissione di
Element.prototype.attachShadow
.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'); } ... });
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>
Utilizzo | Chiama | Risultato |
---|---|---|
<my-component>testo componente</my-component> | slot.assignedNodes(); |
[component text] |
<my-component></my-component> | slot.assignedNodes(); |
[] |
<my-component></my-component> | slot.assignedNodes({flatten: true}); |
[<b>fallback content</b>] |
A quale area è assegnato un elemento?
È 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
efocusout
- 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
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:
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
- Differenze tra Shadow DOM v1 e v0
- "Presentazione dell'API Shadow DOM basata su slot" dal blog di WebKit.
- Componenti web e il futuro del CSS modulare di Philip Walton
- "Elementi personalizzati: creazione di componenti web riutilizzabili" da WebFundamentals di Google.
- Specifiche Shadow DOM v1
- Specifiche v1 per gli elementi personalizzati
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.