Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

fix(nuxt): remove fragment from createClientOnly #7774

Merged
merged 11 commits into from Oct 3, 2022
25 changes: 17 additions & 8 deletions packages/nuxt/src/app/components/client-only.mjs
@@ -1,4 +1,4 @@
import { ref, onMounted, defineComponent, createElementBlock, h, Fragment } from 'vue'
import { ref, onMounted, defineComponent, createElementBlock, h } from 'vue'

export default defineComponent({
name: 'ClientOnly',
Expand Down Expand Up @@ -30,9 +30,14 @@ export function createClientOnly (component) {
if (clone.render) {
// override the component render (non script setup component)
clone.render = (ctx, ...args) => {
return ctx.mounted$
? h(Fragment, ctx.$attrs ?? ctx._.attrs, component.render(ctx, ...args))
: h('div', ctx.$attrs ?? ctx._.attrs)
if (ctx.mounted$) {
const res = component.render(ctx, ...args)
return (res.children === null || typeof res.children === 'string')
? createElementBlock(res.type, res.props, res.children, res.patchFlag, res.dynamicProps, res.shapeFlag)
: h(res)
} else {
return h('div', ctx.$attrs ?? ctx._.attrs)
}
}
} else if (clone.template) {
// handle runtime-compiler template
Expand All @@ -51,10 +56,14 @@ export function createClientOnly (component) {
return typeof setupState !== 'function'
? { ...setupState, mounted$ }
: (...args) => {
return mounted$.value
// use Fragment to avoid oldChildren is null issue
? h(Fragment, ctx.attrs, setupState(...args))
: h('div', ctx.attrs)
if (mounted$.value) {
const res = setupState(...args)
return (res.children === null || typeof res.children === 'string')
? createElementBlock(res.type, res.props, res.children, res.patchFlag, res.dynamicProps, res.shapeFlag)
: h(res)
} else {
return h('div', ctx.attrs)
}
}
})
}
Expand Down
55 changes: 55 additions & 0 deletions test/basic.test.ts
Expand Up @@ -144,13 +144,68 @@ describe('pages', () => {
})

it('/client-only-components', async () => {
// ensure components are not rendered server-side
const html = await $fetch('/client-only-components')
expect(html).toContain('<div class="client-only-script" foo="bar">')
expect(html).toContain('<div class="client-only-script-setup" foo="hello">')
expect(html).toContain('<div>Fallback</div>')
expect(html).not.toContain('Should not be server rendered')
danielroe marked this conversation as resolved.
Show resolved Hide resolved

await expectNoClientErrors('/client-only-components')

const page = await createPage('/client-only-components')

await page.waitForLoadState('networkidle')

const hiddenSelectors = [
'.string-stateful-should-be-hidden',
'.client-script-should-be-hidden',
'.string-stateful-script-should-be-hidden'
]
const visibleSelectors = [
'.string-stateful',
'.string-stateful-script',
'.client-only-script',
'.client-only-script-setup',
'.no-state'
]
// ensure directives are correctly applied
await Promise.all(hiddenSelectors.map(selector => page.locator(selector).isHidden()))
.then(results => results.forEach(isHidden => expect(isHidden).toBeTruthy()))
// ensure hidden components are still rendered
await Promise.all(hiddenSelectors.map(selector => page.locator(selector).innerHTML()))
.then(results => results.forEach(innerHTML => expect(innerHTML).not.toBe('')))

// ensure single root node components are rendered once on client (should not be empty)
await Promise.all(visibleSelectors.map(selector => page.locator(selector).innerHTML()))
.then(results => results.forEach(innerHTML => expect(innerHTML).not.toBe('')))

// ensure multi-root-node is correctly rendered
expect(await page.locator('.multi-root-node-count').innerHTML()).toContain('0')
expect(await page.locator('.multi-root-node-button').innerHTML()).toContain('add 1 to count')
expect(await page.locator('.multi-root-node-script-count').innerHTML()).toContain('0')
expect(await page.locator('.multi-root-node-script-button').innerHTML()).toContain('add 1 to count')

// ensure components reactivity
await page.locator('.multi-root-node-button').click()
await page.locator('.multi-root-node-script-button').click()
await page.locator('.client-only-script button').click()
await page.locator('.client-only-script-setup button').click()

expect(await page.locator('.multi-root-node-count').innerHTML()).toContain('1')
expect(await page.locator('.multi-root-node-script-count').innerHTML()).toContain('1')
expect(await page.locator('.client-only-script-setup button').innerHTML()).toContain('1')
expect(await page.locator('.client-only-script button').innerHTML()).toContain('1')

// ensure components ref is working and reactive
await page.locator('button.test-ref-1').click()
await page.locator('button.test-ref-2').click()
await page.locator('button.test-ref-3').click()
await page.locator('button.test-ref-4').click()
expect(await page.locator('.client-only-script-setup button').innerHTML()).toContain('2')
expect(await page.locator('.client-only-script button').innerHTML()).toContain('2')
expect(await page.locator('.string-stateful-script').innerHTML()).toContain('1')
expect(await page.locator('.string-stateful').innerHTML()).toContain('1')
})
})

