Vue render function: include slots to child component without wrapper

3.5k Views Asked by At

I'm trying to make a functional component that renders a component or another depending on a prop. One of the output has to be a <v-select> component, and I want to pass it down all its slots / props, like if we called it directly.

<custom-component :loading="loading">
  <template #loading>
    <span>Loading...</span>
  </template>
</custom-component>

<!-- Should renders like this (sometimes) -->
<v-select :loading="loading">
  <template #loading>
    <span>Loading...</span>
  </template>
</v-select>

But I can't find a way to include the slots given to my functional component to the I'm rendering without adding a wrapper around them:

render (h: CreateElement, context: RenderContext) {
  // Removed some logic here for clarity
  return h(
    'v-select', 
    {
      props: context.props,
      attrs: context.data.attrs,
      on: context.listeners,
    },
    [
      // I use the `slot` option to tell in which slot I want to render this.
      // But it forces me to add a div wrapper...
      h('div', { slot: 'loading' }, context.slots()['loading'])
    ],
  )
}

I can't use the scopedSlots option since this slot (for example) has no slot props, so the function is never called.

return h(
  'v-select', 
  {
    props: context.props,
    attrs: context.data.attrs,
    on: context.listeners,
    scopedSlots: {
      loading(props) {
        // Never called because no props it passed to that slot
        return context.slots()['loading']
      }
    }
  },

Is there any way to pass down the slots to the component i'm rendering without adding them a wrapper element?

2

There are 2 best solutions below

0
On

I found out it's totally valid to use the createElement function to render a <template> tag, the same used to determine which slot we are on.

So using it like this fixes my problem:

render (h: CreateElement, context: RenderContext) {
  // Removed some logic here for clarity
  return h(
    'v-select', 
    {
      props: context.props,
      attrs: context.data.attrs,
      on: context.listeners,
    },
    [
      // I use the `slot` option to tell in which slot I want to render this.
      // <template> vue pseudo element that won't be actually rendered in the end.
      h('template', { slot: 'loading' }, context.slots()['loading'])
    ],
  )
}
2
On

In Vue 3 it's a way easier.

Check the docs Renderless Components (or playground)

An example from the docs:

App.vue

<script setup>
import MouseTracker from './MouseTracker.vue'
</script>

<template>
  <MouseTracker v-slot="{ x, y }">
    Mouse is at: {{ x }}, {{ y }}
  </MouseTracker>
</template>

MouseTracker.vue

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
  
const x = ref(0)
const y = ref(0)

const update = e => {
  x.value = e.pageX
  y.value = e.pageY
}

onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>

<template>
  <slot :x="x" :y="y"/>
</template>

Just to mention, you can easily override CSS of the component in the slot as well.