Subscribe on changes!

Request to add an official way to check if slot is empty

avatar
Oct 4th 2021

What problem does this feature solve?

There are some times in component composition when we need the ability to determine whether a slot has been passed down by the user of the component with some content. In Vue 2 we could check this easily:

<div v-if="this.$slots.mySlot">
   <p>Some unrelated content to the slot or extra markup</p>
   <slot name="mySlot" />
</div>

With the addition of fragments and the changes to the slot system in Vue 3, this simple truthy/falsy check is no longer possible since a slot function will return a truthy object when containing empty text, comments, fragments, etc.

Vue internals are checking this here: https://github.com/vuejs/vue-next/blob/8610e1c9e23a4316f76fb35eebbab4ad48566fbf/packages/runtime-core/src/helpers/renderSlot.ts#L59

However, there is no official way to do this on the user end that I have found that works 100% of the time without having to tap into undocumented internal API.

Some folks are suggesting checking against the symbol in vnode.type, which works in theory however it seems that when compiled these symbols usually evaluate to undefined. It also creates a very hard-to-maintain series of if checks to try to determine the type of vnode that is being received, along with a combination of checks on its `children.

There was an attempt to request this early in January, but sadly it was closed and dismissed with some answers that don't seem to fully solve the problem. https://github.com/vuejs/vue-next/issues/3056

What does the proposed API look like?

The dream would be to expose some sort of slotIsEmpty (naming?) function through the vue package, similarly to how we are getting isRef, etc. that would simplify solving this particular problem.

avatar
Oct 4th 2021

Instead of being slot specific, it could also be vnode specific, like isVNodeEmpty(arrayOrSingleVNode):

const slotContent = slots.default?.(props)
isVNodeEmpty(slotContent)

One issue is using this in a template would be much verbose in order to reuse the vnodes. So maybe there is an alternative way of doing this that could unify how to do it.

Another issue is that what is empty could be subjective, due to trailing spaces and how they are handled, see https://github.com/vuejs/vue-next/issues/3056#issuecomment-786560172 considering trimming whitespace while in other scenarios they shouldn't be trimmed to consider a slot empty.

Here is a version that removes comments, it can be adapted:

import { VNode, Comment } from 'vue'

export function isVNodeEmpty(
  vnodes: VNode | VNode[] | undefined | null
): boolean {
  return (
    !!vnodes &&
    (Array.isArray(vnodes)
      ? vnodes.every(vnode => vnode.type !== Comment)
      : vnodes.type !== Comment)
  )
}
avatar
Oct 4th 2021

Thanks for the reply @posva this seems like a good base solution, do you think there's a chance that it can be implemented as the official one? I'm more than happy to copy/paste and call it resolved, but still gives me the feels that it has too much internal API "know-how" in it

avatar
Oct 4th 2021

I don't think it makes sense to add this API if we cannot reach a consensus on what isVNodeEmpty() means (ref the comment I linked).

There is nothing internal about that function, it's safe to use

avatar
Oct 4th 2021

Thanks @posva makes sense, closing

avatar
Dec 29th 2021

Another approach is a runtime match using the :empty CSS pseudo class:

<template>
    <div v-if="hasContent">
        <div ref="content">
            <slot/>
        </div>
    </div>
</template>

<script>
export default {
    data () {
        return {
            hasContent: true,
        };
    },

    mounted () {
        this.hasContent = !this.$refs.content.matches(':empty');
    },
};
</script>

The initial hasContent: true ensures the component is fully rendered before determining if the slot was empty.

avatar
Jan 28th 2022

@posva this helper function does not work any more...

I would like to add my vote to the request of having a built in function which does this, or a dictionary on the slots object or something. Currently it's impossible to figure out exactly what is an empty slot and what isn't.

Hmm it seems like it's actually a problem with the way slots are rendered ( will render a Symbol("Text") vnode even if the slot is empty, while slots.default returns undefined) - could this be considered a bug?

avatar
Jan 29th 2022

My version of helper:

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() !== '')
    );
  });
}

Vote for official way.

avatar
Jan 29th 2022

@andrey-hohlov thanks a mill! this seems to work well.

@posva I wonder if there is some way to make this better and more optimised by populating some variable/s on InternalSlots when a template is being compiled?

avatar
Jan 29th 2022

EDIT: Seems it works just fine, ignore this comment.

Previous Comment... @andrey-hohlov I might have spoken too soon - I don't think vnode.type === Comment and vnode.type !== Text actually work - the text one at least is of type Symbol("Text") (on vue 3.2.26 at least) - also I have not managed to get a comment at all - even an empty slot returns Symbol("Text")

avatar
Jan 31st 2022

@mika76

Can you please show me your code?

I made this sandbox https://codesandbox.io/s/smoosh-leftpad-wh6h1?file=/src/App.vue And tried to cover all cases and it works everywhere.

avatar
Jan 31st 2022

@andrey-hohlov it's very strange...

In code sandbox my code look a bit like this (using a render function): https://codesandbox.io/s/old-water-skt34?file=/src/App.vue

But it works here in codesandbox. I then take the same function, copy it to my vite/vue 3 app and it does not work. I first thought it was because I use firefox, so tried in chrome, and nope chrome doesn't work either. So I'm just confused 😅

codesandbox is using the same version of vue as I am 3.2.27 but it's not using vite - I wonder if that might have something to do with it. Also I use typescript on my side...

avatar
Jan 31st 2022

Works here too: SFC Playground so I have no idea...

avatar
Jul 13th 2022

The helper from @andrey-hohlov works for me!

However I'd feel better if this was built-in officially so that I don't need to worry about that helper stop working after some future update of vue.

I think many people will face this issue, in particular regarding that many posts on stackoverflow mention the simple condition approach if ($slots.default) { ... } which doesn't work anymore.

avatar
Sep 12th 2022

I think this at least deserves mention in the migration guide. It is a breaking change without any documentation.

While @andrey-hohlov helper is nice, but I'd rather not handle this on the application level – it's not easy to understand and maintain this piece of code.

I don't think it makes sense to add this API if we cannot reach a consensus on what isVNodeEmpty() means (ref the comment I linked).

Couldn't we reuse the same definition of "empty VNode" from Vue 2? Here's a fork of the previously mentioned sandbox, but using Vue 2 – https://codesandbox.io/s/vue-2-empty-slot-handling-pb50zt?file=/src/HelloWorld.vue.

EDIT:

import { Comment } from 'vue';

export const hasSlotContent = (slot, slotProps = {}) => {
    if (!slot) return false;

    return slot(slotProps).some((vnode) => {
        if (Array.isArray(vnode.children)) {
            return !!vnode.children.length;
        }

        return vnode.type !== Comment;
    });
};

seems to exactly match Vue 2 behaviour When slot content is:

  • Text - renders
  • Whitespace (space, tabs, newline) - does not render
  • Comment - does not render
  • <div v-if="false"></div> - does not render
  • <div v-if="true"></div> - renders
  • <Component /> - renders
  • {{ undefined || null || false || "" }} - renders
  • v-if="$slots.nonexsisting"/v-if="hasSlotContent($slots.nonexisting)" - does not throw an error
  • <slot /> - renders only if it has content passed from parent

EDIT I think the function above might still be incorrect in some cases (e.g. when using <Suspense> or <Transition>), use with caution

avatar
Sep 30th 2022

Please check this RFC out.

avatar
Mar 2nd 2023
import {
  Comment,
  Text,
  Slot,
  VNode,
} from 'vue';

You may need to add type when using Vite.

import { Comment, Text, type Slot, type VNode } from 'vue'
avatar
Mar 4th 2023

Here my version of hasSlotContent(), along with other useful helpers. I found it a bit easier to understand what's actually happening when structured this way:

import { Comment } from 'vue'

export function hasSlotContent(slot, props = {}) {
  return !isSlotEmpty(slot, props)
}

export function isSlotEmpty(slot, props = {}) {
  return isVNodeEmpty(slot?.(props))
}

export function isVNodeEmpty(vnode) {
  return !vnode || asArray(vnode).every(vnode => vnode.type === Comment)
}

export function asArray(arg) {
  return isArray(arg) ? arg : arg != null ? [arg] : []
}
avatar
Apr 18th 2023

TS version of @lehni's helpers in case it's useful for anyone else:

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

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));
}

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

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

Edited to make asArray more flexible.

avatar
Apr 25th 2023

BTW I find asArray() generally useful and would allow it any argument types, nut just VNodes

avatar
May 5th 2023

BTW I find asArray() generally useful and would allow it any argument types, nut just VNodes

Been using these helpers for a couple of weeks, updated my comment to add another check in isVNodeEmpty for empty text nodes that I've found useful. Also updated asArray for flexibility as you suggested @lehni.

avatar
May 5th 2023

@rdunk does not adding a check for Text nodes change the fundamental function? My definition of an empty vnode is not a vnode with text in it. Would this not return true if you just put text into a slot?

avatar
May 5th 2023

@mika76 What is your definition of "with text in it"? It checks for empty text nodes, i.e. when the node is of type Text but the node doesn't actually contain content.

avatar
May 6th 2023

There seems to still be a lot of back and forth and people struggling with this, so I'm sharing the composable I use at work for whoever needs: https://gist.github.com/marina-mosti/99b08783b161fa4ba21ebd2ec664fa14#file-useemptyslotcheck-js It builds up on some great ideas posted here, and can probably be improved in many ways but at least is already copy-pasteable

avatar
Aug 27th 2023

updated version of @rdunk solution, I added a check for Fragmanet too, Its usefull if you have <template> in your slot.

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),
    )
  );
}