Skip to content

Commit

Permalink
improve SSR for Tabs in Vue
Browse files Browse the repository at this point in the history
  • Loading branch information
RobinMalfait committed Dec 6, 2022
1 parent ef3e154 commit c958372
Show file tree
Hide file tree
Showing 2 changed files with 103 additions and 4 deletions.
57 changes: 56 additions & 1 deletion 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'
Expand Down Expand Up @@ -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`
<TabGroup>
<TabList>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
<Tab>Tab 3</Tab>
</TabList>
<TabPanels>
<TabPanel>Content 1</TabPanel>
<TabPanel>Content 2</TabPanel>
<TabPanel>Content 3</TabPanel>
</TabPanels>
</TabGroup>
`,
})

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`
<TabGroup :defaultIndex="1">
<TabList>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
<Tab>Tab 3</Tab>
</TabList>
<TabPanels>
<TabPanel>Content 1</TabPanel>
<TabPanel>Content 2</TabPanel>
<TabPanel>Content 3</TabPanel>
</TabPanels>
</TabGroup>
`,
})

let contents = await renderToString(app)
expect(contents).not.toContain(`Content 1`)
expect(contents).toContain(`Content 2`)
expect(contents).not.toContain(`Content 3`)
})
})
})

describe('`selectedIndex`', () => {
Expand Down
50 changes: 47 additions & 3 deletions packages/@headlessui-vue/src/components/tabs/tabs.ts
Expand Up @@ -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({
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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 () => {
Expand Down

0 comments on commit c958372

Please sign in to comment.