Vue - how to pass down slots inside wrapper component?
Asked Answered
D

5

92

So I've created a simple wrapper component with template like:

<wrapper>
   <b-table v-bind="$attrs" v-on="$listeners"></b-table>
</wrapper>

using $attrs and $listeners to pass down props and events.
Works fine, but how can the wrapper proxy the <b-table> named slots to the child?

Demonstrative answered 16/6, 2018 at 21:23 Comment(1)
FYI, this is going to be solved in this new PR. But if you want to have a solution right now, this github comment would help. tested: jsfiddle.net/jacobgoh101/bptLavov/185Koblas
H
195

Vue 3

Same as the Vue 2.6 example below except:

  • $listeners has been merged into $attrs so v-on="$listeners" is no longer necessary. See the migration guide.
  • $scopedSlots is now just $slots. See migration guide.
<template v-for="(_, slot) of $slots" v-slot:[slot]="scope || {}"><slot :name="slot" v-bind="scope"/></template>

Vue 2.6 (v-slot syntax)

All ordinary slots will be added to scoped slots, so you only need to do this:

<wrapper>
  <b-table v-bind="$attrs" v-on="$listeners">
    <template v-for="(_, slot) of $scopedSlots" v-slot:[slot]="scope"><slot :name="slot" v-bind="scope"/></template>
  </b-table>
</wrapper>

Vue 2.5

See Paul's answer.


Original answer

You need to specify the slots like this:

<wrapper>
  <b-table v-bind="$attrs" v-on="$listeners">
    <!-- Pass on the default slot -->
    <slot/>

    <!-- Pass on any named slots -->
    <slot name="foo" slot="foo"/>
    <slot name="bar" slot="bar"/>

    <!-- Pass on any scoped slots -->
    <template slot="baz" slot-scope="scope"><slot name="baz" v-bind="scope"/></template>
  </b-table>
</wrapper>

Render function

render(h) {
  const children = Object.keys(this.$slots).map(slot => h('template', { slot }, this.$slots[slot]))
  return h('wrapper', [
    h('b-table', {
      attrs: this.$attrs,
      on: this.$listeners,
      scopedSlots: this.$scopedSlots,
    }, children)
  ])
}

You probably also want to set inheritAttrs to false on the component.

