I recently figured out how to implement nested slots recursively, including how to do this with scoped slots.
It all started when I wanted to see if I could build a component that replicated the v-for
directive, but only using the template.
To make this story short: I did.
Rendering a list without using the v-for directive or render functions. Supports scoped slots as well... pic.twitter.com/fHlbJdbrEe
— Michael Thiessen (@MichaelThiessen) June 27, 2019
(follow me to get my tweets as I learn new stuff!)
And yes, it supports slots and scoped slots — and could be made to support named slots as well.
We can use it like this:
<template><div><!-- Regular list --><v-for :list="list" /><!-- List with bolded items --><v-for :list="list"><template v-slot="{ item }"><strong>{{ item }}</strong></template></v-for></div></template>
The first one will print out the list of items normally, while the second one will wrap each item in a <strong>
tag.
It's not a very useful component, but I like to have fun experimenting as I find I learn the most that way.
Let's take a look at what's going on here.
Normally when we'd want to render out a list of elements or components we'd use the v-for
directive. But we want to get rid of it completely.
So, how do we render out a list of items without using a loop?
Any guesses?
The answer is: recursion.
We can use recursion to represent a list of items.
It's actually not too complicated. I'll show you how this works.
One of my favourite courses in university was Programming Language Concepts.
The most interesting part of it to me was exploring functional programming and logic programming, and seeing the differences to imperative programming (Javascript and most popular languages are imperative).
This was the course that really opened my eyes on how to use recursion, since with pure functional languages everything is recursion.
Anyways, from that course I learned that you can represent a list recursively.
Instead of using an array, each list is a value (the head), and another list (the tail).
[head, tail]
So if you wanted to represent the list [1, 2, 3]
, it would be represented recursively as:
[1, [2, [3, null]]]
We have to end the list somehow, so we use null
instead of another array (an empty array would work too).
Maybe you're starting to see where I'm going with this...
We can use this concept and apply it to our components. Instead, we'll be nesting our components recursively in order to represent our list.
We'll end up rendering something like this. Notice the nested structure of our "list":
<div>1<div>2<div>3</div></div></div>
Admittedly, this isn't the exact same thing as what v-for
would render, but that's not exactly the point of this exercise either.
So now that we've worked out what we're doing, how do we create it?
First, I'd like to challenge you to try building it yourself before moving on. I think you'll be able to figure it out, and you'll learn more that way than by just reading how I did it.
Okay, now it's time for me to show how I solved this problem.
First, we'll tackle rendering out the list of items recursively. Once we get that working, we'll come back and figure out how to make the slots work.
Instead of using the recursive list we showed before, we'll work with a plain array:
[1, 2, 3]
We have to cover 2 cases here:
Let's pass [1, 2, 3]
to v-for
:
<template><v-for :list="[1, 2, 3]" /></template>
We want to grab the first item in the list, the 1
, and display it:
<template><div>{{ list[0] }}</div></template>
Now the component will render out 1
, like we expect it to.
But we can't just render out the first value and stop. We need to render out the value, and then render out the rest of the list as well:
<template><div>{{ list[0] }}<v-for :list="list.slice(1)" /></div></template>
Instead of passing in the whole list
array, we chop off the first item and pass down that new array. We've already printed out the first item, so no need to keep it around.
This is the sequence of things that are happening here:
[1, 2, 3]
into v-for
to renderv-for
component renders 1
, and then passes [2, 3]
into the next v-for
to render[2, 3]
and renders the 2
, and then passes [3]
into the next v-for
v-for
component then renders out the 3
, and we've printed out our list!The structure of our Vue app looks like this now:
<App><v-for><v-for><v-for /></v-for></v-for></App>
You can see that we have several v-for
components all nested within each other!
One last thing we need to do here though. We need to stop the recursion!
<template><div>{{ list[0] }}<v-forv-if="list.length > 1":list="list.slice(1)"/></div></template>
Eventually we run out of items to render, so we need to stop our recursion (or we'll get tons of errors and our computer will melt).
Our next step is get slots working.
We've got the basic component working, but we also want it to work with scoped slots, so we can customize how we render each item:
<template><v-for :list="list"><template v-slot="{ item }"><strong>{{ item }}</strong></template></v-for></template>
It's not much extra work to get slots in here, so we'll do that right now!
Once I figured out how to nest slots recursively, it set off this obsession with slots — which I'm still working through 😅
Things like:
— Michael Thiessen (@MichaelThiessen) July 19, 2019
👉 Nesting slots n levels deep
👉 Recursive slots
👉 Wrapping a component to turn one slot into multiple
While making sure that each level can provide defaults and override any slot to maintain flexibility, AND transitions work (trickier than you might think 😅)
If you're looking for some more advanced content on slots, I also did a deep dive into case study that looks at different ways of approaching an architecture problem in Vue with slots.
First we'll take a quick detour into how nested slots work, then we'll show how to incorporate them into our v-for
component.
If you know how to use slots normally, I think you'll get this pretty quick.
Let's say we have three components, a Parent
, a Child
, and a Grandchild
. We want to pass some content from the Parent
component and have it rendered in the Grandchild
.
All we're doing is passing the slot from the Parent
to the Child
like we normally do, and then again from the Child
to the Grandchild
.
Starting with the Parent
, we pass in some content:
// Parent.vue<template><Child><span>Never gonna give you up</span></Child></template>
We do some things in our Child
component — which we'll get to in just a moment. And then our Grandchild
component takes the slot and renders out the content:
// Grandchild.vue<template><div><slot /></div></template>
So what does this Child
component look like?
We need it to take the content from Parent
and provide it to the Grandchild
component, so we're connecting two different slots together here:
// Child.vue<template><Grandchild><slot /></Grandchild></template>
Remember, the <slot />
element renders out content that's passed to the component as a slot. So we're taking that content from Parent
and then rendering it inside of the slot of Grandchild
.
The only thing different with nesting scoped slots is that we also have to pass along the scoped data.
Adding this into our v-for
we now get this:
<template><div><slot v-bind:item="list[0]"><!-- Default -->{{ list[0] }}</slot><v-forv-if="list.length > 1":list="list.slice(1)"><!-- Recursively pass down scoped slot --><template v-slot="{ item }"><slot v-bind:item="item" /></template></v-for></div></template>
Let's look at our base case first.
If no slot is provided, we default to what's inside of the <slot>
element, and render list[0]
just like before. But if we do provide a slot, it will render it out and pass the list item to the parent through the slot scope.
The recursive case here is similar. If we pass in a slot to v-for
, it will render it within the slot of the next v-for
, so we get our nesting. It also grabs the item
from the scoped slot and passes that back up the chain too.
And now, we have a component that imitates (more or less) a v-for
, but only using the template!
We went through a long and winding path to get here, but now we've seen how to create a component that replicates a v-for
, but only using the template.
We covered:
If you enjoyed this article, please share it with others who may enjoy it as well!
And sign up for my email list below if you want to get advanced Vue content like this every week.