One major pain point of working with Vue’s SFC (Single File Components) structure is passing slots through multiple levels of hierarchy. In render functions, it’s as simple as receiving the slots
in the setup call and then passing them to the next component.
With SFCs, you will become very familiar with this pattern:
<BaseComponent> <template v-for="(_, name) in $slots" #[name]="slotData"> <slot :name="name" v-bind="slotData" /> </template></BaseComponent>
Basically, this pattern takes the $slots
property that is automatically made available to your component, iterates over each (where _
becomes the slot function (which is unused here) and name
the – surprisingly – name/key of the slot. Then #[name]
uses that slot name to declare the slot that will be passed to the BaseComponent. In case of any passed scoped slots data, slotData
will catch it.
Next, the <slot>
tag will catch any slotted content passed to our SFC and pass them through to BaseComponent using the same name and slotData as our <template>
tag.
Investigation and Disappointment
In our travels around the Vue ecosystem to find a way to avoid this boilerplate, we came across the following markup:
const App = { setup(props, { slots }) { return () => ( <div> <BaseComponent v-slots={slots}>Hello World</BaseComponent> </div> ); },};
Our excitement was short-lived when we realised that v-slots
was not a standard (hidden) Vue feature, but a directive provided by the Vue JSX Plugin.
After some hours of testing custom directives to see if they could do the same, we realised that reproducing this feature via a directive would require some low-level modification of the Vue build process.
A Component to the Rescue
The answer was staring us in the face all along. If a render function can pass forward slots so easily, why not make one? Well, it turns out, someone beat us to it.
We found Jesse Gall’s Vue Forward Slots package, which (for a time) answered all of our prayers. But there were a few features we thought important (at least to us) that were not included. And as is the beauty of FOSS (Free and Open-Source Software), we created our own evoMark spin on the project.
So there’s the life-story, here we go with the package…
Installation
npm install @evomark/vue-forward-slots
// or
yarn add @evomark/vue-forward-slots
// or
pnpm add @evomark/vue-forward-slots
Vue Forward Slots requires no application installation, so just import it when you need it.
import { ForwardSlots } from "@evomark/vue-forward-slots";
Props
Prop Name | Description | Type | Default |
slots | The slots object to forward (usually $slots) | object | {} |
only | Only forward these slots | string | RegExp | (string|RegExp)[] | undefined |
except | Forward all slots except these | string | RegExp | (string|RegExp)[] | undefined |
inheritAttrs | Forward any $attrs to components in the slots as well | boolean | true |
filterNative | Include native slots in the only/except filtering process | boolean | false |
Examples
The following examples contain content from the original repository readme written by Jesse Gall.
A classic example is that of a table component with multiple levels of nested components. We can easily define and forward slots to nested components using the ForwardSlots
component.
Root Component
We define the slots in the root component.
<template> <TableComponent> <template #name-header> <p class="font-bold">Name</p> </template>
// We still have access to the slot data like we would normally <template #status-cell="{ user }"> <StatusBadge :status="user.status" /> </template> </TableComponent></template>
Table Component
We forward the slots to the child components.
<template> <table> // Notice that we can wrap multiple components in the ForwardSlots component <ForwardSlots :slots="$slots"> <TableHeadComponent /> <TableBodyComponent /> </ForwardSlots> </table></template>
TableHead Component
The TableHeadComponent now has access to the slots defined in the root component. If no slot is provided, it will default to the text in the slot.
<template> <thead> <tr> <th> <slot name="name-header"> Some default text </slot> </th> <th> <slot name="status-header"> Some default text </slot> </th> </tr> </thead></template>
TableBody Component
The TableBodyComponent also has access to the slots defined in the root component. Notice how we also pass the user data.
<template> <tbody> <tr v-for="user in users"> <td> <slot name="name-cell" :user="user"> {{ user.name }} </slot> </td> <td> <slot name="status-cell" :user="user"> {{ user.status }} </slot> </td> </tr> </tbody></template>
We could even go a step further and forward the slots to the next level of child components.
<template> <thead> <tr> <th v-for="header in headers"> <ForwardSlots :slots="$slots"> <TableHeaderCell :header="header" /> </ForwardSlots> </th> </tr> </thead></template>
In theory, we could keep forwarding slots to as many levels of child components as we need.
Forwarding Only Specific Slots
<template> // For a single slot <ForwardSlots :slots="$slots" only="header"> <MyComponent /> </ForwardSlots>
// For multiple slots <ForwardSlots :slots="$slots" :only="['header', 'footer']"> <MyComponent /> </ForwardSlots></template>
Excluding Specific Slots
<template> // For a single slot <ForwardSlots :slots="$slots" except="sidebar"> <MyComponent /> </ForwardSlots>
// For multiple slots <ForwardSlots :slots="$slots" :except="['sidebar', 'footer']"> <MyComponent /> </ForwardSlots></template>
New Examples
The following examples use the new props and functionality added by the evoMark fork of the project.
Using a wildcard match with ‘only’
<template> <ForwardSlots :slots="$slots" :only="['default','prepend.*']"> <MyComponent /> </ForwardSlots></template>
Wildcards can only be used at the beginning or the end of your string (e.g. ‘*prepend’ or ‘prepend.*’). Using them in the middle (e.g. ‘item.*.prepend’) will not work. For this you should use a regex match (more below).
The above example might match the following slots: ‘default’, ‘prepend.header’, ‘prepend.footer’. Because of the dot (.), it would not match a slot simply called ‘prepend’.
The same example would work to the opposite effect with the except
prop.
Using regex matching
If you need to get a little more complex with your matching, you can use a regular expression (RegExp or regex) in place of a string.
<template> <ForwardSlots :slots="$slots" :only="['default', /item\.[a-z]+\.prepend/i ]"> <MyComponent /> </ForwardSlots></template>
The above example might match ‘default’, ‘item.table.prepend’, ‘item.header.prepend’ and ‘item.footer.prepend’. If in doubt, consult Regex101 to check your expression.
Disable forwarding of attributes
By default, Vue will only forward attributes to a component when (1) it is the root component; and (2) it is the only root component. In our fork of the package, we extended this forwarding of attributes to all subcomponents by default.
If you need to disable this functionality, simply do the following:
<template> <ForwardSlots :slots="$slots" only="default" :inherit-attrs="false"> <MyComponent /> <MySecondComponent /> </ForwardSlots></template>
Now MyComponent
and MySecondComponent
won’t receive any attributes available in our SFC.
Issues and Comments
If you spot any issues in this package, or thing of any cool new features we absolutely have to (consider to) implement, then leave us a message in the appropriate place (issues for issues, discussion for comments) on the Vue Forward Slots repo.