Heritage answered 17/6, 2018 at 1:2 Comment(2)
The answer for "Vue 2.6" doesn't work for me when using a named slot (on vue 2.6.11). If I remove the parts ="scope" and v-bind="scope", it works for named slots but I can't use it for scoped slots any more :(Dede
I'm having the same issue with 2.6. It seems that the issue is with trying to access the scope o non-scoped slots. Sergey's answer below has worked for me.Oppidan
E
92

I have been automating the passing of any (and all) slots using v-for, as shown below. The nice thing with this method is that you don't need to know which slots have to be passed on, including the default slot. Any slots passed to the wrapper will be passed on.

<wrapper>
  <b-table v-bind="$attrs" v-on="$listeners">

    <!-- Pass on all named slots -->
    <slot v-for="slot in Object.keys($slots)" :name="slot" :slot="slot"/>

    <!-- Pass on all scoped slots -->
    <template v-for="slot in Object.keys($scopedSlots)" :slot="slot" slot-scope="scope"><slot :name="slot" v-bind="scope"/></template>

  </b-table>
</wrapper>
Earmuff answered 15/10, 2018 at 18:50 Comment(1)
This line is the magic if you wanna use the scoped slots from parent for vuetify data-tables (you wanna specify the UI for a specific column from parent component wrapping the data-table). <template v-for="slot in Object.keys($scopedSlots)" :slot="slot" slot-scope="scope"><slot :name="slot" v-bind="scope"/></template>Theophilus
B
14

Here is updated syntax for vue >2.6 with scoped slots and regular slots, thanks Nikita-Polyakov, link to discussion

<!-- pass through scoped slots -->
<template v-for="(_, scopedSlotName) in $scopedSlots" v-slot:[scopedSlotName]="slotData">
  <slot :name="scopedSlotName" v-bind="slotData" />
</template>

<!-- pass through normal slots -->
<template v-for="(_, slotName) in $slots" v-slot:[slotName]>
  <slot :name="slotName" />
</template>

<!-- after iterating over slots and scopedSlots, you can customize them like this -->
<template v-slot:overrideExample>
    <slot name="overrideExample" />
    <span>This text content goes to overrideExample slot</span>
</template>
Badinage answered 5/2, 2021 at 11:28 Comment(0)
K
12

This solution for Vue 3.2 version and above

<template v-for="(_, slot) in $slots" v-slot:[slot]="scope">
    <slot :name="slot" v-bind="scope || {}" />
</template>
Kumquat answered 22/6, 2022 at 20:35 Comment(4)
This works great except when the slot is dynamically created and rendered as a fragment or text root node... then you get a ton of Vue Warns in the dev console. I ran into this issue when wanting to wrap the Kendo UI Grid component and exposing its cell template slots.Luo
I'm getting a typescript error at [slot]: Element implicitly has an 'any' type because expression of type 'string | number' can't be used to index type {...}. Any idea how to solve this?Annual
Same here. It does seem to run correctly, as far as I can tell, but VSCode flags it as a type error. If I use this method, ALL of my wrapper components will be flagged in the Explorer as files with errors, potentially hiding real errors down the line...Renick
I figured out how to fix the typescript error (by accident): You need toimport the component you are wrapping, even if it's already globally registered. (e.g. wrapping Quasar's QInput component, you need to add import { QInput } from 'quasar' to your component.)Renick
N
0

For Vue 3 with Typescript, using Vuetify as an example.

<template>
  <v-autocomplete
    v-bind="$attrs"
    v-on="$listeners"
  >
    <template
      v-for="slotName in slotsNamesToPassThrough"
      #[slotName]="slotProps"
    >
      <slot
        v-if="slotName === 'prepend-item'"
        :name="slotName"
        v-bind="slotProps"
      >
        <v-list-item><em>Search for a user ...</em></v-list-item>
      </slot>
      <slot
        v-else
        :name="slotName"
        v-bind="slotProps"
      ></slot>
    </template>
  </v-autocomplete>
</template>

<script lang="ts" setup>
import { computed, useSlots } from "vue"

import type { VAutocomplete } from "vuetify/lib/components/index.mjs"

type WrappedSlotNames = keyof VAutocomplete["$slots"]
const wrappedSlotNames = new Set<WrappedSlotNames>([
  "append-inner",
  "append-item",
  "append",
  "chip",
  "clear",
  "details",
  "item",
  "label",
  "loader",
  "message",
  "no-data",
  "prepend-inner",
  "prepend-item",
  "prepend",
  "selection",
])

const slots = useSlots()

const slotsNamesToPassThrough = computed(() => {
  return Object.keys(slots).filter((slotName): slotName is WrappedSlotNames =>
    wrappedSlotNames.has(slotName as WrappedSlotNames)
  )
})
</script>

The key parts are:

  1. Re-defining a list of slot names that the original component provides. I hate this part, as I feel like it breaks the key value of inheritance and slot pass-through in general. I haven't found a workaround.
  2. Filtering the computed result of useSlots() to build a list of slots that have been passed to the component, filtered by the list of slots that are allowed by the wrapped component.
  3. Providing new defaults for slots via v-if="slotName === 'some-name'"

Positives:

  1. If you provide an invalid slot name you will get a type error.

Negatives:

  1. If you miss providing a valid slot name, you will not get a type error.

Is this nice? No. Does it work and solve some basic type safety things? Yes. Hopefully there will be some future improvements to TypeScript or Vue or Vuetify that make inheritance easier and slot pass-through easier.

Neolith answered 27/3 at 23:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.