0

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?

Kapcash
  • 6,377
  • 2
  • 18
  • 40

2 Answers2

2

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'])
    ],
  )
}
Kapcash
  • 6,377
  • 2
  • 18
  • 40
-2

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.

eXception
  • 1,307
  • 1
  • 7
  • 22
  • My question was about 1. render function (not template rendering), 2. not about renderless components, but passing a slot transparently to a nested component without extra wrapper (which looked required by the render function api). – Kapcash Sep 21 '22 at 12:12
  • @Kapcash 1. Your question is a first link in google for "vue components without wrapper". So it's not for you, but for people who will come by the same search request. 2. Render function is the only way to achieve that in vue 2, so why not to do that without render function in vue 3? – eXception Sep 22 '22 at 13:53