mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-21 21:25:11 +00:00
feat(nuxt): allow using nuxt-client
in all components (#25479)
This commit is contained in:
parent
a80bdd1e59
commit
6d93014c52
@ -318,7 +318,7 @@ You can partially hydrate a component by setting a `nuxt-client` attribute on th
|
||||
```
|
||||
|
||||
::alert{type=info}
|
||||
This only works within a server component. Slots for client components are not available yet.
|
||||
This only works within a server component. Slots for client components are working only with `experimental.componentIsland.selectiveClient` set to `'deep'` and since they are rendered server-side, they are not interactive once client-side.
|
||||
::
|
||||
|
||||
#### Server Component Context
|
||||
|
@ -260,20 +260,24 @@ export default defineComponent({
|
||||
}
|
||||
if (import.meta.server) {
|
||||
for (const [id, info] of Object.entries(payloads.components ?? {})) {
|
||||
const { html } = info
|
||||
const { html, slots } = info
|
||||
let replaced = html.replaceAll('data-island-uid', `data-island-uid="${uid.value}"`)
|
||||
for (const slot in slots) {
|
||||
replaced = replaced.replaceAll(`data-island-slot="${slot}">`, (full) => full + slots[slot])
|
||||
}
|
||||
teleports.push(createVNode(Teleport, { to: `uid=${uid.value};client=${id}` }, {
|
||||
default: () => [createStaticVNode(html, 1)]
|
||||
default: () => [createStaticVNode(replaced, 1)]
|
||||
}))
|
||||
}
|
||||
}
|
||||
if (selectiveClient && import.meta.client && canLoadClientComponent.value) {
|
||||
} else if (selectiveClient && import.meta.client && canLoadClientComponent.value) {
|
||||
for (const [id, info] of Object.entries(payloads.components ?? {})) {
|
||||
const { props } = info
|
||||
const { props, slots } = info
|
||||
const component = components!.get(id)!
|
||||
// use different selectors for even and odd teleportKey to force trigger the teleport
|
||||
const vnode = createVNode(Teleport, { to: `${isKeyOdd ? 'div' : ''}[data-island-uid='${uid.value}'][data-island-component="${id}"]` }, {
|
||||
default: () => {
|
||||
return [h(component, props)]
|
||||
return [h(component, props, Object.fromEntries(Object.entries(slots || {}).map(([k, v]) => ([k, () => createStaticVNode(`<div style="display: contents" data-island-uid data-island-slot="${k}">${v}</div>`, 1)
|
||||
]))))]
|
||||
}
|
||||
})
|
||||
teleports.push(vnode)
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { Component } from 'vue'
|
||||
import { Teleport, defineComponent, h } from 'vue'
|
||||
import type { Component, InjectionKey } from 'vue'
|
||||
import { Teleport, defineComponent, h, inject, provide } from 'vue'
|
||||
import { useNuxtApp } from '../nuxt'
|
||||
// @ts-expect-error virtual file
|
||||
import { paths } from '#build/components-chunk'
|
||||
@ -9,6 +9,8 @@ type ExtendedComponent = Component & {
|
||||
__name: string
|
||||
}
|
||||
|
||||
export const NuxtTeleportIslandSymbol = Symbol('NuxtTeleportIslandComponent') as InjectionKey<false | string>
|
||||
|
||||
/**
|
||||
* component only used with componentsIsland
|
||||
* this teleport the component in SSR only if it needs to be hydrated on client
|
||||
@ -37,8 +39,10 @@ export default defineComponent({
|
||||
setup (props, { slots }) {
|
||||
const nuxtApp = useNuxtApp()
|
||||
|
||||
if (!nuxtApp.ssrContext?.islandContext || !props.nuxtClient) { return () => slots.default!() }
|
||||
// if there's already a teleport parent, we don't need to teleport or to render the wrapped component client side
|
||||
if (!nuxtApp.ssrContext?.islandContext || !props.nuxtClient || inject(NuxtTeleportIslandSymbol, false)) { return () => slots.default?.() }
|
||||
|
||||
provide(NuxtTeleportIslandSymbol, props.to)
|
||||
const islandContext = nuxtApp.ssrContext!.islandContext!
|
||||
|
||||
return () => {
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { Teleport, defineComponent, h } from 'vue'
|
||||
import type { VNode } from 'vue'
|
||||
import { Teleport, createVNode, defineComponent, h, inject } from 'vue'
|
||||
import { useNuxtApp } from '../nuxt'
|
||||
|
||||
import { NuxtTeleportIslandSymbol } from './nuxt-teleport-island-component'
|
||||
|
||||
/**
|
||||
* component only used within islands for slot teleport
|
||||
*/
|
||||
@ -9,37 +11,50 @@ export default defineComponent({
|
||||
name: 'NuxtTeleportIslandSlot',
|
||||
props: {
|
||||
name: {
|
||||
type: String,
|
||||
required: true
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
/**
|
||||
* must be an array to handle v-for
|
||||
*/
|
||||
props: {
|
||||
type: Object as () => Array<any>
|
||||
type: Object as () => Array<any>
|
||||
}
|
||||
},
|
||||
setup (props, { slots }) {
|
||||
const nuxtApp = useNuxtApp()
|
||||
const islandContext = nuxtApp.ssrContext?.islandContext
|
||||
|
||||
if(!islandContext) {
|
||||
return () => slots.default?.()
|
||||
if (!islandContext) {
|
||||
return () => slots.default?.()[0]
|
||||
}
|
||||
|
||||
const componentName = inject(NuxtTeleportIslandSymbol, false)
|
||||
islandContext.slots[props.name] = {
|
||||
props: (props.props || []) as unknown[]
|
||||
props: (props.props || []) as unknown[]
|
||||
}
|
||||
|
||||
return () => {
|
||||
const vnodes = [h('div', {
|
||||
style: 'display: contents;',
|
||||
'data-island-uid': '',
|
||||
'data-island-slot': props.name,
|
||||
})]
|
||||
const vnodes: VNode[] = []
|
||||
|
||||
if (nuxtApp.ssrContext?.islandContext && slots.default) {
|
||||
vnodes.push(h('div', {
|
||||
style: 'display: contents;',
|
||||
'data-island-uid': '',
|
||||
'data-island-slot': props.name,
|
||||
}, {
|
||||
// Teleport in slot to not be hydrated client-side with the staticVNode
|
||||
default: () => [createVNode(Teleport, { to: `island-slot=${componentName};${props.name}` }, slots.default?.())]
|
||||
}))
|
||||
} else {
|
||||
vnodes.push(h('div', {
|
||||
style: 'display: contents;',
|
||||
'data-island-uid': '',
|
||||
'data-island-slot': props.name,
|
||||
}))
|
||||
}
|
||||
|
||||
if (slots.fallback) {
|
||||
vnodes.push(h(Teleport, { to: `island-fallback=${props.name}`}, slots.fallback()))
|
||||
vnodes.push(h(Teleport, { to: `island-fallback=${props.name}` }, slots.fallback()))
|
||||
}
|
||||
|
||||
return vnodes
|
||||
|
@ -21,7 +21,7 @@ interface ServerOnlyComponentTransformPluginOptions {
|
||||
/**
|
||||
* allow using `nuxt-client` attribute on components
|
||||
*/
|
||||
selectiveClient?: boolean
|
||||
selectiveClient?: boolean | 'deep'
|
||||
}
|
||||
|
||||
interface ComponentChunkOptions {
|
||||
@ -47,6 +47,7 @@ export const islandsTransform = createUnplugin((options: ServerOnlyComponentTran
|
||||
enforce: 'pre',
|
||||
transformInclude (id) {
|
||||
if (!isVue(id)) { return false }
|
||||
if (options.selectiveClient === 'deep') { return true }
|
||||
const components = options.getComponents()
|
||||
|
||||
const islands = components.filter(component =>
|
||||
|
@ -70,6 +70,7 @@ export interface NuxtIslandClientResponse {
|
||||
html: string
|
||||
props: unknown
|
||||
chunk: string
|
||||
slots?: Record<string, string>
|
||||
}
|
||||
|
||||
export interface NuxtIslandResponse {
|
||||
@ -629,6 +630,7 @@ function getServerComponentHTML (body: string[]): string {
|
||||
|
||||
const SSR_SLOT_TELEPORT_MARKER = /^uid=([^;]*);slot=(.*)$/
|
||||
const SSR_CLIENT_TELEPORT_MARKER = /^uid=([^;]*);client=(.*)$/
|
||||
const SSR_CLIENT_SLOT_MARKER = /^island-slot=(?:[^;]*);(.*)$/
|
||||
|
||||
function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['slots'] {
|
||||
if (!ssrContext.islandContext) { return {} }
|
||||
@ -636,7 +638,7 @@ function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse[
|
||||
for (const slot in ssrContext.islandContext.slots) {
|
||||
response[slot] = {
|
||||
...ssrContext.islandContext.slots[slot],
|
||||
fallback: ssrContext.teleports?.[`island-fallback=${slot}`]
|
||||
fallback: ssrContext.teleports?.[`island-fallback=${slot}`],
|
||||
}
|
||||
}
|
||||
return response
|
||||
@ -645,16 +647,33 @@ function getSlotIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse[
|
||||
function getClientIslandResponse (ssrContext: NuxtSSRContext): NuxtIslandResponse['components'] {
|
||||
if (!ssrContext.islandContext) { return {} }
|
||||
const response: NuxtIslandResponse['components'] = {}
|
||||
|
||||
for (const clientUid in ssrContext.islandContext.components) {
|
||||
const html = ssrContext.teleports?.[clientUid] || ''
|
||||
response[clientUid] = {
|
||||
...ssrContext.islandContext.components[clientUid],
|
||||
html,
|
||||
slots: getComponentSlotTeleport(ssrContext.teleports ?? {})
|
||||
}
|
||||
}
|
||||
return response
|
||||
}
|
||||
|
||||
function getComponentSlotTeleport (teleports: Record<string, string>) {
|
||||
const entries = Object.entries(teleports)
|
||||
const slots: Record<string, string> = {}
|
||||
|
||||
for (const [key, value] of entries) {
|
||||
const match = key.match(SSR_CLIENT_SLOT_MARKER)
|
||||
if (match) {
|
||||
const [, slot] = match
|
||||
if (!slot) { continue }
|
||||
slots[slot] = value
|
||||
}
|
||||
}
|
||||
return slots
|
||||
}
|
||||
|
||||
function replaceIslandTeleports (ssrContext: NuxtSSRContext, html: string) {
|
||||
const { teleports, islandContext } = ssrContext
|
||||
|
||||
|
@ -386,7 +386,7 @@ export const nuxtConfigTemplate: NuxtTemplate = {
|
||||
`export const cookieStore = ${!!ctx.nuxt.options.experimental.cookieStore}`,
|
||||
`export const appManifest = ${!!ctx.nuxt.options.experimental.appManifest}`,
|
||||
`export const remoteComponentIslands = ${typeof ctx.nuxt.options.experimental.componentIslands === 'object' && ctx.nuxt.options.experimental.componentIslands.remoteIsland}`,
|
||||
`export const selectiveClient = ${typeof ctx.nuxt.options.experimental.componentIslands === 'object' && ctx.nuxt.options.experimental.componentIslands.selectiveClient}`,
|
||||
`export const selectiveClient = ${typeof ctx.nuxt.options.experimental.componentIslands === 'object' && Boolean(ctx.nuxt.options.experimental.componentIslands.selectiveClient)}`,
|
||||
`export const devPagesDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.dir.pages) : 'null'}`,
|
||||
`export const devRootDir = ${ctx.nuxt.options.dev ? JSON.stringify(ctx.nuxt.options.rootDir) : 'null'}`,
|
||||
`export const nuxtLinkDefaults = ${JSON.stringify(ctx.nuxt.options.experimental.defaults.nuxtLink)}`,
|
||||
|
@ -1610,6 +1610,12 @@ describe('server components/islands', () => {
|
||||
// test islands wrapped with client-only
|
||||
expect(await page.locator('#wrapped-client-only').innerHTML()).toContain('Was router enabled')
|
||||
|
||||
if (!isWebpack) {
|
||||
// test nested client components
|
||||
await page.locator('.server-with-nested-client button').click()
|
||||
expect(await page.locator('.server-with-nested-client .sugar-counter').innerHTML()).toContain('Sugar Counter 13 x 1 = 13')
|
||||
}
|
||||
|
||||
if (!isWebpack) {
|
||||
// test client component interactivity
|
||||
expect(await page.locator('.interactive-component-wrapper').innerHTML()).toContain('Sugar Counter 12')
|
||||
@ -1667,9 +1673,9 @@ describe('server components/islands', () => {
|
||||
const text = (await page.innerText('pre')).replaceAll(/ data-island-uid="([^"]*)"/g, '').replace(/data-island-component="([^"]*)"/g, (_, content) => `data-island-component="${content.split('-')[0]}"`)
|
||||
|
||||
if (isWebpack) {
|
||||
expect(text).toMatchInlineSnapshot('" End page <pre></pre><section id="fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"></div><!--]--></div></section><section id="no-fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"></div><!--]--></div></section><div> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component bellow is not a slot but declared as interactive <div class="sugar-counter" nuxt-client=""> Sugar Counter 12 x 1 = 12 <button> Inc </button></div></div></div>"')
|
||||
expect(text).toMatchInlineSnapshot(`" End page <pre></pre><section id="fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><section id="no-fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><div> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component bellow is not a slot but declared as interactive <div class="sugar-counter" nuxt-client=""> Sugar Counter 12 x 1 = 12 <button> Inc </button></div></div></div>"`)
|
||||
} else {
|
||||
expect(text).toMatchInlineSnapshot(`" End page <pre></pre><section id="fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"></div><!--]--></div></section><section id="no-fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"></div><!--]--></div></section><div> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component bellow is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-component="Counter"></div><!--teleport start--><!--teleport end--><!--]--></div></div>"`)
|
||||
expect(text).toMatchInlineSnapshot(`" End page <pre></pre><section id="fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><section id="no-fallback"><div> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">42</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div></section><div> ServerWithClient.server.vue : <p>count: 0</p> This component should not be preloaded <div><!--[--><div>a</div><div>b</div><div>c</div><!--]--></div> This is not interactive <div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><div class="interactive-component-wrapper" style="border:solid 1px red;"> The component bellow is not a slot but declared as interactive <!--[--><div style="display: contents;" data-island-component="Counter"></div><!--teleport start--><!--teleport end--><!--]--></div></div>"`)
|
||||
}
|
||||
expect(text).toContain('async component that was very long')
|
||||
|
||||
@ -1928,7 +1934,7 @@ describe('component islands', () => {
|
||||
"link": [],
|
||||
"style": [],
|
||||
},
|
||||
"html": "<div data-island-uid><div> count is above 2 </div><!--[--><div style="display: contents;" data-island-uid data-island-slot="default"></div><!--]--> that was very long ... <div id="long-async-component-count">3</div> <!--[--><div style="display: contents;" data-island-uid data-island-slot="test"></div><!--]--><p>hello world !!!</p><!--[--><div style="display: contents;" data-island-uid data-island-slot="hello"></div><!--teleport start--><!--teleport end--><!--]--><!--[--><div style="display: contents;" data-island-uid data-island-slot="fallback"></div><!--teleport start--><!--teleport end--><!--]--></div>",
|
||||
"html": "<div data-island-uid><div> count is above 2 </div><!--[--><div style="display: contents;" data-island-uid data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--> that was very long ... <div id="long-async-component-count">3</div> <!--[--><div style="display: contents;" data-island-uid data-island-slot="test"><!--teleport start--><!--teleport end--></div><!--]--><p>hello world !!!</p><!--[--><div style="display: contents;" data-island-uid data-island-slot="hello"><!--teleport start--><!--teleport end--></div><!--teleport start--><!--teleport end--><!--]--><!--[--><div style="display: contents;" data-island-uid data-island-slot="fallback"><!--teleport start--><!--teleport end--></div><!--teleport start--><!--teleport end--><!--]--></div>",
|
||||
"slots": {
|
||||
"default": {
|
||||
"props": [],
|
||||
@ -1992,7 +1998,7 @@ describe('component islands', () => {
|
||||
"link": [],
|
||||
"style": [],
|
||||
},
|
||||
"html": "<div data-island-uid> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">2</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-uid data-island-slot="default"></div><!--]--></div>",
|
||||
"html": "<div data-island-uid> This is a .server (20ms) async component that was very long ... <div id="async-server-component-count">2</div><div class="sugar-counter"> Sugar Counter 12 x 1 = 12 <button> Inc </button></div><!--[--><div style="display: contents;" data-island-uid data-island-slot="default"><!--teleport start--><!--teleport end--></div><!--]--></div>",
|
||||
"props": {},
|
||||
"slots": {},
|
||||
"state": {},
|
||||
|
9
test/fixtures/basic/components/CounterWithNuxtClient.vue
vendored
Normal file
9
test/fixtures/basic/components/CounterWithNuxtClient.vue
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
<template>
|
||||
<div>
|
||||
this is a normal component within a server component
|
||||
<Counter
|
||||
nuxt-client
|
||||
:multiplier="1"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
5
test/fixtures/basic/components/ServerWithNestedClient.server.vue
vendored
Normal file
5
test/fixtures/basic/components/ServerWithNestedClient.server.vue
vendored
Normal file
@ -0,0 +1,5 @@
|
||||
<template>
|
||||
<div class="server-with-nested-client">
|
||||
<CounterWithNuxtClient />
|
||||
</div>
|
||||
</template>
|
2
test/fixtures/basic/nuxt.config.ts
vendored
2
test/fixtures/basic/nuxt.config.ts
vendored
@ -211,7 +211,7 @@ export default defineNuxtConfig({
|
||||
restoreState: true,
|
||||
clientNodeCompat: true,
|
||||
componentIslands: {
|
||||
selectiveClient: true
|
||||
selectiveClient: 'deep'
|
||||
},
|
||||
treeshakeClientOnly: true,
|
||||
asyncContext: process.env.TEST_CONTEXT === 'async',
|
||||
|
1
test/fixtures/basic/pages/islands.vue
vendored
1
test/fixtures/basic/pages/islands.vue
vendored
@ -110,6 +110,7 @@ const count = ref(0)
|
||||
</div>
|
||||
</div>
|
||||
<ServerWithClient />
|
||||
<ServerWithNestedClient />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -26,7 +26,7 @@ vi.mock('vue', async (original) => {
|
||||
const consoleError = vi.spyOn(console, 'error')
|
||||
const consoleWarn = vi.spyOn(console, 'warn')
|
||||
|
||||
function expectNoConsoleIssue() {
|
||||
function expectNoConsoleIssue () {
|
||||
expect(consoleError).not.toHaveBeenCalled()
|
||||
expect(consoleWarn).not.toHaveBeenCalled()
|
||||
}
|
||||
@ -93,7 +93,7 @@ describe('runtime server component', () => {
|
||||
link: [],
|
||||
style: []
|
||||
},
|
||||
json() {
|
||||
json () {
|
||||
return this
|
||||
}
|
||||
}
|
||||
@ -123,7 +123,7 @@ describe('client components', () => {
|
||||
vi.doMock(mockPath, () => ({
|
||||
default: {
|
||||
name: 'ClientComponent',
|
||||
setup() {
|
||||
setup () {
|
||||
return () => h('div', 'client component')
|
||||
}
|
||||
}
|
||||
@ -145,7 +145,7 @@ describe('client components', () => {
|
||||
chunk: mockPath
|
||||
}
|
||||
},
|
||||
json() {
|
||||
json () {
|
||||
return this
|
||||
}
|
||||
}
|
||||
@ -184,14 +184,14 @@ describe('client components', () => {
|
||||
style: []
|
||||
},
|
||||
components: {},
|
||||
json() {
|
||||
json () {
|
||||
return this
|
||||
}
|
||||
}))
|
||||
|
||||
await wrapper.vm.$.exposed!.refresh()
|
||||
await nextTick()
|
||||
expect(wrapper.html()).toMatchInlineSnapshot( `
|
||||
expect(wrapper.html()).toMatchInlineSnapshot(`
|
||||
"<div data-island-uid="3">hello<div>
|
||||
<div>fallback</div>
|
||||
</div>
|
||||
@ -202,10 +202,10 @@ describe('client components', () => {
|
||||
expectNoConsoleIssue()
|
||||
})
|
||||
|
||||
|
||||
|
||||
it('should not replace nested client components data-island-uid', async () => {
|
||||
const componentId = 'Client-12345'
|
||||
|
||||
|
||||
const stubFetch = vi.fn(() => {
|
||||
return {
|
||||
id: '1234',
|
||||
@ -215,7 +215,7 @@ describe('client components', () => {
|
||||
link: [],
|
||||
style: []
|
||||
},
|
||||
json() {
|
||||
json () {
|
||||
return this
|
||||
}
|
||||
}
|
||||
@ -238,4 +238,66 @@ describe('client components', () => {
|
||||
vi.mocked(fetch).mockReset()
|
||||
expectNoConsoleIssue()
|
||||
})
|
||||
})
|
||||
|
||||
it('pass a slot to a client components within islands', async () => {
|
||||
const mockPath = '/nuxt-client-with-slot.js'
|
||||
const componentId = 'ClientWithSlot-12345'
|
||||
|
||||
vi.doMock(mockPath, () => ({
|
||||
default: {
|
||||
name: 'ClientWithSlot',
|
||||
setup (_, { slots }) {
|
||||
return () => h('div', { class: "client-component" }, slots.default())
|
||||
}
|
||||
}
|
||||
}))
|
||||
|
||||
const stubFetch = vi.fn(() => {
|
||||
return {
|
||||
id: '123',
|
||||
html: `<div data-island-uid>hello<div data-island-uid data-island-component="${componentId}"></div></div>`,
|
||||
state: {},
|
||||
head: {
|
||||
link: [],
|
||||
style: []
|
||||
},
|
||||
components: {
|
||||
[componentId]: {
|
||||
html: '<div>fallback</div>',
|
||||
props: {},
|
||||
chunk: mockPath,
|
||||
slots: {
|
||||
default: '<div>slot in client component</div>'
|
||||
}
|
||||
}
|
||||
},
|
||||
json () {
|
||||
return this
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
vi.stubGlobal('fetch', stubFetch)
|
||||
const wrapper = await mountSuspended(NuxtIsland, {
|
||||
props: {
|
||||
name: 'NuxtClientWithSlot',
|
||||
},
|
||||
attachTo: 'body'
|
||||
})
|
||||
expect(fetch).toHaveBeenCalledOnce()
|
||||
expect(wrapper.html()).toMatchInlineSnapshot(`
|
||||
"<div data-island-uid="5">hello<div data-island-uid="5" data-island-component="ClientWithSlot-12345">
|
||||
<div class="client-component">
|
||||
<div style="display: contents" data-island-uid="" data-island-slot="default">
|
||||
<div>slot in client component</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--teleport start-->
|
||||
<!--teleport end-->"
|
||||
`)
|
||||
|
||||
expectNoConsoleIssue()
|
||||
})
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user