How I finally understood Vue scoped slots.
The real world project that finally made me understand scoped slots, put it into practice, and pushed me to write about it.
Vue scoped slots seemed like a confusing set of magic and mystery to me for a while.
But recently I needed a way to run some functions on data, that was provided inside the existing component. Normally I would run a method or use a computed property, but the way the data is returned in the component from the API (and indeed for a lot of the components we are using) means we have to go one level deeper with components. This data was also going to be used in a few different places so I wanted to be able to use the 'function' in multiple places and then display that content in multiple different ways.
Problem
Consider an agenda for an event that may take place over 3 days. I get a list of agenda entries and I want to:
- sort them in time order,
- group them by day,
- expose just a single day (today)
- show some speaker images,
- have links to different things.
I tried at first using a monolithic component that could handle both functionality and UI, using props, and options and the context of where it was used. This did not get very far. I ended up having so many different v-ifs for edge cases and options for showing and hiding certain things.
I thought about making some sort of global function but then I read the documentation again and realised this was the perfect use case for scoped slots.
Scoped Slots solution
I can pass my data into a new component, use the functions inside that component and then instead of trying to create the UI inside the same component, I would pass the data back out and expose it with scoped slots.
No longer would I need to pass down lots of props and options.
Instead, I can render the utility component, pass the data in, and get the data that I need back. Then, depending on where I use the component I can use smaller components that will be more manageable and easier to update.
parent.vue
<AgendaUtility v-slot="{ groupedAgenda, today }" :items="agenda.entries">
<UiTabs :selected="today.index">
<UiTab
v-for="day in groupedAgenda"
:id="day.date"
:key="day.date"
:title="day.name"
>
<AgendaDayHeader :day="day" />
<AgendaEntryFull
v-for="(entry, index) in day.entries"
:key="index"
:entry="entry"
/>
</UiTab>
</UiTabs>
</AgendaUtility>
AgendaUtility.vue
<template>
<div>
<slot
:groupedAgenda="groupedAgenda"
:today="todayAgenda"
:formatDay="formatDay"
></slot>
</div>
</template>
<script>
export default {
computed: {
groupedAgenda() {
let ...
... return sortedArray
},
selectedTab() {
const today = this.groupedAgenda.find((element) => element.today)
return today.index
},
},
}
</script>
General Example
Thinking about this in a much simpler way: Imagine we have an array that is not sorted unsortedItems
, and we want to sort it.
We can enlist the help of the scoped slot.
We pass into the UtilityComponent
the unsortedItems
array via props, which it expects as items
, and then expose some data returned from the component through the v-slot
.
The special syntax v-slot="{ sortedItems }"
is just ES2015 destructuring - which allows the code to be cleaner.
parent.vue
<template>
<utility-component v-slot="{ sortedItems }" :items="unsortedItems" />
<ul>
<li v-for="item in sortedItems">
{{item}}
</li>
</ul>
</utility-component>
</template>
UtilityComponent.vue
<template>
<div>
<slot :sortedItems="sortedItems"></slot>
</div>
</template>
<script>
export default {
props: {
items: {
type: Array,
default: () => [],
},
},
computed: {
sortedItems() {
return [...this.items].sort((a, b) => (a.name > b.name ? 1 : -1));
},
},
};
</script>
Of course, this simple example is just to show the basic happenings and not an example of the best use case. The functions and data returned could be much more complex.