skip to Main Content

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.

2

Answers


  1. Just wrap your slot logic into a functional component (a render function):

    Playground

    <script setup>
    import {h, useSlots} from 'vue';
    const slots = useSlots()
    
    const slotted = () => {
      return slots.default().map(slot => {
        debugger;
        if (slot.props?.baz)
          return h('div', 'A component in the default slot WITH baz prop = ' + slot.props.baz);
        else
          return h('div', 'A component in the default slot WITHOUT baz prop');
      });
    };
    </script>
    
    <template>
      <div>
        <slotted/>
      </div>
    </template>
    
    Login or Signup to reply.
  2. 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?.().map(vnode => vnode.props?.baz ? vnode : null)
      }
    }
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search