Utilizzo delle slot in Vue.js
Pubblicato: 2022-03-10Con il recente rilascio di Vue 2.6, la sintassi per l'utilizzo degli slot è stata resa più concisa. Questa modifica agli slot mi ha riinteressato a scoprire il potenziale potere degli slot di fornire riutilizzabilità, nuove funzionalità e leggibilità più chiara ai nostri progetti basati su Vue. Di cosa sono veramente capaci le slot?
Se non conosci Vue o non hai visto le modifiche dalla versione 2.6, continua a leggere. Probabilmente la migliore risorsa per conoscere gli slot è la documentazione di Vue, ma cercherò di fare una carrellata qui.
Cosa sono le slot?
Gli slot sono un meccanismo per i componenti Vue che ti consente di comporre i tuoi componenti in un modo diverso dalla stretta relazione genitore-figlio. Gli slot ti offrono uno sbocco per posizionare i contenuti in nuovi posti o rendere i componenti più generici. Il modo migliore per capirli è vederli in azione. Iniziamo con un semplice esempio:
// frame.vue <template> <div class="frame"> <slot></slot> </div> </template>
Questo componente ha un wrapper div
. Facciamo finta che div
sia lì per creare una cornice stilistica attorno al suo contenuto. Questo componente può essere utilizzato genericamente per avvolgere una cornice attorno a qualsiasi contenuto desiderato. Vediamo come appare ad usarlo. Il componente del frame
qui si riferisce al componente che abbiamo appena creato sopra.
// app.vue <template> <frame><img src="an-image.jpg"></frame> </template>
Il contenuto che si trova tra i tag del frame
di apertura e di chiusura verrà inserito nel componente del frame
in cui si trova lo slot
, sostituendo i tag dello slot
. Questo è il modo più semplice per farlo. Puoi anche specificare il contenuto predefinito da inserire in uno slot semplicemente compilandolo:
// frame.vue <template> <div class="frame"> <slot>This is the default content if nothing gets specified to go here</slot> </div> </template>
Quindi ora se lo usiamo in questo modo invece:
// app.vue <template> <frame /> </template>
Verrà visualizzato il testo predefinito di "Questo è il contenuto predefinito se non viene specificato nulla per andare qui", ma se lo usiamo come abbiamo fatto prima, il testo predefinito verrà sovrascritto dal tag img
.
Slot multipli/con nome
Puoi aggiungere più slot a un componente, ma se lo fai, tutti tranne uno devono avere un nome. Se ce n'è uno senza nome, è lo slot predefinito. Ecco come creare più slot:
// titled-frame.vue <template> <div class="frame"> <header><h2><slot name="header">Title</slot></h2></header> <slot>This is the default content if nothing gets specified to go here</slot> </div> </template>
Abbiamo mantenuto lo stesso slot predefinito, ma questa volta abbiamo aggiunto uno slot denominato header
in cui è possibile inserire un titolo. Lo usi in questo modo:
// app.vue <template> <titled-frame> <template v-slot:header> <!-- The code below goes into the header slot --> My Image's Title </template> <!-- The code below goes into the default slot --> <img src="an-image.jpg"> </titled-frame> </template>
Proprio come prima, se vogliamo aggiungere contenuto allo slot predefinito, inserirlo direttamente all'interno del componente titled-frame
. Per aggiungere contenuto a uno slot con nome, tuttavia, dovevamo racchiudere il codice in un tag template
con una direttiva v-slot
. Aggiungi i due punti ( :
) dopo lo v-slot
e quindi scrivi il nome dello slot a cui desideri passare il contenuto. Nota che lo v-slot
è una novità per Vue 2.6, quindi se stai utilizzando una versione precedente, dovrai leggere i documenti sulla sintassi dello slot deprecata.
Slot con mirino
Un'altra cosa che devi sapere è che gli slot possono trasmettere dati/funzioni ai loro figli. Per dimostrarlo, avremo bisogno di un componente di esempio completamente diverso con gli slot, uno che sia ancora più artificioso del precedente: copiamo l'esempio dai documenti creando un componente che fornisce i dati sull'utente corrente ai suoi slot:
// current-user.vue <template> <span> <slot v-bind:user="user"> {{ user.lastName }} </slot> </span> </template> <script> export default { data () { return { user: ... } } } </script>
Questo componente ha una proprietà chiamata user
con i dettagli sull'utente. Per impostazione predefinita, il componente mostra il cognome dell'utente, ma si noti che utilizza v-bind
per associare i dati dell'utente allo slot. Con ciò, possiamo utilizzare questo componente per fornire i dati dell'utente al suo discendente:
// app.vue <template> <current-user> <template v-slot:default="slotProps">{{ slotProps.user.firstName }}</template> </current-user> </template>
Per accedere ai dati passati allo slot, specifichiamo il nome della variabile scope con il valore della direttiva v-slot
.
Ci sono alcune note da prendere qui:
- Abbiamo specificato il nome di
default
, anche se non è necessario per lo slot predefinito. Invece potremmo semplicemente usarev-slot="slotProps"
. - Non è necessario utilizzare
slotProps
come nome. Puoi chiamarlo come vuoi. - Se stai utilizzando solo uno slot predefinito, puoi saltare quel tag del
template
interno e inserire la direttivav-slot
direttamente nel tagcurrent-user
. - È possibile utilizzare la destrutturazione degli oggetti per creare riferimenti diretti ai dati dello slot con ambito anziché utilizzare un singolo nome di variabile. In altre parole, puoi usare
v-slot="{user}"
invece div-slot="slotProps"
e poi puoi usareuser
direttamente invece dislotProps.user
.
Tenendo conto di queste note, l'esempio sopra può essere riscritto in questo modo:
// app.vue <template> <current-user v-slot="{user}"> {{ user.firstName }} </current-user> </template>
Un altro paio di cose da tenere a mente:
- Puoi associare più di un valore con le direttive
v-bind
. Quindi nell'esempio, avrei potuto fare più di un sempliceuser
. - Puoi anche passare le funzioni agli slot con scope. Molte librerie lo usano per fornire componenti funzionali riutilizzabili come vedrai in seguito.
-
v-slot
ha un alias di#
. Quindi, invece di scriverev-slot:header="data"
, puoi scrivere#header="data"
. Puoi anche specificare#header
invece div-slot:header
quando non stai usando slot con scope. Per quanto riguarda gli slot predefiniti, dovrai specificare il nome didefault
quando usi l'alias. In altre parole, dovrai scrivere#default="data"
invece di#="data"
.
Ci sono alcuni altri punti minori che puoi imparare dai documenti, ma questo dovrebbe essere sufficiente per aiutarti a capire di cosa stiamo parlando nel resto di questo articolo.
Cosa puoi fare con le slot?
Le slot non sono state create per un unico scopo, o almeno se lo erano, si sono evolute ben oltre l'intenzione originale di essere uno strumento potente per fare molte cose diverse.
Modelli riutilizzabili
I componenti sono sempre stati progettati per poter essere riutilizzati, ma alcuni modelli non sono pratici da applicare con un singolo componente "normale" perché il numero di props
di scena necessari per personalizzarlo può essere eccessivo o necessario passare ampie sezioni di contenuto e potenzialmente altri componenti attraverso gli props
di scena. Gli slot possono essere utilizzati per racchiudere la parte "esterna" del pattern e consentire ad altri componenti HTML e/o posizionati al loro interno per personalizzare la parte "interna", consentendo al componente con slot di definire il pattern e i componenti iniettati nel slot per essere unici.
Per il nostro primo esempio, iniziamo con qualcosa di semplice: un pulsante. Immagina che tu e il tuo team stiate utilizzando Bootstrap*. Con Bootstrap, i tuoi pulsanti sono spesso legati con la classe base `btn` e una classe che specifica il colore, come `btn-primary`. Puoi anche aggiungere una classe di dimensioni, come `btn-lg`.
* Non ti incoraggio né ti scoraggio dal farlo, avevo solo bisogno di qualcosa per il mio esempio ed è abbastanza noto.
Assumiamo ora, per semplicità, che la tua app/sito utilizzi sempre btn-primary
e btn-lg
. Non vuoi dover sempre scrivere tutte e tre le classi sui pulsanti, o forse non ti fidi di un principiante che si ricordi di farle tutte e tre. In tal caso, puoi creare un componente che abbia automaticamente tutte e tre queste classi, ma come consenti la personalizzazione del contenuto? Un prop
non è pratico perché un tag button
può contenere tutti i tipi di HTML, quindi dovremmo usare uno slot.
<!-- my-button.vue --> <template> <button class="btn btn-primary btn-lg"> <slot>Click Me!</slot> </button> </template>
Ora possiamo usarlo ovunque con qualsiasi contenuto desideri:
<!-- somewhere else, using my-button.vue --> <template> <my-button> <img src="/img/awesome-icon.jpg"> SMASH THIS BUTTON TO BECOME AWESOME FOR ONLY $500!!! </my-button> </template>
Certo, puoi andare con qualcosa di molto più grande di un pulsante. Rimanendo con Bootstrap, diamo un'occhiata a una parte modale, o almeno alla parte HTML; Non entrerò nella funzionalità... ancora.
<!-- my-modal.vue --> <template> <div class="modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <slot name="header"></slot> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <slot name="body"></slot> </div> <div class="modal-footer"> <slot name="footer"></slot> </div> </div> </div> </div> </template>
Ora, usiamo questo:
<!-- somewhere else, using my-modal.vue --> <template> <my-modal> <template #header><!-- using the shorthand for `v-slot` --> <h5>Awesome Interruption!</h5> </template> <template #body> <p>We interrupt your use of our application to let you know that this application is awesome and you should continue using it every day for the rest of your life!</p> </template> <template #footer> <em>Now back to your regularly scheduled app usage</em> </template> </my-modal> </template>
Il tipo di caso d'uso sopra per gli slot è ovviamente molto utile, ma può fare anche di più.
Riutilizzo della funzionalità
I componenti Vue non riguardano solo HTML e CSS. Sono costruiti con JavaScript, quindi riguardano anche la funzionalità. Gli slot possono essere utili per creare funzionalità una volta e utilizzarle in più posizioni. Torniamo al nostro esempio modale e aggiungiamo una funzione che chiude il modale:
<!-- my-modal.vue --> <template> <div class="modal" tabindex="-1" role="dialog"> <div class="modal-dialog" role="document"> <div class="modal-content"> <div class="modal-header"> <slot name="header"></slot> <button type="button" class="close" data-dismiss="modal" aria-label="Close"> <span aria-hidden="true">×</span> </button> </div> <div class="modal-body"> <slot name="body"></slot> </div> <div class="modal-footer"> <!-- using `v-bind` shorthand to pass the `closeModal` method to the component that will be in this slot --> <slot name="footer" :closeModal="closeModal"></slot> </div> </div> </div> </div> </template> <script> export default { //... methods: { closeModal () { // Do what needs to be done to close the modal... and maybe remove it from the DOM } } } </script>
Ora quando usi questo componente, puoi aggiungere un pulsante al piè di pagina che può chiudere il modale. Normalmente, nel caso di una modale Bootstrap, puoi semplicemente aggiungere data-dismiss="modal"
a un pulsante, ma vogliamo nascondere le cose specifiche di Bootstrap lontano dai componenti che verranno inseriti in questo componente modale. Quindi passiamo loro una funzione che possono chiamare e non sono più saggi riguardo al coinvolgimento di Bootstrap:
<!-- somewhere else, using my-modal.vue --> <template> <my-modal> <template #header><!-- using the shorthand for `v-slot` --> <h5>Awesome Interruption!</h5> </template> <template #body> <p>We interrupt your use of our application to let you know that this application is awesome and you should continue using it every day for the rest of your life!</p> </template> <!-- pull in `closeModal` and use it in a button's click handler --> <template #footer="{closeModal}"> <button @click="closeModal"> Take me back to the app so I can be awesome </button> </template> </my-modal> </template>
Componenti senza rendering
E infine, puoi prendere ciò che sai sull'uso degli slot per passare funzionalità riutilizzabili ed eliminare praticamente tutto l'HTML e utilizzare semplicemente gli slot. Questo è essenzialmente ciò che è un componente senza rendering: un componente che fornisce solo funzionalità senza HTML.
Rendere i componenti veramente senza rendering può essere un po' complicato perché dovrai scrivere funzioni di render
piuttosto che usare un modello per rimuovere la necessità di un elemento radice, ma potrebbe non essere sempre necessario. Diamo un'occhiata a un semplice esempio che ci consente di utilizzare prima un modello, però:
<template> <transition name="fade" v-bind="$attrs" v-on="$listeners"> <slot></slot> </transition> </template> <style> .fade-enter-active, .fade-leave-active { transition: opacity 0.3s; } .fade-enter, .fade-leave-to { opacity: 0; } </style>
Questo è uno strano esempio di componente senza rendering perché non contiene nemmeno JavaScript. Ciò è dovuto principalmente al fatto che stiamo solo creando una versione riutilizzabile preconfigurata di una funzione incorporata senza rendering: transition
.
Sì, Vue ha componenti renderless integrati. Questo particolare esempio è tratto da un articolo sulle transizioni riutilizzabili di Cristi Jora e mostra un modo semplice per creare un componente senza rendering in grado di standardizzare le transizioni utilizzate nell'applicazione. L'articolo di Cristi va molto più in profondità e mostra alcune varianti più avanzate delle transizioni riutilizzabili, quindi consiglio di verificarlo.
Per il nostro altro esempio, creeremo un componente che gestisce il cambio di ciò che viene mostrato durante i diversi stati di una promessa: in sospeso, risolto con successo e fallito. È un modello comune e sebbene non richieda molto codice, può confondere molti dei tuoi componenti se la logica non viene estratta per la riutilizzabilità.
<!-- promised.vue --> <template> <span> <slot name="rejected" v-if="error" :error="error"></slot> <slot name="resolved" v-else-if="resolved" :data="data"></slot> <slot name="pending" v-else></slot> </span> </template> <script> export default { props: { promise: Promise }, data: () => ({ resolved: false, data: null, error: null }), watch: { promise: { handler (promise) { this.resolved = false this.error = null if (!promise) { this.data = null return } promise.then(data => { this.data = data this.resolved = true }) .catch(err => { this.error = err this.resolved = true }) }, immediate: true } } } </script>
Allora cosa sta succedendo qui? In primo luogo, nota che stiamo ricevendo un sostegno chiamato promise
che è una Promise
. Nella sezione di watch
osserviamo le modifiche alla promessa e quando cambia (o immediatamente alla creazione del componente grazie alla proprietà immediate
) cancelliamo lo stato, then
chiamiamo e catch
la promessa, aggiornando lo stato quando termina con successo o non riesce.
Quindi, nel modello, mostriamo uno slot diverso in base allo stato. Nota che non siamo riusciti a mantenerlo veramente senza rendering perché avevamo bisogno di un elemento radice per utilizzare un modello. Stiamo passando data
ed error
anche agli ambiti slot pertinenti.
Ed ecco un esempio del suo utilizzo:
<template> <div> <promised :promise="somePromise"> <template #resolved="{ data }"> Resolved: {{ data }} </template> <template #rejected="{ error }"> Rejected: {{ error }} </template> <template #pending> Working on it... </template> </promised> </div> </template> ...
Passiamo in somePromise
al componente renderless. Mentre aspettiamo che finisca, visualizziamo "Ci sto lavorando..." grazie allo slot pending
. Se riesce, visualizziamo "Risolto:" e il valore della risoluzione. Se fallisce, visualizziamo "Rifiutato:" e l'errore che ha causato il rifiuto. Ora non abbiamo più bisogno di tenere traccia dello stato della promessa all'interno di questo componente perché quella parte viene estratta nel proprio componente riutilizzabile.
Quindi, cosa possiamo fare per quell'intervallo che avvolge gli promised.vue
span
Per rimuoverlo, dovremo rimuovere la parte del template
e aggiungere una funzione di render
al nostro componente:
render () { if (this.error) { return this.$scopedSlots['rejected']({error: this.error}) } if (this.resolved) { return this.$scopedSlots['resolved']({data: this.data}) } return this.$scopedSlots['pending']() }
Non c'è niente di troppo complicato qui. Stiamo solo usando alcuni blocchi if
per trovare lo stato e quindi restituendo lo slot con ambito corretto (tramite this.$scopedSlots['SLOTNAME'](...)
) e passando i dati rilevanti all'ambito dello slot. Quando non stai utilizzando un modello, puoi saltare l'utilizzo dell'estensione del file .vue
estraendo JavaScript dal tag di script
e semplicemente inserendolo in un file .js
. Questo dovrebbe darti un leggero aumento delle prestazioni durante la compilazione di quei file Vue.
Questo esempio è una versione ridotta e leggermente modificata di vue-promised, che consiglierei di utilizzare l'esempio precedente perché copre alcune potenziali insidie. Ci sono anche molti altri ottimi esempi di componenti senza rendering là fuori. Baleada è un'intera libreria piena di componenti senza rendering che forniscono funzionalità utili come questa. C'è anche vue-virtual-scroller per controllare il rendering dell'elemento dell'elenco in base a ciò che è visibile sullo schermo o PortalVue per "teletrasportare" il contenuto in parti completamente diverse del DOM.
Sono fuori
Le slot di Vue portano lo sviluppo basato sui componenti a un livello completamente nuovo e, sebbene io abbia dimostrato molti ottimi modi in cui le slot possono essere utilizzate, ce ne sono innumerevoli altre là fuori. A quale grande idea ti viene in mente? In che modo pensi che le slot possano ottenere un aggiornamento? Se ne hai, assicurati di portare le tue idee al team Vue. Dio benedica e felice codifica.