Expand Down
14 changes: 14 additions & 0 deletions test/fixtures/basic/components/client/MultiRootNode.vue
@@ -0,0 +1,14 @@
<template>
<div v-bind="$attrs" class="multi-root-node-count">
{{ count }}
</div>
<button class="multi-root-node-button" @click="add">
add 1 to count
</button>
</template>

<script setup>
const count = ref(0)

const add = () => count.value++
</script>
19 changes: 19 additions & 0 deletions test/fixtures/basic/components/client/MultiRootNodeScript.vue
@@ -0,0 +1,19 @@
<template>
<div v-bind="$attrs" class="multi-root-node-script-count">
{{ count }}
</div>
<button class="multi-root-node-script-button" @click="add">
add 1 to count
</button>
</template>

<script>
export default defineNuxtComponent({
setup () {
const count = ref(0)

const add = () => count.value++
return { count, add }
}
})
</script>
3 changes: 3 additions & 0 deletions test/fixtures/basic/components/client/NoState.vue
@@ -0,0 +1,3 @@
<template>
<div>Hello world !</div>
</template>
45 changes: 45 additions & 0 deletions test/fixtures/basic/components/client/Script.client.vue
@@ -0,0 +1,45 @@
<script lang="ts">
export default defineNuxtComponent({
name: 'ClientScript',
props: {
foo: {
type: String
}
},
setup (_p, ctx) {
const count = ref(0)
const add = () => count.value++

ctx.expose({ add })

return {
count,
add
}
}
})
</script>

<template>
<div>
<div class="client-only-css">
client only script component {{ foo }}
</div>
<button @click="add">
{{ count }}
</button>
<slot name="test" />
</div>
</template>

<style>
:root {
--client-only: "client-only";
}
</style>

<style scoped>
.client-only-css {
color: rgb(50, 50, 50);
}
</style>
18 changes: 18 additions & 0 deletions test/fixtures/basic/components/client/SetupScript.client.vue
@@ -0,0 +1,18 @@
<script setup lang="ts">
const props = defineProps<{ foo: string }>()
const count = ref(0)
const add = () => count.value++

defineExpose({ add })
</script>

<template>
<div>
<div>client only script setup component {{ props.foo }}</div>
<button @click="add">
{{ count }}
</button>

<slot name="test" />
</div>
</template>
@@ -0,0 +1,14 @@
<script setup lang="ts">
const state = ref(0)

const add = () => state.value++

defineExpose({
state,
add
})
</script>

<template>
<div>Hi i should be rendered {{ state }}</div>
</template>
@@ -0,0 +1,18 @@
<script lang="ts">
export default defineNuxtComponent({
setup (_p, ctx) {
const state = ref(0)

const add = () => state.value++

ctx.expose({ add, state })
return {
state
}
}
})
</script>

<template>
<div>Hi i should be rendered {{ state }}</div>
</template>
48 changes: 45 additions & 3 deletions test/fixtures/basic/pages/client-only-components.vue
@@ -1,18 +1,60 @@
<template>
<div>
<ClientOnlyScript class="client-only-script" foo="bar" />
<ClientOnlySetupScript class="client-only-script-setup" foo="hello">
<ClientScript ref="clientScript" class="client-only-script" foo="bar" />
<ClientSetupScript
ref="clientSetupScript"
class="client-only-script-setup"
foo="hello"
>
<template #test>
<div class="slot-test">
Hello
</div>
</template>
</ClientOnlySetupScript>
</ClientSetupScript>
<ClientOnly>
Should not be server rendered.
<template #fallback>
<div>Fallback</div>
</template>
</ClientOnly>
<!-- ensure multi root node components are correctly rendered (Fragment) -->
<ClientMultiRootNode class="multi-root-node" />
<ClientMultiRootNodeScript class="multi-root-node-script" />

<!-- ensure components with a single single child are correctly rendered -->
<ClientStringChildStateful ref="stringStatefulComp" class="string-stateful" />
<ClientStringChildStatefulScript
ref="stringStatefulScriptComp"
class="string-stateful-script"
/>
<!-- ensure directives are correctly passed -->
<ClientStringChildStateful v-show="false" class="string-stateful-should-be-hidden" />
<ClientSetupScript v-show="false" class="client-script-should-be-hidden" foo="bar" />
<ClientStringChildStatefulScript
v-show="false"
class="string-stateful-script-should-be-hidden"
/>
<ClientNoState class="no-state" />

<button class="test-ref-1" @click="stringStatefulComp.add">
increment count
</button>
<button class="test-ref-2" @click="stringStatefulScriptComp.add">
increment count
</button>
<button class="test-ref-3" @click="clientScript.add">
increment count
</button>
<button class="test-ref-4" @click="clientSetupScript.add">
increment count
</button>
</div>
</template>

<script setup lang="ts">
const stringStatefulComp = ref(null)
const stringStatefulScriptComp = ref(null)
const clientScript = ref(null)
const clientSetupScript = ref(null)
</script>