174

Is there a way to only display a slot if it has any content?

For example, I'm building a simple Card.vue component, and I only want the footer displayed if the footer slot has content:

Template

<template>
    <div class="panel" :class="panelType">
        <div class="panel-heading">
            <h3 class="panel-title">
                <slot name="title">
                    Default Title
                </slot>
            </h3>
        </div>

        <div class="panel-body">
            <slot name="body"></slot>
            
            <p class="category">
                <slot name="category"></slot>
            </p>
        </div>

        <div class="panel-footer" v-if="hasFooterSlot">
            <slot name="footer"></slot>
        </div>
    </div>
</template>

Script

<script>
    export default {
        props: {
            active: true,
            type: {
                type: String,
                default: 'default',
            },
        },

        computed: {
            panelType() {
                return `panel-${this.type}`;
            },

            hasFooterSlot() {
                return this.$slots['footer']
            }
        }
    }
</script>

In in View:

<card type="success"></card>

Since the above component doesn't contain a footer, it should not be rendered, but it is.

I've tried using this.$slots['footer'], but this returns undefined.

Does anyone have any tips?

Steve Bauman
  • 8,165
  • 7
  • 40
  • 56

15 Answers15

198

It should be available at

this.$slots.footer

So, this should work.

hasFooterSlot() {
  return !!this.$slots.footer;
}

Example.

radbyx
  • 9,352
  • 21
  • 84
  • 127
