Vue Forward Slots

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 NameDescriptionTypeDefault
slotsThe slots object to forward (usually $slots)object{}
onlyOnly forward these slotsstring | RegExp | (string|RegExp)[]undefined
exceptForward all slots except thesestring | RegExp | (string|RegExp)[]undefined
inheritAttrsForward any $attrs to components in the slots as wellbooleantrue
filterNativeInclude native slots in the only/except filtering processbooleanfalse

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.

Part of evoMark's contribution to Free and Open Source Software (FOSS)

To read more about evoMark and our open-source software, head to our Open-Source Software page

npm i @evomark/vue-forward-slots
Get in Touch

Get in touch with us today 01202 790300

monday 09:00 - 17:00
tuesday 09:00 - 17:00
wednesday 09:00 - 17:00
thursday 09:00 - 17:00
friday 09:00 - 16:00
saturday Closed
sunday Closed