Search code examples
javascriptvue.jsvuejs3

Conditionally render content in Vue parent component based on whether a slotted child component has a prop


In Vue 3, how can a block of code in a parent component template be conditionally rendered depending on whether or not a slotted child component has a prop passed to it?

Take the following code for example:

<Foo>
  <Bar/>
  <Bar baz="baz" />
</Foo>

Conditional slots can be used with $slots in templates, i.e. v-if="$slots.default", but there doesn't seem to be a way to conditionally render certain content in the parent based on whether or not a slot has a prop.

Inside of <Foo/>, note the v-if does not work but this is the idea.

<script setup></script>

<template>
  <h1>Foo</h1>
  <template v-if="$slots.default.props.baz">
    <!-- add custom logic here depending on whether or not "baz" prop is present -->
  </template>
  <slot/>
</template>

This also will not work:

<script setup></script>

<template>
  <h1>Foo</h1>
  <template v-for="slot in $slots.default">
    <div v-if="slot.props.baz"><!-- do something --></div>
  </template>
  <slot/>
</template>

The only working solution is to use the useSlots() composable and use a render function.

<script setup>
const slots = useSlots()

slots.default().forEach(slot => {
  if (slots.props?.baz)
    console.log('baz prop present')
  else
    console.log('baz prop not present')
})
</script>

The thing is that vue templates are compiled to render functions, so it would seem that there is a way to access props on $slots.


Solution

  • This is possible with a template. The problem in the original code is that $slots.default is a function which cannot be iterated.

    It should be:

    <template>
      <h1>Foo</h1>
      <template v-for="vnode in $slots.default?.()">
        <component :is="vnode" v-if="vnode.props?.baz"></component>
      </template>
    </template>
    

    :is="vnode" is a working but undocumented use and can be extended to :is="() => vnode" in case of problems.

    Render function is more flexible than a template, and it's more efficient to do this there, as suggested in another answer. And it's even more efficient to do this in script rather than script setup to not work around its limitations:

    export default {
      setup(props, { slots }) {
        return () => slots.default?.().filter(vnode => vnode.props?.baz)
      }
    }