Vue: Implementing "Slot Inheritance" With Dynamic Slot Names

leishanglin  Updated:
Created:

Background

Recently, I’ve been re-packaging components based on Ant Design Vue for use in a form designer. The re-packaged components look like this:

<script setup>
import { Switch } from 'ant-design-vue'
import { switchProps } from 'ant-design-vue/es/switch'
import { omit } from 'lodash-es'

const props = defineProps(switchProps())

const value = defineModel('value', {
  type: [Boolean, Number, String],
})
</script>

<template>
  <Switch v-bind="omit(props, ['value'])" v-model:checked="value">
    <!-- ... -->
  </Switch>
</template>

In the example above, I re-packaged the <Switch /> component with the aim of changing the original v-model:checked to v-model:value. This is useful for dynamically rendering forms (because it standardizes the two-way data binding).

However, I wasn’t sure how to “inherit” the built-in slots of the <Switch /> component. The following approach is something I can’t accept (because it’s too cumbersome):

<template>
  <Switch v-bind="omit(props, ['value'])" v-model:checked="value">
    <template #checkedChildren>
      <slot name="checkedChildren"></slot>
    </template>

    <template #unCheckedChildren>
      <slot name="unCheckedChildren"></slot>
    </template>

    <!-- And more ... -->
  </Switch>
</template>

Solution

After reading the section on slots in the official Vue documentation, I found a way to "inherit" the built-in slots of a component. The method is: use useSlots to get all the slots and then dynamically render them inside the component using v-for:

<script setup>
import { useSlots } from 'vue'

// ...

const slots = useSlots()
</script>

<template>
  <Switch v-bind="omit(props, ['value'])" v-model:checked="value">
    <template v-for="slotName in Object.keys(slots)" v-slot:[slotName]>
      <slot :name="slotName"></slot>
    </template>
  </Switch>
</template>

This way, we can “intercept” all the slots used by the outer component and dynamically register them. Additionally, we can replace useSlots with $slots. Here is the complete code:

<script setup>
import { Switch } from 'ant-design-vue'
import { switchProps } from 'ant-design-vue/es/switch'
import { omit } from 'lodash-es'

const props = defineProps(switchProps())

const value = defineModel('value', {
  type: [Boolean, Number, String],
})
</script>

<template>
  <Switch v-bind="omit(props, ['value'])" v-model:checked="value">
    <template v-for="slotName in Object.keys($slots)" v-slot:[slotName]>
      <slot :name="slotName"></slot>
    </template>
  </Switch>
</template>

Note: After v-slot:[slotName], you can also receive parameters passed from scoped slots, for example: v-slot:[slotName]="xxx".