Skip to content

Instantly share code, notes, and snippets.

@marina-mosti
Last active April 23, 2024 09:40
Show Gist options
  • Star 10 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save marina-mosti/99b08783b161fa4ba21ebd2ec664fa14 to your computer and use it in GitHub Desktop.
Save marina-mosti/99b08783b161fa4ba21ebd2ec664fa14 to your computer and use it in GitHub Desktop.
Vue 3 check for slot with no content
import { ref, computed, onBeforeUpdate, Comment, Fragment, Text, useSlots } from 'vue'
// Adapted from https://github.com/vuejs/vue-next/blob/ca17162e377e0a0bf3fae9d92d0fdcb32084a9fe/packages/runtime-core/src/helpers/renderSlot.ts#L77
// Demo: https://play.vuejs.org/#eNqVVm1P3EYQ/iuDU+UOdNiEa0NBHE2gRE1bJVWg/RJH1Z49PhvstbW7PkDX+++d2bV9vmCIggTa3Xl7ZuaZMSvvbVX5yxq9E+9URyqrDGg0dXUWyqyoSmXgoiwqSFRZwMgP+MLqo068AoXJBEp5jkmp8O8qFgYnUGu8ykujYd3YOqNQRqXUBgq9gBlbjke/ZTuj3Y3ElItFbm1JYbwLszMKEUpgE38p8hrpfXP+BWSd53AC1k8o16E8DVwilAJdDBZVTpDoBnDK+O0JYLWyKNZk4R46VXghRYFnaXYabFvb9K1busxrY0oJb6I8i25nobcBHnpnF/wIBZ4GTo2Mes68iWc0pZtkC/9Gl5KKv2KfoRdRgCxH9bEyGZUj9E7ASlgm8ry8+92+GVXjpH2PUoxuB95v9D2/hd5fCjWqJYZeJzNCLZCAsvjy6gPe07kTFmVc56T9jPAT6jKvGaNTO69lTLB7ehbte8uRTC6u9eW9QanbpBgoa66tfugRO7i2T6W+gTv1p9aOOk1VbOnI7N1ulWYCcRu5M6jJFoKehM9b3aXrMP2/j9+N0RX7uCwq88BKF9ygboQGZL3BYHSaGN76H9NoOMkKCK0z0hOIMckkxi2G2VDIsXVmZ2sb/riZK64HOy9z9PNyMXbl3oRxM7YdzL2xJjle0283btSPARBEcOrO88WkNhYozQTeKbFwp2ti3aPdEeztUeA9+BUNqoIgabhL0aSoQNjKQaYdfCD38A/th+kJpMZU+iQIFplJ67lPIxaQwxvNf/clxQkyrWvUwfTgp9fW/5tKKFHA6l0tIybj2jnfh+sU3fEH/uszv5yBosSV1LA6L6mYgtcQ7AUWcwBvY1EZjF0y30Yzz8t5EIlXR69eH+L06AgPxME8mSYCj+Pjw/ggiaP59PDg5x/FcYJBJaJbsSD0qpYmK3A/osIGWkVBinmFigQoY1TcEN/oF38eHTGspEkNlh/KGN9r2zQYLyXd9K6jhksK3JuPS1QPYz533AHIErBPvnmo6H02a3u521q3kz6ka5v88iXs2OcozfKYwPpGZcV49wkP7gSw7aklDnlrFfqJubhtACItKzRZ8s+jSN1HoZEkItduYTHr+1z81HSebRlgQ8RUaILI00VrzwyyCv6Dj/MbjExDL+EI6y5df4jHTnDoBKU1eZ51eG+nze2NrOntzNm3veNa7vDLV3VmGXGWxHcItHTbhEwq7HhJi7SFNyGtkaLEiSYWo5AxqSI/6Tb7CegSMjNyxnY+WwTcwDJxEXaoj6PW8aiD1ZS+T8h+Z9mUyNJ0pcmcFpao814Vm1W4WXkNDrc/3X8iK+5tK93ar07++YtdpRuNZgmxzh/4wGq9vQoDW5M0XMf9W9Lv1nNHN+fYfs+7JTrg9qu4/IlwImf/r8S7KyNMzaYr5sQgGJ9W5KWIUguD/PRCQM/H50b8hZw1VHLAO0EzTly94Q8IWW789dJ9+qNEqWwXgXPchBgQ9ujReOh9LR+XoP0fIpTe+n9hfcRC
function vNodeIsEmpty (vnodes) {
return vnodes.every(node => {
if (node.type === Comment) return true
if (node.type === Text && !node.children.trim()) return true
if (
node.type === Fragment &&
vNodeIsEmpty(node.children)
) {
return true
}
return false
})
}
/**
* Returns true if a slot has no content
* @param {Function | Object} slot a Vue 3 slot function or a Vue 2 slot object
* @returns {Boolean}
*/
export const isEmpty = (slot, props) => {
if (!slot) return true
// if we get a slot that is not a function, we're in vue 2 and there is content, so it's not empty
if (typeof slot !== 'function') return false
return vNodeIsEmpty(slot(props))
}
/**
* @param {String} [slotName=default] Optional string to check for a specific slot's status with the slotIsEmpty convinience computed
* @param {Object} [scopedSlotInjection={}] Object key/value collection of data to be injected to scoped slots
* @returns
*/
export default function (slotName = 'default', scopedSlotInjection = {}) {
const emptySlots = ref({})
const definedSlots = ref([])
const checkEmptySlots = () => {
const slots = useSlots()
console.log(slots)
definedSlots.value = Object.keys(slots)
const _newStatus = {}
definedSlots.value.forEach(slotKey => {
_newStatus[slotKey] = isEmpty(slots[slotKey], scopedSlotInjection[slotKey])
})
emptySlots.value = _newStatus
}
onBeforeUpdate(() => {
checkEmptySlots()
})
checkEmptySlots()
/** Single slot data - Requires `slotName` */
const slotIsEmpty = computed(() => {
if (!slotName) return undefined
return !definedSlots.value.includes(slotName) || emptySlots.value[slotName]
})
return {
emptySlots,
definedSlots,
slotIsEmpty
}
}
import SetupEmptySlotCheck from './SetupEmptySlotCheck'
import { mount } from '@vue/test-utils'
import { ref } from 'vue'
const ComponentWithSlots = {
name: 'ComponentWithSlots',
template: `
<div>
Default:
<slot />
Named:
<slot name="namedSlot" />
Empty:
<slot name="leaveEmpty" />
Empty string:
<slot name="emptyString" />
Comment:
<slot name="comment" />
Fragment:
<slot name="fragment" />
<p v-if="!definedSlots.includes('default') || emptySlots.default">Default is empty</p>
<p v-if="slotIsEmpty">Named is empty</p>
</div>
`,
setup (_, { slots }) {
// Multi slot check
const { emptySlots, definedSlots } = SetupEmptySlotCheck(slots)
// Single slot check
const { slotIsEmpty } = SetupEmptySlotCheck(slots, 'namedSlot')
return {
emptySlots,
definedSlots,
slotIsEmpty
}
}
}
function factory (opts = {}) {
return mount({
name: 'ComponentUsingSlots',
components: { ComponentWithSlots },
template: opts.template,
setup: opts.setup ? opts.setup : () => ({})
})
}
let wrapper
describe('SetupEmptySlotCheck', () => {
beforeEach(() => {
jest.clearAllMocks()
})
afterEach(() => {
if (wrapper) wrapper.unmount()
})
describe('identifying empty slots', () => {
describe('default slots', () => {
it('marks the default slot as empty when it has no content', async () => {
const template = `
<ComponentWithSlots>
<template #default></template>
</ComponentWithSlots>
`
wrapper = factory({ template })
await wrapper.vm.$nextTick()
expect(wrapper.text()).toContain('Default is empty')
})
it('marks the default slot as not empty when it has content', async () => {
const template = `
<ComponentWithSlots>
<template #default>
Some content!
</template>
</ComponentWithSlots>
`
wrapper = factory({ template })
await wrapper.vm.$nextTick()
expect(wrapper.text()).not.toContain('Default is empty')
})
})
describe('named slots', () => {
it('marks a named slot as empty when it has no content', async () => {
const template = `
<ComponentWithSlots>
<template #namedSlot></template>
</ComponentWithSlots>
`
wrapper = factory({ template })
await wrapper.vm.$nextTick()
expect(wrapper.text()).toContain('Named is empty')
})
it('marks a named slot as not empty when it has content', async () => {
const template = `
<ComponentWithSlots>
<template #namedSlot>
Some named content!
</template>
</ComponentWithSlots>
`
wrapper = factory({ template })
await wrapper.vm.$nextTick()
expect(wrapper.text()).not.toContain('Named is empty')
})
})
describe('other empty conditions', () => {
it('marks a slot as empty if the parent puts nothing in the slot', async () => {
const template = `
<ComponentWithSlots />
`
wrapper = factory({ template })
await wrapper.vm.$nextTick()
expect(wrapper.text()).toContain('Named is empty')
})
it('marks a slot as empty if the slot only contains whitespace', async () => {
const template = `
<ComponentWithSlots>
<template #namedSlot>
</template>
</ComponentWithSlots>
`
wrapper = factory({ template })
await wrapper.vm.$nextTick()
expect(wrapper.text()).toContain('Named is empty')
})
it('marks a slot as empty if the slot only contains a comment', async () => {
const template = `
<ComponentWithSlots>
<template #namedSlot>
<!-- Comment -->
</template>
</ComponentWithSlots>
`
wrapper = factory({ template })
await wrapper.vm.$nextTick()
expect(wrapper.text()).toContain('Named is empty')
})
it('marks a slot as empty if the slot only contains another empty slot', async () => {
const template = `
<ComponentWithSlots>
<template #namedSlot>
<slot name="anotherSlot" />
</template>
</ComponentWithSlots>
`
wrapper = factory({ template })
await wrapper.vm.$nextTick()
expect(wrapper.text()).toContain('Named is empty')
})
})
})
describe('handling changes to slot content', () => {
it('marks a slot as empty when it becomes empty', async () => {
const template = `
<ComponentWithSlots>
<template #default>
<div v-if="showSlotContent">Slot content!</div>
</template>
</ComponentWithSlots>
`
const showSlotContent = ref(true)
wrapper = factory({
template,
setup () {
return { showSlotContent }
}
})
expect(wrapper.text()).not.toContain('Default is empty')
showSlotContent.value = false
await wrapper.vm.$nextTick()
expect(wrapper.text()).toContain('Default is empty')
})
it('marks the slot as not empty when it gets content', async () => {
const template = `
<ComponentWithSlots>
<template #default>
<div v-if="showSlotContent">Slot content!</div>
</template>
</ComponentWithSlots>
`
const showSlotContent = ref(false)
wrapper = factory({
template,
setup () {
return { showSlotContent }
}
})
expect(wrapper.text()).toContain('Default is empty')
showSlotContent.value = true
await wrapper.vm.$nextTick()
expect(wrapper.text()).not.toContain('Default is empty')
})
})
})
@nicooprat
Copy link

