How can I pass slots through to a child component?
This is a problem that arises when trying to write a wrapper component around another component. The underlying component exposes some slots, and you'd like your wrapper to expose those same slots.
Passing individual slots
Default slot
Let's start with the simplest case, where we're only interested in the default
slot. For that we can use:
<ChildComponent>
<slot />
</ChildComponent>
See it on the SFC Playground.
The following is equivalent, but it is more explicit about the use of v-slot
:
<ChildComponent>
<template v-slot:default>
<slot />
</template>
</ChildComponent>
Behind the scenes, Vue compiles templates into render functions. It's also possible to use render functions directly to write components. Render functions are plain JavaScript, so they allow us to see how slots work without any Vue template magic.
Slots also compile down to functions, very similar to render functions. In the example above, the <template v-slot:default>
creates a slot function, called default
, to pass down to ChildComponent
. The <slot />
is used to invoke the parent's default
slot function. We're effectively wrapping one function in another function to pass it down:
h(ChildComponent, null, {
default() { // <template v-slot:default>
return slots.default?.() // <slot />
} // </template>
})
You can see a running example of this on the SFC Playground.
The code above could potentially be simplified to pass on the function directly, e.g.:
h(ChildComponent, null, {
default: slots.default
})
While this does work, it's perhaps less clear how this maps back onto the template, so we'll use the longer version for render function examples on this page. To reiterate:
- Slots are functions passed down from the parent to the child.
<template v-slot>
is used to create a slot function to pass down.<slot />
is used to invoke a slot function passed in from the parent.
Default scoped slot
If the slot is a scoped slot then we'll need to pass on the slot props too:
<ChildComponent v-slot="{ value }">
<slot :value="value" />
</ChildComponent>
See it on the SFC Playground.
Rather than passing on the slot props individually, we can pass them all in one go by using v-bind
with an object:
<ChildComponent v-slot="slotProps">
<slot v-bind="slotProps ?? {}" />
</ChildComponent>
See it on the SFC Playground.
You might have noticed the ?? {}
part. That's to avoid errors if slotProps
is undefined
. If ChildComponent
is using a template then that shouldn't happen, but if it's using a render function then it could be undefined
. v-bind
will throw an error if the argument is undefined
, so we need to defend against that. Example.
Named slots
In this example we have 3 slots. The body
slot is a scoped slot, passing on a slot prop. As with the earlier examples, it could pass along the whole slotProps
object instead:
<ChildComponent>
<template #header>
<slot name="header" />
</template>
<template #body="{ value }">
<slot name="body" :value="value" />
</template>
<template #footer>
<slot name="footer" />
</template>
</ChildComponent>
See it on the SFC Playground.
The equivalent render function would be:
h(ChildComponent, null, {
header() { // <template #header>
return slots?.header() // <slot name="header" />
}, // </template>
body({ value }) { // <template #body="{ value }">
return slots?.body({ value }) // <slot name="body" :value="value" />
}, // </template>
footer() { // <template #footer>
return slots?.footer() // <slot name="footer" />
} // </template>
})
There are more concise ways to write this render function, but the way we've shown it here allows us to illustrate how each line maps back to the template.
See it on the SFC Playground.
Passing all slots
To pass through all slots you'd use:
<ChildComponent>
<template
v-for="(_, slotName) in $slots"
v-slot:[slotName]="slotProps"
>
<slot :name="slotName" v-bind="slotProps ?? {}" />
</template>
</ChildComponent>
See it on the SFC Playground.
If you find the code above a bit overwhelming, that's normal.
You may want to take a look at the earlier examples for passing individual slots first, as the ideas from those examples are reused here.
$slots
$slots
is an object containing the slot functions. We saw this same object in some of the render function examples from earlier, though it was referred to as slots
in those examples. In the template this object is exposed with the name $slots
instead.
This object contains the slot functions passed in by the parent component. If you're familiar with $props
or $attrs
then it's a similar idea. These objects each contain things passed in from the parent template. However, for $slots
there's no direct equivalent of v-bind="$props"
or v-bind="$attrs"
to allow us to pass along all of the slots from $slots
in one go.
Instead, we need to iterate over $slots
using v-for
. For each of the slot functions that have been passed in, we want to pass down a slot function of our own to ChildComponent
.
v-for
We're using v-for
to iterate over an object, so the general form is:
v-for="(propertyValue, propertyName, index) in object"
In our case, the property value is the slot function, but we don't actually need that. All we need is the slot name. So we write it as v-for="(_, slotName) in $slots"
. Using _
is a common naming convention for unused positional arguments.
If you prefer, the v-for
could have been written as:
v-for="slotName in Object.keys($slots)"
You might notice that there isn't a key
attribute on the <template>
tag, which would usually be expected for the v-for
. That isn't a mistake, it's a special case. Using a key
when iterating over a <template v-slot>
won't do anything. Roughly speaking, this is because the iteration is creating slot functions, not VNodes, so there's nowhere to put the key. Effectively the name of the slot is performing the same role as a key.
v-slot
The v-slot:[slotName]
creates a slot function to pass down to the child, using the name slotName
. The [...]
part is needed to give the slot a dynamic name. It's just like v-slot:header
or #header
, but with a dynamic value instead of a fixed name like header
. You could use #[slotName]
instead, if you prefer. The official documentation for dynamic slots names is at https://vuejs.org/guide/components/slots.html#dynamic-slot-names.
We then need to handle the slot props for scoped slots. This is similar to the earlier examples, and we use the name slotProps
for that object.
<slot>
The <slot>
is used to invoke the slot function that was passed in from the parent.
The :name="slotName"
determines the name of the slot function to invoke.
The v-bind="slotProps ?? {}"
is the same as in the earlier examples for a default scoped slot.
With a render function
If we wanted to write this same example as a render function it would actually be a lot easier. We can pass along all the slots in one go, no looping required:
h(ChildComponent, null, slots)
See it on the SFC Playground.