Shadow DOM Slots e Composizione
Impara gli slot e la composizione nel Shadow DOM di JavaScript: slot predefiniti e con nome, light DOM vs. shadow DOM, albero appiattito, evento slotchange e assignedNodes/assignedElements.
Gli slot e la composizione sono ciò che rende lo Shadow DOM genuinamente riutilizzabile. L'autore di un componente scrive una struttura interna fissa una volta sola, e i consumatori la riempiono con il proprio markup — senza che i due collidano mai. Questa pagina tratta l'elemento <slot> (slot predefiniti e con nome), come il light DOM e lo shadow DOM si combinano in un albero appiattito, l'evento slotchange, e i metodi assignedNodes() / assignedElements() utilizzati per leggere ciò che è stato inserito in uno slot.
Se sei nuovo allo Shadow DOM, leggi prima Shadow DOM per le basi di attachShadow() e delle shadow root, e Web Components per capire come gli slot si integrano con gli elementi personalizzati e i template.
Light DOM vs. shadow DOM
La composizione coinvolge due alberi:
- Light DOM — il markup che l'utente scrive tra i tag del tuo elemento:
<my-card>...questa parte...</my-card>. Risiede nel documento normale e rimane lì. - Shadow DOM — il markup che tu colleghi con
attachShadow(). È incapsulato e non direttamente raggiungibile dal documento esterno.
Un <slot> è una finestra: si trova nello shadow DOM e proietta i figli del light DOM al suo interno. I nodi del light DOM non vengono spostati — vengono solo visualizzati nella posizione dello slot. Questa vista combinata è l'albero appiattito, ed è ciò che il browser effettivamente renderizza e applica agli stili.
Capire gli slot nello Shadow DOM
Uno slot è un segnaposto nel tuo shadow DOM in cui il browser inserisce il contenuto fornito dal light DOM. Gli slot sono il modo in cui un componente generico permette a ogni istanza di apparire diversa pur condividendo un unico template interno.
Definire uno slot predefinito
Usa l'elemento <slot>. Uno slot senza attributo name è lo slot predefinito: cattura qualsiasi figlio del light DOM che non ha un attributo slot. Il testo all'interno di <slot> è il contenuto di fallback, mostrato solo quando non viene assegnato nulla.
<body>
<script>
class CustomElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<div class="container">
<slot>Default content</slot>
</div>
`;
}
}
customElements.define('custom-element', CustomElement);
</script>
<!-- No children: the slot shows its fallback, "Default content" -->
<custom-element></custom-element>
<!-- Children with no slot attribute go into the default slot -->
<custom-element><strong>Hello from the light DOM!</strong></custom-element>
</body>Il primo <custom-element> mostra "Default content" perché non è stato assegnato nulla. Il secondo mostra il testo in grassetto — il suo figlio del light DOM sostituisce il fallback. Si noti che il markup rimane nel documento; lo slot lo visualizza soltanto.
Slot con nome
Quando un componente ha più di un punto di inserimento, assegna un name a ogni <slot> e abbinalo dal light DOM con un attributo slot="...". È così che si instrada il contenuto corretto nel posto giusto.
<body>
<!-- Define Custom Element -->
<script>
// Define Custom Element Class
class CustomElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
/* Define styles for the component */
.container {
border: 1px solid #ccc;
padding: 20px;
}
</style>
<div class="container">
<slot name="content">Default content</slot>
</div>
`;
}
}
// Define Custom Element
customElements.define('custom-element', CustomElement);
</script>
<!-- Displaying the custom element -->
<custom-element>
<div slot="content">Content from parent</div>
</custom-element>
</body>Il <div slot="content"> viene abbinato a <slot name="content">, quindi "Content from parent" sostituisce il fallback. Qualsiasi cosa che non corrisponda a uno slot con nome passerebbe a uno slot predefinito, se esiste, oppure semplicemente non verrebbe renderizzata.
Migliorare la composizione nello Shadow DOM
La composizione nel contesto dello Shadow DOM si riferisce all'assemblaggio di componenti UI e contenuti combinando slot e il loro contenuto distribuito per creare strutture più complesse e riutilizzabili. Applicata nel contesto dello Shadow DOM, la composizione consente la creazione di web component altamente personalizzabili e riutilizzabili.
Per applicare stili al contenuto distribuito negli slot dal genitore, usa lo pseudo-elemento CSS ::slotted() — ad esempio, ::slotted(div) { color: blue; }. Vedi Shadow DOM Styling per il quadro completo di ::slotted(), :host e le proprietà personalizzate CSS.
Comporre componenti con gli slot
Un modo potente per sfruttare la composizione è combinare più slot in un layout strutturato. Qui un componente composito definisce le regioni header, content e footer:
<body>
<script>
// Define Composite Element Class
class CompositeElement extends HTMLElement {
constructor() {
super();
const shadowRoot = this.attachShadow({ mode: 'open' });
shadowRoot.innerHTML = `
<style>
/* Define styles for the composite component */
.container {
border: 1px solid #ccc;
padding: 20px;
}
</style>
<div class="container">
<slot name="header"></slot>
<slot name="content"></slot>
<slot name="footer"></slot>
</div>
`;
}
}
// Define Composite Element
customElements.define('composite-element', CompositeElement);
</script>
<composite-element>
<div slot="header">Header</div>
<div slot="content">Content</div>
<div slot="footer">Footer</div>
</composite-element>
</body>Ogni figlio con slot="..." viene instradato al corrispondente slot con nome, producendo un layout pulito header/content/footer che ogni istanza può popolare diversamente.
Reagire ai cambiamenti degli slot con slotchange
Il contenuto slottato è dinamico: un consumatore può aggiungere, rimuovere o sostituire i figli del light DOM in qualsiasi momento. L'evento slotchange si attiva su un <slot> ogni volta che cambiano i suoi nodi assegnati, così il tuo componente può reagire — ridisegnare un riepilogo, validare, effettuare lazy-load e così via. Ascoltalo dall'interno della shadow root:
<body>
<script>
class TabList extends HTMLElement {
constructor() {
super();
const root = this.attachShadow({ mode: 'open' });
root.innerHTML = `<p id="count"></p><slot></slot>`;
this._slot = root.querySelector('slot');
this._count = root.querySelector('#count');
}
connectedCallback() {
this._slot.addEventListener('slotchange', () => this.update());
this.update();
}
update() {
// assignedElements() returns only element nodes in the slot
const items = this._slot.assignedElements();
this._count.textContent = `Tabs: ${items.length}`;
}
}
customElements.define('tab-list', TabList);
</script>
<tab-list>
<button>One</button>
<button>Two</button>
</tab-list>
<script>
// Adding a child later fires slotchange → count updates to 3
const list = document.querySelector('tab-list');
const extra = document.createElement('button');
extra.textContent = 'Three';
list.appendChild(extra);
</script>
</body>Inizialmente il componente mostra "Tabs: 2". Quando viene aggiunto il terzo <button>, slotchange si attiva e il contatore si aggiorna a "Tabs: 3".
Leggere il contenuto slottato: assignedNodes() vs. assignedElements()
Entrambi i metodi vengono chiamati su un <slot> e restituiscono ciò che il browser gli ha assegnato dal light DOM:
slot.assignedNodes()restituisce tutti i nodi — elementi e nodi di testo (inclusi gli spazi bianchi tra i tag).slot.assignedElements()restituisce solo i nodi elemento. Di solito è questo ciò di cui hai bisogno.
Passa { flatten: true } per scendere negli slot annidati quando gli slot sono concatenati tra componenti:
// All nodes, including stray text/whitespace nodes
slot.assignedNodes(); // e.g. [text, <button>, text, <button>, text]
// Elements only — cleaner for iteration
slot.assignedElements(); // e.g. [<button>, <button>]
// Flatten through nested <slot> assignments
slot.assignedElements({ flatten: true });Preferisci assignedElements() a meno che tu non abbia specificamente bisogno dei nodi di testo; ti evita di filtrare gli spazi bianchi.
L'albero appiattito, riepilogato
Il browser non sposta letteralmente i nodi del light DOM nello shadow DOM. Invece costruisce un albero appiattito sostituendo ogni slot con i suoi nodi assegnati per la renderizzazione e lo stile. Conseguenze pratiche:
- Gli elementi slottati rimangono nel documento, quindi
document.querySelector()li trova ancora e i loroclass/idoriginali si applicano ancora. - Vengono stilizzati dal CSS della pagina, mentre il componente li raggiunge solo tramite
::slotted(). - I listener di eventi collegati nel light DOM continuano a funzionare — gli eventi si propagano attraverso l'albero appiattito.
Conclusione
Gli slot e la composizione trasformano uno shadow DOM incapsulato in un componente flessibile e riutilizzabile: tu definisci la struttura, e i consumatori forniscono il contenuto tramite slot predefiniti e con nome. Ricorda gli elementi chiave — light DOM vs. shadow DOM, l'albero appiattito che il browser renderizza, l'evento slotchange per reagire ai cambiamenti, e assignedElements() per leggere ciò che è stato slottato.
Per approfondire, vedi Web Components per il quadro generale, Custom Elements per il ciclo di vita degli elementi, Shadow DOM Styling per ::slotted() e :host, e Shadow DOM per i fondamentali.