fix(nuxt): remove fragment from createClientOnly (#7774)

Co-authored-by: Daniel Roe <daniel@roe.dev>
Co-authored-by: jhuang@hsk-partners.com <jhuang@hsk-partners.com>
This commit is contained in:
Julien Huang 2022-10-03 16:14:55 +02:00 committed by GitHub
parent 2c8c21209b
commit e6ca07bdc0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 262 additions and 11 deletions

View File

@ -1,4 +1,4 @@
import { ref, onMounted, defineComponent, createElementBlock, h, Fragment } from 'vue'
import { ref, onMounted, defineComponent, createElementBlock, h, createElementVNode } from 'vue'
export default defineComponent({
name: 'ClientOnly',
@ -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')
? createElementVNode(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
@ -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')
? createElementVNode(res.type, res.props, res.children, res.patchFlag, res.dynamicProps, res.shapeFlag)
: h(res)
} else {
return h('div', ctx.attrs)
}
}
})
}

View File

@ -145,12 +145,74 @@ describe('pages', () => {
it('/client-only-components', async () => {
const html = await $fetch('/client-only-components')
// ensure fallbacks with classes and arbitrary attributes are rendered
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>')
// ensure components are not rendered server-side
expect(html).not.toContain('Should not be server rendered')
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',
'.no-state-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')
// ensure directives are reactive
await page.locator('button#show-all').click()
await Promise.all(hiddenSelectors.map(selector => page.locator(selector).isVisible()))
.then(results => results.forEach(isVisible => expect(isVisible).toBeTruthy()))
})
})

View File

@ -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>

View File

@ -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>

View File

@ -0,0 +1,3 @@
<template>
<div>Hello world !</div>
</template>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -1,18 +1,67 @@
<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"
/>
<ClientNoState class="no-state" />
<!-- ensure directives are correctly passed -->
<ClientStringChildStateful v-show="show" class="string-stateful-should-be-hidden" />
<ClientSetupScript v-show="show" class="client-script-should-be-hidden" foo="bar" />
<ClientStringChildStatefulScript
v-show="show"
class="string-stateful-script-should-be-hidden"
/>
<ClientNoState v-show="show" class="no-state-hidden" />
<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>
<button id="show-all" @click="show = true">
Show all
</button>
</div>
</template>
<script setup lang="ts">
const stringStatefulComp = ref(null)
const stringStatefulScriptComp = ref(null)
const clientScript = ref(null)
const clientSetupScript = ref(null)
const show = ref(false)
</script>