Many thanks for sharing this!

It still seems to miss reactivity though: if the slot content changes, the computed won't be triggered.

Did you find a way to fix this?

I'm currently migrating through Vue compat mode so I hope it's not related to this.

@marina-mosti
Copy link
Author

Hey @nicooprat yes, you have a good point. I'll have to rewrite it with an onBeforeUpdate hook to account for this type of change

@marina-mosti
Copy link
Author

Many thanks for sharing this!

It still seems to miss reactivity though: if the slot content changes, the computed won't be triggered.

Did you find a way to fix this?

I'm currently migrating through Vue compat mode so I hope it's not related to this.

Give that a shot @nicooprat 👌

@nicooprat
Copy link

Thanks! Though I'm not sure to understand how it's meant to be used. For instance, if I try to display or not a div wrapping the default slot according to the slot emptiness, it doesn't seem to work (only once apparently): Vue SFC demo

I'll try to dig into your script if I can find some time 👍

@nicooprat
Copy link

nicooprat commented Jul 21, 2023

I made a slightly different version, but I encounter a weird behavior: Vue SFC demo

It works great when using v-show, but when using v-if it stops working as soon as the slot is empty: clicking the button hides the slot, clicking again won't show it up again. It seems that onBeforeUpdate is not triggered anymore.

It's hard to believe how difficult it is to reactively know if a slot has content in Vue 3...

Edit: some related issues:

@raimund-schluessler
Copy link

Using vnodes.some(…) and inverting the logic instead of every might speed things up in case there are many vnodes.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment