16

I have a little Loading component, whose default text I want to be 'Loading...'. Good candidate for slots, so I have something like this as my template:

<p class="loading"><i class="fa fa-spinner fa-spin"></i><slot>Loading...</slot></p>

That allows me to change the loading message with e.g. <loading>Searching...</loading>. The behaviour I would like, though, is not just to display the default message if no slot content is supplied, but also if the slot content is null or blank. At the moment if I do e.g.<loading>{{loadingMessage}}</loading> and loadingMessage is null, no text is displayed (where I want the default text to be displayed). So ideally I need to test this.$slots.default. This tells me whether content was passed in, but how do I find whether or not it was empty? this.$slots.default.text returns undefined.

John Moore
  • 6,739
  • 14
  • 51
  • 67

3 Answers3

26

You'd need a computed property which checks for this.$slots. With a default slot you'd check this.$slots.default, and with a named slot just replace default with the slot name.

computed: {
  slotPassed() {
    return !!this.$slots.default[0].text.length
  }
}

And then use it in your template:

<template>
  <div>
    <slot v-if="slotPassed">Loading...</slot>
    <p v-else>Searching...</p>
  </div>
</template>

You can see a small example here. Notice how fallback content is displayed and not "default content", which is inside the slot.


Edit: My wording could've been better. What you need to do is check for $slots.X value, but computed property is a way to check that. You could also just write the slot check in your template:

<template>
  <div>
    <slot v-if="!!$slots.default[0].text">Loading...</slot>
    <p v-else>Searching...</p>
  </div>
</template>

Edit 2: As pointed out by @GoogleMac in the comments, checking for a slot's text property fails for renderless components (e.g. <transition>, <keep-alive>, ...), so the check they suggested is:

!!this.$slots.default && !!this.$slots.default[0]

// or..
!!(this.$slots.default || [])[0]
kano
  • 5,626
  • 3
  • 33
  • 48
  • 1
    The problem I'm seeing is that `$slots.default.text` is always undefined. Here's a jsfiddle: https://jsfiddle.net/JohnMoore/rmafyz2x/2/ – John Moore Nov 22 '17 at 12:52
  • 1
    My bad, `$slots.default` is an array, so the thing you need is `$slots.default[0].text` : ) – kano Nov 22 '17 at 13:02
  • 1
    Yes, that's the answer I've been looking for. As long as I can test for `$slots.default[0].text` I can achieve what I want. – John Moore Nov 22 '17 at 13:20
  • 3
    For me, there was a slot passed, but the above answer still failed because the slot was a Vue `` element with another element inside. The transition element has a text property of `undefined`. In my case I have a computed property which returns `!!this.$slots.default && !!this.$slots.default[0]`. – parker_codes Jan 23 '20 at 20:52
  • 1
    @GoogleMac aha, a TIL-moment for me! Thanks for sharing, I've edited my answer to include your insight – kano Jan 25 '20 at 13:31
6

@kano's answer works well, but there's a gotcha: this.$slots isn't reactive, so if it starts out being false, and then becomes true, any computed property won't update.

The solution is to not rely on a computed value but instead on created and beforeUpdated (as @MathewSonke points out):

export default {
  name: "YourComponentWithDynamicSlot",
  data() {
    return {
      showFooter: false,
      showHeader: false,
    };
  },
  created() {
    this.setShowSlots();
  },
  beforeUpdate() {
    this.setShowSlots();
  },
  methods: {
    setShowSlots() {
      this.showFooter = this.$slots.footer?.[0];
      this.showHeader = this.$slots.header?.[0];
    },
  },
};

UPDATE: Vue 3 (Composition API)

For Vue 3, it seems that the way to check whether a slot has content has changed (using the new composition API):

import { computed, defineComponent } from "vue";

export default defineComponent({
  setup(_, { slots }) {
    const showHeader = computed(() => !!slots.header);

    return {
      showHeader,
    };
  },
});

note: I can't find any documentation on this, so take it with a pinch of salt, but seems to work in my very limited testing.

fredrivett
  • 5,419
  • 3
  • 35
  • 48
  • For Vue 2, if you're using scoped slots, the reference is actually a function, not an object. So you have to execute it before checking for child elements. `const scopedSlot = this.$scopedSlots.myScopedSlot; const scopedSlotResolved = scopedSlot(); const hasChild = !!(scopedSlotResolved?.[0]);` Can then do a null check, since the scoped slot reference won't be a function until later in the lifecycle. – Siegen Aug 20 '21 at 23:58
0

this.$slots can be checked to see if a slot has been used.

It is important to note that this.$slots is not reactive. This could cause problems when using this.$slots in a computed value.

https://v2.vuejs.org/v2/api/?redirect=true#:~:text=Please%20note%20that%20slots%20are%20not%20reactive.

This means we need to ensure that this.slots is checked whenever the component re-renders. We can do this simply by using a method instead of a computed property.

https://v2.vuejs.org/v2/guide/computed.html?redirect=true#:~:text=In%20comparison%2C%20a%20method%20invocation%20will%20always%20run%20the%20function%20whenever%20a%20re%2Drender%20happens

<template>
    <div>
        <slot v-if="hasHeading" name="heading"/>
    </div>
</template>


<script>
export default{
    name: "some component",
    methods: {
        hasHeading(){ return !!this.slots.heading}
    }
}
</script>