Skip to content

Instantly share code, notes, and snippets.

@marina-mosti
Last active October 22, 2024 21:29
Show Gist options
  • 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')
})
})
})
@raimund-schluessler
Copy link

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

@thedamon
Copy link

I can't comment on 4733 because it's closed and locked, but I am realizing that Vue itself has logic to determine if it would show the default content passed to a slot doesn't it?

So if we treat it as wouldDefaultSlotContentRender? this doesn't have to be treated as subjective, and the logic to determine this already mostly exists in Vue...

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