Slot
Si assume che tu abbia già letto le Basi dei componenti. Leggi prima quello se sei nuovo al concetto di componente.
Contenuto e outlet degli slot
Abbiamo imparato che i componenti possono accettare props, che possono essere valori JavaScript di qualsiasi tipo. Ma cosa succede con il contenuto del template? In alcuni casi, potremmo voler passare un frammento di template a un componente figlio e lasciare che il componente figlio renda il frammento all'interno del suo stesso template.
Ad esempio, potremmo avere un componente <FancyButton>
che supporta l'utilizzo come segue:
template
<FancyButton>
Cliccami! <!-- contenuto slot -->
</FancyButton>
Ed il template di <FancyButton>
sarà:
template
<button class="fancy-btn">
<slot></slot> <!-- outlet slot -->
</button>
L'elemento <slot>
è un outlet per slot che indica dove il contentuto dello slot fornito dal genitore dovrebbe essere renderizzato.
E il DOM renderizzato alla fine:
html
<button class="fancy-btn">Cliccami!</button>
Con gli slot, il componente <FancyButton>
è responsabile di rendere l'elemento <button>
esterno (e il suo stile), mentre il contenuto interno è fornito dal componente genitore.
Un altro modo per comprendere gli slot è confrontarli con le funzioni JavaScript:
js
// componente genitore che passa il contenuto dello slot
FancyButton('Cliccami!')
// FancyButton rende il contenuto dello slot nel proprio template
function FancyButton(slotContent) {
return `<button class="fancy-btn">
${slotContent}
</button>`
}
Il contenuto dello slot non è limitato solo al testo. Può essere qualsiasi contenuto di template valido. Ad esempio, possiamo passare più elementi o addirittura altri componenti:
template
<FancyButton>
<span style="color:red">Cliccami!</span>
<AwesomeIcon name="plus" />
</FancyButton>
Utilizzando gli slot, il nostro <FancyButton>
è più flessibile e riutilizzabile. Ora possiamo utilizzarlo in diversi luoghi con contenuti interni diversi, ma tutti con lo stesso stile elegante.
Il meccanismo degli slot dei componenti Vue è ispirato dall'elemento nativo <slot>
dei Web Component, ma con funzionalità aggiuntive che vedremo più avanti.
Ambito di rendering
Il contenuto dello slot ha accesso all'ambito dei dati del componente genitore, poiché è definito nel genitore. Ad esempio:
template
<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>
Qui entrambe le interpolazioni {{ message }}
renderanno lo stesso contenuto.
Il contenuto dello slot non ha accesso ai dati del componente figlio. Le espressioni nei template Vue possono accedere solo all'ambito in cui sono definite, in linea con l'ambito lessicale di JavaScript. In altre parole:
Le espressioni nel template genitore hanno accesso solo all'ambito del genitore; le espressioni nel template figlio hanno accesso solo all'ambito del figlio.
Contenuto di fallback
Ci sono casi in cui è utile specificare un contenuto di fallback (cioè predefinito) per uno slot, da renderizzare solo quando non viene fornito alcun contenuto. Ad esempio, in un componente <SubmitButton>
:
template
<button type="submit">
<slot></slot>
</button>
Potremmo voler renderizzare il testo "Invia" all'interno del <button>
se il genitore non fornisce alcun contenuto per lo slot. Per fare di "Invia" il contenuto di fallback, possiamo inserirlo tra i tag <slot>
:
template
<button type="submit">
<slot>
Submit <!-- contenuto fallback -->
</slot>
</button>
Ora, quando usiamo <SubmitButton>
in un componente genitore, senza fornire alcun contenuto per lo slot:
template
<SubmitButton />
Verrà renderizzato il contenuto di fallback, "Invia":
html
<button type="submit">Submit</button>
Ma se forniamo del contenuto:
template
<SubmitButton>Save</SubmitButton>
Allora il contenuto fornito verrà renderizzato al suo posto:
html
<button type="submit">Save</button>
Slot con nome
Ci sono momenti in cui è utile avere più slot in un singolo componente. Ad esempio, in un componente <BaseLayout>
con il seguente template:
template
<div class="container">
<header>
<!-- We want header content here -->
</header>
<main>
<!-- We want main content here -->
</main>
<footer>
<!-- We want footer content here -->
</footer>
</div>
Per questi casi, l'elemento <slot>
ha un attributo speciale, name
, che può essere usato per assegnare un ID univoco a diversi slot in modo da poter determinare dove il contenuto deve essere renderizzato:
template
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
Un outlet <slot>
senza name
ha implicitamente il nome "default".
In un componente genitore che utilizza <BaseLayout>
, abbiamo bisogno di un modo per passare più frammenti di contenuto slot, ognuno destinato a un diverso slot. Ecco dove entrano in gioco gli slot con nome.
Per passare uno slot con nome, dobbiamo utilizzare un elemento <template>
con la direttiva v-slot
, e quindi passare il nome dello slot come argomento av-slot
:
template
<BaseLayout>
<template v-slot:header>
<!-- contenuto per l'header dello slot -->
</template>
</BaseLayout>
La direttiva v-slot
ha un abbreviazione dedicata, #
, quindi <template v-slot:header>
può essere abbreviato semplicemente come <template #header>
. Pensalo come "renderizza questo frammento di template nello slot 'header' del componente figlio".
Ecco il codice che passa il contenuto per tutti e tre gli slot a <BaseLayout>
utilizzando la sintassi abbreviata:
template
<BaseLayout>
<template #header>
<h1>Qui potrebbe esserci un titolo</h1>
</template>
<template #default>
<p>Un paragrafo per il contenuto principale</p>
<p>Un altro.</p>
</template>
<template #footer>
<p>Qua ci sono delle info di contatto</p>
</template>
</BaseLayout>
Quando un componente accetta sia uno slot predefinito che slot con nomi, tutti i nodi di primo livello non <template>
sono implicitamente considerati contenuto per lo slot predefinito. Pertanto, il codice sopra può essere scritto anche come:
template
<BaseLayout>
<template #header>
<h1>Qui potrebbe esserci un titolo</h1>
</template>
<!-- implicit default slot -->
<p>Un paragrafo per il contenuto principale</p>
<p>Un altro.</p>
<template #footer>
<p>Qua ci sono delle info di contatto</p>
</template>
</BaseLayout>
Ora, tutto ciò che è contenuto all'interno degli elementi <template>
sarà passato agli slot corrispondenti. L'HTML renderizzato finale sarà:
html
<div class="container">
<header>
<h1>Qui potrebbe esserci un titolo</h1>
</header>
<main>
<p>Un paragrafo per il contenuto principale</p>
<p>Un altro.</p>
</main>
<footer>
<p>Qua ci sono delle info di contatto</p>
</footer>
</div>
Ancora una volta, potrebbe aiutare a comprendere meglio gli slot con nomi usando l'analogia delle funzioni JavaScript:
js
// passaggio di frammenti slot multipli con nomi diversi
BaseLayout({
header: `...`,
default: `...`,
footer: `...`
})
// <BaseLayout> li renderizza in posizioni diverse
function BaseLayout(slots) {
return `<div class="container">
<header>${slots.header}</header>
<main>${slots.default}</main>
<footer>${slots.footer}</footer>
</div>`
}
Nomi di slot dinamici
Gli argomenti dinamici delle direttive funzionano anche su v-slot
, consentendo la definizione di nomi di slot dinamici:
template
<base-layout>
<template v-slot:[nomeDinamicoSlot]>
...
</template>
<!-- con abbreviazione -->
<template #[nomeDinamicoSlot]>
...
</template>
</base-layout>
Tieni presente che l'espressione è soggetta ai vincoli di sintassi degli argomenti dinamici delle direttive.
Slot con lo 'scope'
Come discusso in Render Scope, il contenuto dello slot non ha accesso allo stato nel componente figlio.
Tuttavia, ci sono casi in cui potrebbe essere utile se il contenuto di uno slot può utilizzare dati sia dallo scope del genitore che dallo scope del figlio. Per ottenere ciò, abbiamo bisogno di un modo per far sì che il figlio possa passare dati a uno slot quando lo sta rendendo.
Infatti, possiamo fare esattamente questo: possiamo passare attributi a un punto di inserimento per uno slot, proprio come facciamo per le props di un componente:
template
<!-- template di <MyComponent> -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
Ricevere le props dello slot è un po' diverso quando si utilizza un singolo slot predefinito rispetto all'utilizzo di slot con nomi. Mostreremo prima come ricevere le props utilizzando un singolo slot predefinito, utilizzando v-slot
direttamente sul tag del componente figlio:
template
<MyComponent v-slot="slotProps">
{{ slotProps.text }} {{ slotProps.count }}
</MyComponent>
Le props passate allo slot dal componente figlio sono disponibili come valore della direttiva v-slot
corrispondente, a cui si può avere accesso dalle espressioni all'interno dello slot.
Puoi pensare a uno slot con scope come a una funzione che viene passata al componente figlio. Il componente figlio la chiama poi, passando le props come argomenti:
js
MyComponent({
// passaggio dello slot predefinito, ma come funzione
default: (slotProps) => {
return `${slotProps.text} ${slotProps.count}`
}
})
function MyComponent(slots) {
const greetingMessage = 'ciao'
return `<div>${
// chiamata alla funzione dello slot con le props
slots.default({ text: greetingMessage, count: 1 })
}</div>`
}
n realtà, questo è molto simile a come gli slot con scope vengono compilati e come li useresti nelle funzioni di renderizzazione manuali.
Nota come v-slot="slotProps"
corrisponde alla firma della funzione dello slot. Proprio come con gli argomenti delle funzioni, possiamo usare la destrutturazione in v-slot
:
template
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>
Slot con scope nominati
Gli slot con scope nominati funzionano in modo simile: le props dello slot sono accessibili come valore della direttiva v-slot
: v-slot:name="slotProps"
. Utilizzando la sintassi abbreviata, appare così:
template
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
<template #footer="footerProps">
{{ footerProps }}
</template>
</MyComponent>
Passare le props a uno slot nominato:
template
<slot name="header" message="ciao"></slot>
Nota che il name
di uno slot non verrà incluso nelle props perché è riservato, quindi le headerProps
risultanti sarebbero { message: 'ciao' }
.
Se stai mischiando gli slot nominati con lo slot con scope predefinito, è necessario utilizzare un tag <template>
esplicito per lo slot predefinito. Tentare di posizionare la direttiva v-slot
direttamente sul componente causerà un errore di compilazione. Questo per evitare qualsiasi ambiguità riguardo allo scope delle props dello slot predefinito. Ad esempio:
template
<!-- Questo template non verrà compilato -->
<template>
<MyComponent v-slot="{ message }">
<p>{{ message }}</p>
<template #footer>
<!-- message appartiene allo slot predefinito e non è disponibile qui -->
<p>{{ message }}</p>
</template>
</MyComponent>
</template>
Utilizzare un tag <template>
esplicito per lo slot predefinito aiuta a rendere chiaro che la prop message
non è disponibile all'interno dell'altro slot:
template
<template>
<MyComponent>
<!-- Usa uno slot predefinito esplicito -->
<template #default="{ message }">
<p>{{ message }}</p>
</template>
<template #footer>
<p>Qua ci sono delle info di contatto</p>
</template>
</MyComponent>
</template>
Esempio di lista fantasia
Potresti chiederti quale potrebbe essere un buon caso d'uso per gli slot con scope. Ecco un esempio: immagina un componente <FancyList>
che renderizza una lista di elementi. Questo componente potrebbe includere la logica per caricare dati remoti, utilizzare i dati per mostrare una lista, o anche funzionalità avanzate come la paginazione o lo scorrimento infinito. Tuttavia, vogliamo che sia flessibile rispetto all'aspetto di ciascun elemento e lasciare lo stile di ogni elemento al componente genitore che lo consuma. Quindi l'utilizzo desiderato potrebbe apparire così:
template
<FancyList :api-url="url" :per-page="10">
<template #item="{ body, username, likes }">
<div class="item">
<p>{{ body }}</p>
<p>by {{ username }} | {{ likes }} likes</p>
</div>
</template>
</FancyList>
All'interno di <FancyList>
, possiamo renderizzare lo stesso <slot>
più volte con diversi dati dell'elemento (nota che stiamo usando v-bind
per passare un oggetto come props dello slot):
template
<ul>
<li v-for="item in items">
<slot name="item" v-bind="item"></slot>
</li>
</ul>
Componenti senza renderizzazione
Il caso d'uso di <FancyList>
che abbiamo discusso in precedenza incapsula sia la logica riutilizzabile (recupero dati, paginazione, ecc.) che l'output visivo, delegando parte dell'output visivo al componente consumatore tramite gli slot con scope.
Se spingiamo un po' più avanti questo concetto, possiamo arrivare a dei componenti che incapsulano solo la logica e non renderizzano nulla da soli: l'output visivo è completamente delegato al componente consumatore tramite gli slot con scope. Chiamiamo questo tipo di componente un Componente senza renderizzazione / renderless.
Un esempio di componente renderless potrebbe essere uno che incapsula la logica per tracciare la posizione attuale del mouse:
template
<MouseTracker v-slot="{ x, y }">
Il mouse è a: {{ x }}, {{ y }}
</MouseTracker>
Sebbene sia un pattern interessante, gran parte di ciò che può essere ottenuto con i Renderless Components può essere realizzato in modo più efficiente utilizzando Composition API, senza incorrere nel costo aggiuntivo dell'annidamento eccessivo dei componenti. Più avanti, vedremo come possiamo implementare la stessa funzionalità di tracciamento del mouse come Composable.
Detto ciò, gli scoped slots rimangono utili nei casi in cui è necessario sia incapsulare la logica che comporre l'output visivo, come nell'esempio di <FancyList>
.