mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-25 15:15:19 +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({
|
||||
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)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -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()))
|
||||
})
|
||||
})
|
||||
|
||||
|
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>
|
||||
<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>
|
||||
|
Loading…
Reference in New Issue
Block a user