diff --git a/packages/@headlessui-vue/CHANGELOG.md b/packages/@headlessui-vue/CHANGELOG.md index 65b9c049a..d31bb3cba 100644 --- a/packages/@headlessui-vue/CHANGELOG.md +++ b/packages/@headlessui-vue/CHANGELOG.md @@ -16,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Fix crash when using `multiple` mode without `value` prop (uncontrolled) for `Listbox` and `Combobox` components ([#2058](https://github.com/tailwindlabs/headlessui/pull/2058)) - Allow passing in your own `id` prop ([#2060](https://github.com/tailwindlabs/headlessui/pull/2060)) - Add `null` as a valid type for Listbox and Combobox in Vue ([#2064](https://github.com/tailwindlabs/headlessui/pull/2064), [#2067](https://github.com/tailwindlabs/headlessui/pull/2067)) +- Improve SSR for Tabs in Vue ([#2068](https://github.com/tailwindlabs/headlessui/pull/2068)) ## [1.7.4] - 2022-11-03 diff --git a/packages/@headlessui-vue/src/components/tabs/tabs.test.ts b/packages/@headlessui-vue/src/components/tabs/tabs.test.ts index f0a3aa690..af8636c14 100644 --- a/packages/@headlessui-vue/src/components/tabs/tabs.test.ts +++ b/packages/@headlessui-vue/src/components/tabs/tabs.test.ts @@ -1,4 +1,5 @@ -import { nextTick, ref } from 'vue' +import { createSSRApp, nextTick, ref } from 'vue' +import { renderToString } from 'vue/server-renderer' import { createRenderTemplate, render } from '../../test-utils/vue-testing-library' import { TabGroup, TabList, Tab, TabPanels, TabPanel } from './tabs' import { suppressConsoleLogs } from '../../test-utils/suppress-console-logs' @@ -554,6 +555,60 @@ describe('Rendering', () => { assertTabs({ active: 2 }) }) }) + + describe('SSR', () => { + it('should be possible to server side render the first Tab and Panel', async () => { + let app = createSSRApp({ + components: { TabGroup, TabList, Tab, TabPanels, TabPanel }, + template: html` + + + Tab 1 + Tab 2 + Tab 3 + + + + Content 1 + Content 2 + Content 3 + + + `, + }) + + let contents = await renderToString(app) + expect(contents).toContain(`Content 1`) + expect(contents).not.toContain(`Content 2`) + expect(contents).not.toContain(`Content 3`) + }) + + it('should be possible to server side render the defaultIndex Tab and Panel', async () => { + let app = createSSRApp({ + components: { TabGroup, TabList, Tab, TabPanels, TabPanel }, + template: html` + + + Tab 1 + Tab 2 + Tab 3 + + + + Content 1 + Content 2 + Content 3 + + + `, + }) + + let contents = await renderToString(app) + expect(contents).not.toContain(`Content 1`) + expect(contents).toContain(`Content 2`) + expect(contents).not.toContain(`Content 3`) + }) + }) }) describe('`selectedIndex`', () => { diff --git a/packages/@headlessui-vue/src/components/tabs/tabs.ts b/packages/@headlessui-vue/src/components/tabs/tabs.ts index 4cc35ed10..dd38c2ca0 100644 --- a/packages/@headlessui-vue/src/components/tabs/tabs.ts +++ b/packages/@headlessui-vue/src/components/tabs/tabs.ts @@ -58,6 +58,10 @@ function useTabsContext(component: string) { return context } +let TabsSSRContext = Symbol('TabsSSRContext') as InjectionKey< + Ref<{ tabs: string[]; panels: string[] } | null> +> + // --- export let TabGroup = defineComponent({ @@ -84,7 +88,7 @@ export let TabGroup = defineComponent({ ) let api = { - selectedIndex, + selectedIndex: computed(() => selectedIndex.value ?? props.defaultIndex ?? null), orientation: computed(() => (props.vertical ? 'vertical' : 'horizontal')), activation: computed(() => (props.manual ? 'manual' : 'auto')), tabs, @@ -116,6 +120,16 @@ export let TabGroup = defineComponent({ provide(TabsContext, api) + let SSRCounter = ref({ tabs: [], panels: [] }) + let mounted = ref(false) + onMounted(() => { + mounted.value = true + }) + provide( + TabsSSRContext, + computed(() => (mounted.value ? null : SSRCounter.value)) + ) + watchEffect(() => { if (api.tabs.value.length <= 0) return if (props.selectedIndex === null && selectedIndex.value !== null) return @@ -231,7 +245,22 @@ export let Tab = defineComponent({ onMounted(() => api.registerTab(internalTabRef)) onUnmounted(() => api.unregisterTab(internalTabRef)) - let myIndex = computed(() => api.tabs.value.indexOf(internalTabRef)) + let SSRContext = inject(TabsSSRContext)! + let mySSRIndex = computed(() => { + if (SSRContext.value) { + let mySSRIndex = SSRContext.value.tabs.indexOf(props.id) + if (mySSRIndex === -1) return SSRContext.value.tabs.push(props.id) - 1 + return mySSRIndex + } + + return -1 + }) + + let myIndex = computed(() => { + let myIndex = api.tabs.value.indexOf(internalTabRef) + if (myIndex === -1) return mySSRIndex.value + return myIndex + }) let selected = computed(() => myIndex.value === api.selectedIndex.value) function activateUsing(cb: () => FocusResult) { @@ -391,7 +420,22 @@ export let TabPanel = defineComponent({ onMounted(() => api.registerPanel(internalPanelRef)) onUnmounted(() => api.unregisterPanel(internalPanelRef)) - let myIndex = computed(() => api.panels.value.indexOf(internalPanelRef)) + let SSRContext = inject(TabsSSRContext)! + let mySSRIndex = computed(() => { + if (SSRContext.value) { + let mySSRIndex = SSRContext.value.panels.indexOf(props.id) + if (mySSRIndex === -1) return SSRContext.value.panels.push(props.id) - 1 + return mySSRIndex + } + + return -1 + }) + + let myIndex = computed(() => { + let myIndex = api.panels.value.indexOf(internalPanelRef) + if (myIndex === -1) return mySSRIndex.value + return myIndex + }) let selected = computed(() => myIndex.value === api.selectedIndex.value) return () => {