Skip to content
This repository was archived by the owner on Apr 6, 2023. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 17 additions & 8 deletions packages/nuxt/src/app/components/client-only.mjs
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
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')

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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<template>
<div>Hello world !</div>
</template>
45 changes: 45 additions & 0 deletions test/fixtures/basic/components/client/Script.client.vue
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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>