mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 13:45:18 +00:00
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:
parent
2c8c21209b
commit
e6ca07bdc0
@ -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({
|
export default defineComponent({
|
||||||
name: 'ClientOnly',
|
name: 'ClientOnly',
|
||||||
@ -30,9 +30,14 @@ export function createClientOnly (component) {
|
|||||||
if (clone.render) {
|
if (clone.render) {
|
||||||
// override the component render (non script setup component)
|
// override the component render (non script setup component)
|
||||||
clone.render = (ctx, ...args) => {
|
clone.render = (ctx, ...args) => {
|
||||||
return ctx.mounted$
|
if (ctx.mounted$) {
|
||||||
? h(Fragment, ctx.$attrs ?? ctx._.attrs, component.render(ctx, ...args))
|
const res = component.render(ctx, ...args)
|
||||||
: h('div', ctx.$attrs ?? ctx._.attrs)
|
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) {
|
} else if (clone.template) {
|
||||||
// handle runtime-compiler template
|
// handle runtime-compiler template
|
||||||
@ -51,10 +56,14 @@ export function createClientOnly (component) {
|
|||||||
return typeof setupState !== 'function'
|
return typeof setupState !== 'function'
|
||||||
? { ...setupState, mounted$ }
|
? { ...setupState, mounted$ }
|
||||||
: (...args) => {
|
: (...args) => {
|
||||||
return mounted$.value
|
if (mounted$.value) {
|
||||||
// use Fragment to avoid oldChildren is null issue
|
const res = setupState(...args)
|
||||||
? h(Fragment, ctx.attrs, setupState(...args))
|
return (res.children === null || typeof res.children === 'string')
|
||||||
: h('div', ctx.attrs)
|
? createElementVNode(res.type, res.props, res.children, res.patchFlag, res.dynamicProps, res.shapeFlag)
|
||||||
|
: h(res)
|
||||||
|
} else {
|
||||||
|
return h('div', ctx.attrs)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -145,12 +145,74 @@ describe('pages', () => {
|
|||||||
|
|
||||||
it('/client-only-components', async () => {
|
it('/client-only-components', async () => {
|
||||||
const html = await $fetch('/client-only-components')
|
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" foo="bar">')
|
||||||
expect(html).toContain('<div class="client-only-script-setup" foo="hello">')
|
expect(html).toContain('<div class="client-only-script-setup" foo="hello">')
|
||||||
expect(html).toContain('<div>Fallback</div>')
|
expect(html).toContain('<div>Fallback</div>')
|
||||||
|
// ensure components are not rendered server-side
|
||||||
expect(html).not.toContain('Should not be server rendered')
|
expect(html).not.toContain('Should not be server rendered')
|
||||||
|
|
||||||
await expectNoClientErrors('/client-only-components')
|
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()))
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
14
test/fixtures/basic/components/client/MultiRootNode.client.vue
vendored
Normal file
14
test/fixtures/basic/components/client/MultiRootNode.client.vue
vendored
Normal 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>
|
19
test/fixtures/basic/components/client/MultiRootNodeScript.client.vue
vendored
Normal file
19
test/fixtures/basic/components/client/MultiRootNodeScript.client.vue
vendored
Normal 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>
|
3
test/fixtures/basic/components/client/NoState.client.vue
vendored
Normal file
3
test/fixtures/basic/components/client/NoState.client.vue
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<div>Hello world !</div>
|
||||||
|
</template>
|
45
test/fixtures/basic/components/client/Script.client.vue
vendored
Normal file
45
test/fixtures/basic/components/client/Script.client.vue
vendored
Normal 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>
|
18
test/fixtures/basic/components/client/SetupScript.client.vue
vendored
Normal file
18
test/fixtures/basic/components/client/SetupScript.client.vue
vendored
Normal 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>
|
14
test/fixtures/basic/components/client/StringChildStateful.client.vue
vendored
Normal file
14
test/fixtures/basic/components/client/StringChildStateful.client.vue
vendored
Normal 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>
|
18
test/fixtures/basic/components/client/StringChildStatefulScript.client.vue
vendored
Normal file
18
test/fixtures/basic/components/client/StringChildStatefulScript.client.vue
vendored
Normal 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>
|
@ -1,18 +1,67 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<ClientOnlyScript class="client-only-script" foo="bar" />
|
<ClientScript ref="clientScript" class="client-only-script" foo="bar" />
|
||||||
<ClientOnlySetupScript class="client-only-script-setup" foo="hello">
|
<ClientSetupScript
|
||||||
|
ref="clientSetupScript"
|
||||||
|
class="client-only-script-setup"
|
||||||
|
foo="hello"
|
||||||
|
>
|
||||||
<template #test>
|
<template #test>
|
||||||
<div class="slot-test">
|
<div class="slot-test">
|
||||||
Hello
|
Hello
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</ClientOnlySetupScript>
|
</ClientSetupScript>
|
||||||
<ClientOnly>
|
<ClientOnly>
|
||||||
Should not be server rendered.
|
Should not be server rendered.
|
||||||
<template #fallback>
|
<template #fallback>
|
||||||
<div>Fallback</div>
|
<div>Fallback</div>
|
||||||
</template>
|
</template>
|
||||||
</ClientOnly>
|
</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>
|
</div>
|
||||||
</template>
|
</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>
|
||||||
|
Loading…
Reference in New Issue
Block a user