Bert
  • 80,741
  • 17
  • 199
  • 164
  • Works! Sorry that was my fault, was using incorrect syntax when entering content into the slot. Thanks!! :) – Steve Bauman May 19 '17 at 19:38
  • 6
    `this.$slots.footer` and `this.$slots['footer']` are equivalent, aren't they? Was this really the solution or was there something else going wrong? – Daniel Beck May 19 '17 at 20:19
  • 1
    @DanielBeck You're correct, they will both point to the same thing. And both will return undefined if there is no content. The only thing that is different is I return a boolean value, but, testing it, returning undefined from hasFooterSlot will result in v-if hiding the component.. So, technically I think your code should have worked. – Bert May 19 '17 at 20:27
  • (To clarify, I'm not the original asker, just an interested observer :) – Daniel Beck May 19 '17 at 22:13
  • @DanielBeck I think it was due to me not converting it to a boolean using Bert's method. – Steve Bauman Nov 14 '18 at 21:54
  • 6
    This doesn't seem to work in all cases in Vue 2.6 - was incorrectly returning false when other $scopedSlots were present (at least in certain circumstances, such as an element that gets removed from the dom and added back via `v-if`) – AlexMA Apr 01 '20 at 21:17
  • You can store this.$slot.footer !== undefined in a computed property – rostamiani Jun 14 '20 at 08:38
  • 6
    Note that in Vue 3 properties in $slots are now methods, so you would need to use syntax like this: `this.$slots.footer()` – launchoverit May 20 '21 at 23:51
  • Also note that the function might return a node even if there is no content, e.g. wenn you do ``. So to really make sure, you could check like `const hasFooter = slot.footer ? !!slot.footer()[0].children : false` – Matthias S Dec 16 '21 at 12:22
  • @AlaxMA is seems to work with `slot="name"` but not with `v-slot:name="{data}"` – yellowsir Feb 15 '22 at 14:27
  • 1
    @launchoverit that doesn't quite work tho bc `this.$slots.footer()` won't be defined if the slot is not used and you'll get an exception. you have to check for `this.$slots.footer`'s existence before calling it in vue 3 (this is fairly unclear from the docs/migration guide and made us have to refactor everything twice.. :/ – Damon Oct 11 '22 at 12:48
36

Now, in Vue3 composition API , you can use useSlots.

<script setup>
import { useSlots } from 'vue'
const slots = useSlots()
</script>

<template>
  <div v-if="slots.content" class="classname">
    <slot name="content"></slot>
  </div>
</template>
Max Stevens
  • 112
  • 8
Sunghyun Cho
  • 689
  • 5
  • 8
  • 4
    Just missing an `import { useSlots } from 'vue'` but otherwise good stuff. – zenw0lf Nov 04 '22 at 18:07
  • One can also directly use `$slots`, as per documentation: Usage of `slots` and `attrs` inside ` – Jason Sep 02 '23 at 09:15
33

You should check vm.$slots and also vm.$scopedSlots for it.

hasSlot (name = 'default') {
   return !!this.$slots[ name ] || !!this.$scopedSlots[ name ];
}
Madmadi
  • 2,064
  • 1
  • 14
  • 17
22

CSS simplifies this a lot. Just use the following code and voila!

.panel-footer:empty {
  display: none;
}
biquillo
  • 6,469
  • 7
  • 37
  • 40
13

This is the solution for Vue 3 composition API:

<template>
    <div class="md:grid md:grid-cols-5 md:gap-6">

        <!-- Here, you hide the wrapper if there is no used slot or empty --> 
        <div class="md:col-span-2" v-if="hasTitle">
            <slot name="title"></slot>
        </div>

        <div class="mt-5 md:mt-0" 
            :class="{'md:col-span-3': hasTitle, 'md:col-span-5': !hasTitle}">
            <div class="bg-white rounded-md shadow">
                <div class="py-7">
                    <slot></slot>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import {ref} from "vue";

export default {
    setup(props, {slots}) {
        const hasTitle = ref(false)

        // Check if the slot exists by name and has content.
        // It returns an empty array if it's empty.
        if (slots.title && slots.title().length) {
            hasTitle.value = true
        }

        return {
            hasTitle
        }
    }
}
</script>
Mustafa Akçakaya
  • 1,069
  • 2
  • 11
  • 12
6

In short do this in inline:

<template lang="pug">
  div
    h2(v-if="$slots.title")
      slot(name="title")
    h3(v-if="$slots['sub-title']")
      slot(name="sub-title")
</template>
Syed
  • 15,657
  • 13
  • 120
  • 154
5

I have ran into a similiar issue but across a wide code base and when creating atomic design structured components it can be tiring writing hasSlot() methods all the time and when it comes to TDD - its one more method to test... Saying that, you can always put the raw logic in a v-if but i have found that the template end up cluttered and harder to read on occasions especially for a new dev checking out the code structure.

I was tasked to find out a way of removing parent divs of slots when the slot isnt provided.

Issue:

<template>
  <div>
    <div class="hello">
      <slot name="foo" />
    </div>
    <div class="world">
      <slot name="bar" />
    </div>
  </div>
</template>

//instantiation
<my-component>
  <span slot="foo">show me</span>
</my-component>

//renders
<div>
  <div class="hello">
    <span slot="foo">show me</span>
  </div>
  <div class="world"></div>
</div>

as you can see, the issue is that i have an almost 'trailing' div, that could provide styling issues when the component author decides there is no need for a bar slot.

ofcourse we could go <div v-if="$slots.bar">...</div> or <div v-if="hasBar()">...</div> etc but like i said - that can get tiresome and eventually end up harder to read.

Solution

My solution was to make a generic slot component that just rendered out a slot with a surrounding div...see below.

//slot component
<template>
  <div v-if="!!$slots.default">
    <slot />
  </div>
</template>


//usage within <my-component/>
<template>
  <div>
    <slot-component class="hello">
      <slot name="foo"/>
    </slot-component>
    <slot-component class="world">
      <slot name="bar"/>
    </slot-component>
  </div>
</template>

//instantiation
<my-component>
  <span slot="foo">show me</span>
</my-component>

//renders
<div>
  <div class="hello">
    <span>show me</span>
  </div>
</div>

I came into use-case issues when trying this idea and sometimes it was my markup structure that needed to change for the benefit of this approach. This approach reduces the need for small slot checks within each component template. i suppose you could see the component as a <conditional-div /> component...

It is also worth noting that applying attributes to the slot-component instantiation (<slot-component class="myClass" data-random="randomshjhsa" />) is fine as the attributes trickle into the containing div of the slot-component template.

Hope this helps.

UPDATE I wrote a plugin for this so the need for importing the custom-slot component in each consumer component is not needed anymore and you will only have to write Vue.use(SlotPlugin) in your main.js instantiation. (see below)

const SLOT_COMPONENT = {
  name: 'custom-slot',
  template: `
    <div v-if="$slots.default">
      <slot />
    </div>
  `
}

const SLOT_PLUGIN = {
  install (Vue) {
    Vue.component(SLOT_COMPONENT.name, SLOT_COMPONENT)
  }
}

export default SLOT_PLUGIN

//main.js
import SlotPlugin from 'path/to/plugin'
Vue.use(SlotPlugin)
//...rest of code
Francis Leigh
  • 1,870
  • 10
  • 25
  • 2
    this is pretty cool but has a slippery slope because of the various edge cases, scopedSlots, default slot inner content, named slots, etc.; in my humble opinion wrapping slots seems like an anti-pattern and it's probably just better to ask if the slot you want to wrap exists in the component (this.$slots). Curious how your solution has evolved since. – qodeninja Dec 16 '19 at 23:37
4

Initially I thought https://stackoverflow.com/a/50096300/752916 was working, but I had to expand on it a bit since $scopeSlots returns a function which is always truthy regardless of its return value. This is my solution, though I've come to the conclusion that the real answer to this question is "doing this is an antipattern and you should avoid it if possible". E.g. just make a separate footer component that could be slotted in.

Hacky solution

   hasFooterSlot() {
        const ss = this.$scopedSlots;
        const footerNodes = ss && ss.footer && ss.footer();
        return footerNodes && footerNodes.length;
   }

Best Practice (helper component for footer)

const panelComponent = {
  template: `
    <div class="nice-panel">
        <div class="nice-panel-content">
          <!-- Slot for main content -->
          <slot />
        </div>
        <!-- Slot for optional footer -->
        <slot name="footer"></slot>
    </div>
  `
}
const footerComponent = {
  template: `
    <div class="nice-panel-footer">
        <slot />
    </div>
  `
}

var app = new Vue({
  el: '#app',
  components: {
    panelComponent,
    footerComponent
  },
  data() {
    return {
      name: 'Vue'
    }
  }
})
.nice-panel {
  max-width: 200px;
  border: 1px solid lightgray;
}

.nice-panel-content {
  padding: 30px;
}

.nice-panel-footer {
  background-color: lightgray;
  padding: 5px 30px;
  text-align: center;
}
<script src="https://unpkg.com/vue@2.6.11/dist/vue.min.js"></script>
<div id="app">
  <h1>Panel with footer</h1>

  <panel-component>
    lorem ipsum
    <template #footer>
      <footer-component> Some Footer Content</footer-component>
    </template>
  </panel-component>

  <h1>Panel without footer</h1>

  <panel-component>
    lorem ipsum
  </panel-component>
</div>
AlexMA
  • 9,842
  • 7
  • 42
  • 64
3

For Vue 3:

Create an utility function

    //utils.js
    function isSlotHasContent(slotName, slots) {
      return Boolean(!!slots[slotName] && slots[slotName]()[0].children.length > 0);
    }

In your component:

    <script setup>
      import { isSlotHasContent } from 'path/to/utils.js';

      const slots = useSlots();

      // "computed" props has a better performance
      const isFooSlotHasContent = computed(() => isSlotHasContent('foo', slots));
    </script>

    <template>
      <div>
        <div v-if="isFooSlotHasContent">
          <slot name="foo" />
        </div>
        <div v-if="!isFooSlotHasContent">
          Some placeholder
        </div>
      </div>
    </template>
eXception
  • 1,307
  • 1
  • 7
  • 22
2

Hope I understand this right. Why not using a <template> tag, which is not rendered, if the slot is empty.

<slot name="foo"></slot>

Use it like this:

<template slot="foo">
   ...
</template>
grindking
  • 917
  • 5
  • 13
  • 29
  • 4
    The issue is when you want external elements to wrap the slot, but don't want the external elements to be rendered when the slot isn't given or is empty. This wouldn't resolve the use case issue. – Steve Bauman Jul 31 '19 at 15:11
2

@Bert answer does not seem to work for dynamic templates like <template v-slot:foo="{data}"> ... </template>. i ended up using:

 return (
        Boolean(this.$slots.foo) ||
        Boolean(typeof this.$scopedSlots.foo == 'function')
      );
yellowsir
  • 741
  • 1
  • 9
  • 27
2

I like the Solution of @AlexMA however in my case I needed to pass props to the function in order to get the nodes to show up.

Here is an example of how I am passing the "row" to the scoped slot, in my case the row contains a type param that I want to test against in the calling component.

<other-component>
    <template v-slot:expand="{ row }" v-if="!survey.editable">
        <div v-if="row.type != 1" class="flex">
            {{ row }}
        </div>
    </template>
</other-component>

In "other-component" I have the template defined as

<template>
    <div>
        <div v-for="(row, index) in rows">
            {{ hasSlotContent(row) }}
            <slot name="expand" :row="row"> </slot>
        </div>
    </div>
</template>

Because the v-slot requires "row" to be passed to it I created a a method

methods:{
    hasSlotContent(row){
        const ss = this.$scopedSlots
        const nodes = ss && ss.expand && ss.expand({ row: row })
        return !!(nodes && nodes.length)
    }
}

I call this on each iteration so that it can evaluate itself and give back the appropriate response. you can use the "hasSlotContent(row)" method where-ever you need it, in my example I'm just outputting the truthy value to the DOM.

I hope this helps someone come to a quicker solution.

2

Reposting a Vue 3 solution from Github, which also works with Options API, since there was a fairly upvoted method from an Issue there:

The comment itself: https://github.com/vuejs/core/issues/4733#issuecomment-1024816095

The function (remove types if you're not writing TypeScript):

import {
  Comment,
  Text,
  Slot,
  VNode,
} from 'vue';

export function hasSlotContent(slot: Slot|undefined, slotProps = {}): boolean {
  if (!slot) return false;

  return slot(slotProps).some((vnode: VNode) => {
    if (vnode.type === Comment) return false;

    if (Array.isArray(vnode.children) && !vnode.children.length) return false;

    return (
      vnode.type !== Text
      || (typeof vnode.children === 'string' && vnode.children.trim() !== '')
    );
  });
}

This works just as fine, if you delete the slotProps argument (unless you need it).

0

TESTED

So this work for me in vue 3:

I use onMounted to first get the value, and then onUpdate so the value can update.

   <template>
     <div v-if="content" class="w-1/2">
          <slot name="content"></slot>
     </div>
   </template>


 <script>
     import { ref, onMounted, defineComponent, onUpdated } from "vue";   
     export default defineComponent({
        setup(props, { slots }) {
        const content = ref()
        onMounted(() => {
                if (slots.content && slots.content().length) {
                    content.value = true
                }
            })
            onUpdated(() => {
                content.value = slots.content().length
                console.log('CHECK VALUE', content.value)
            })
})
</script>
Non404
  • 1,130
  • 2
  • 12
  • 22
0

None of the solutions worked for me. I found this code that worked :

import { Comment, Fragment, Text, type VNode, type Slot } from 'vue';

export function isVNodeEmpty(vnode: VNode | VNode[] | undefined | null) {
  return (
    !vnode ||
    asArray(vnode).every(
      (vnode) =>
        vnode.type === Comment ||
        (vnode.type === Text && !vnode.children?.length) ||
        (vnode.type === Fragment && !vnode.children?.length),
    )
  );
}

export function asArray<T>(arg: T | T[] | null) {
  return Array.isArray(arg) ? arg : arg !== null ? [arg] : [];
}

export function hasSlotContent(slot: Slot | undefined | null, props: any = {}) {
  return !isSlotEmpty(slot, props);
}

export function isSlotEmpty(slot: Slot | undefined | null, props: any = {}) {
  return isVNodeEmpty(slot?.(props));
}

https://github.com/vuejs/core/issues/4733#issuecomment-1694589309
https://github.com/vuejs/core/issues/4733#issuecomment-1512876503

Exlord
  • 5,009
  • 4
  • 31
  • 51