Last active
October 22, 2024 21:29
-
-
Save marina-mosti/99b08783b161fa4ba21ebd2ec664fa14 to your computer and use it in GitHub Desktop.
Vue 3 check for slot with no content
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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') | |
}) | |
}) | |
}) |
Using vnodes.some(…)
and inverting the logic instead of every
might speed things up in case there are many vnodes.
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
I made a slightly different version, but I encounter a weird behavior: Vue SFC demo
It works great when using
v-show
, but when usingv-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 thatonBeforeUpdate
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: