mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 05:35:13 +00:00
feat(nuxt): add <NuxtClientFallback>
component (#8216)
This commit is contained in:
parent
c4222b1f6e
commit
1729d2e42f
@ -342,6 +342,28 @@ The content will not be included in production builds and tree-shaken.
|
||||
</template>
|
||||
```
|
||||
|
||||
## <NuxtClientFallback> Component
|
||||
|
||||
::StabilityEdge
|
||||
|
||||
Nuxt provides the `<NuxtClientFallback>` component to render its content on the client if any of its children trigger an error in SSR.
|
||||
You can specify a `fallbackTag` to make it render a specific tag if it fails to render on the server.
|
||||
|
||||
```html{}[pages/example.vue]
|
||||
<template>
|
||||
<div>
|
||||
<Sidebar />
|
||||
<!-- this component will be rendered on client-side -->
|
||||
<NuxtClientFallback fallback-tag="span">
|
||||
<Comments />
|
||||
<BrokeInSSR />
|
||||
</NuxtClientFallback>
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
::
|
||||
|
||||
## Library Authors
|
||||
|
||||
Making Vue component libraries with automatic tree-shaking and component registration is super easy ✨
|
||||
|
60
docs/3.api/2.components/1.nuxt-client-fallback.md
Normal file
60
docs/3.api/2.components/1.nuxt-client-fallback.md
Normal file
@ -0,0 +1,60 @@
|
||||
---
|
||||
title: "<NuxtClientFallback>"
|
||||
description: "Nuxt provides `<NuxtClientFallback>` component to render its content on the client if any of its children trigger an error in SSR"
|
||||
---
|
||||
|
||||
# `<NuxtClientFallback>`
|
||||
|
||||
Nuxt provides a `<NuxtClientFallback>` component to render its content on the client if any of its children trigger an error in SSR.
|
||||
|
||||
::alert{type=warning}
|
||||
This component is experimental and in order to use it you must enable the `experimental.clientFallback` option in your `nuxt.config`.
|
||||
::
|
||||
|
||||
:StabilityEdge{title=NuxtClientFallback}
|
||||
|
||||
## Events
|
||||
|
||||
- **`@ssr-error`**: Event emitted when a child triggers an error in SSR. Note that this will only be triggered on the server.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<NuxtClientFallback @ssr-error="logSomeError">
|
||||
<!-- ... -->
|
||||
</NuxtClientFallback>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Props
|
||||
|
||||
- **placeholderTag** | **fallbackTag**: Specify a fallback tag to be rendered if the slot fails to render.
|
||||
- **type**: `string`
|
||||
- **default**: `div`
|
||||
- **placeholder** | **fallback**: Specify fallback content to be rendered if the slot fails to render.
|
||||
- **type**: `string`
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<!-- render <span>Hello world</span> server-side if the default slot fails to render -->
|
||||
<NuxtClientFallback fallback-tag="span" fallback="Hello world">
|
||||
<BrokeInSsr />
|
||||
</NuxtClientFallback>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Slots
|
||||
|
||||
- **#fallback**: specify content to be displayed server-side if the slot fails to render.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
<NuxtClientFallback>
|
||||
<!-- ... -->
|
||||
<template #fallback>
|
||||
<!-- this will be rendered on server side if the default slot fails to render in ssr -->
|
||||
<p>Hello world</p>
|
||||
</template>
|
||||
</NuxtClientFallback>
|
||||
</template>
|
||||
```
|
||||
|
46
packages/nuxt/src/app/components/client-fallback.client.mjs
Normal file
46
packages/nuxt/src/app/components/client-fallback.client.mjs
Normal file
@ -0,0 +1,46 @@
|
||||
import { defineComponent, createElementBlock } from 'vue'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NuxtClientFallback',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
uid: {
|
||||
type: String
|
||||
},
|
||||
fallbackTag: {
|
||||
type: String,
|
||||
default: () => 'div'
|
||||
},
|
||||
fallback: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
},
|
||||
placeholder: {
|
||||
type: String
|
||||
},
|
||||
placeholderTag: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
emits: ['ssr-error'],
|
||||
setup (props, ctx) {
|
||||
const mounted = ref(false)
|
||||
const ssrFailed = useState(`${props.uid}`)
|
||||
|
||||
if (ssrFailed.value) {
|
||||
onMounted(() => { mounted.value = true })
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (mounted.value) { return ctx.slots.default?.() }
|
||||
if (ssrFailed.value) {
|
||||
const slot = ctx.slots.placeholder || ctx.slots.fallback
|
||||
if (slot) { return slot() }
|
||||
const fallbackStr = props.placeholder || props.fallback
|
||||
const fallbackTag = props.placeholderTag || props.fallbackTag
|
||||
return createElementBlock(fallbackTag, null, fallbackStr)
|
||||
}
|
||||
return ctx.slots.default?.()
|
||||
}
|
||||
}
|
||||
})
|
74
packages/nuxt/src/app/components/client-fallback.server.mjs
Normal file
74
packages/nuxt/src/app/components/client-fallback.server.mjs
Normal file
@ -0,0 +1,74 @@
|
||||
import { defineComponent, getCurrentInstance, onErrorCaptured } from 'vue'
|
||||
import { ssrRenderVNode, ssrRenderAttrs, ssrRenderSlot } from 'vue/server-renderer'
|
||||
import { createBuffer } from './utils'
|
||||
|
||||
const NuxtClientFallbackServer = defineComponent({
|
||||
name: 'NuxtClientFallback',
|
||||
inheritAttrs: false,
|
||||
props: {
|
||||
uid: {
|
||||
type: String
|
||||
},
|
||||
fallbackTag: {
|
||||
type: String,
|
||||
default: () => 'div'
|
||||
},
|
||||
fallback: {
|
||||
type: String,
|
||||
default: () => ''
|
||||
},
|
||||
placeholder: {
|
||||
type: String
|
||||
},
|
||||
placeholderTag: {
|
||||
type: String
|
||||
}
|
||||
},
|
||||
emits: ['ssr-error'],
|
||||
setup (props, ctx) {
|
||||
const vm = getCurrentInstance()
|
||||
const ssrFailed = ref(false)
|
||||
|
||||
onErrorCaptured(() => {
|
||||
useState(`${props.uid}`, () => true)
|
||||
ssrFailed.value = true
|
||||
ctx.emit('ssr-error')
|
||||
return false
|
||||
})
|
||||
|
||||
try {
|
||||
const defaultSlot = ctx.slots.default?.()
|
||||
const ssrVNodes = createBuffer()
|
||||
|
||||
for (let i = 0; i < defaultSlot.length; i++) {
|
||||
ssrRenderVNode(ssrVNodes.push, defaultSlot[i], vm)
|
||||
}
|
||||
|
||||
return { ssrFailed, ssrVNodes }
|
||||
} catch {
|
||||
// catch in dev
|
||||
useState(`${props.uid}`, () => true)
|
||||
ctx.emit('ssr-error')
|
||||
return { ssrFailed: true, ssrVNodes: [] }
|
||||
}
|
||||
},
|
||||
ssrRender (ctx, push, parent) {
|
||||
if (ctx.ssrFailed) {
|
||||
const { fallback, placeholder } = ctx.$slots
|
||||
if (fallback || placeholder) {
|
||||
ssrRenderSlot(ctx.$slots, fallback ? 'fallback' : 'placeholder', {}, null, push, parent)
|
||||
} else {
|
||||
const content = ctx.placeholder || ctx.fallback
|
||||
const tag = ctx.placeholderTag || ctx.fallbackTag
|
||||
push(`<${tag}${ssrRenderAttrs(ctx.$attrs)}>${content}</${tag}>`)
|
||||
}
|
||||
} else {
|
||||
// push Fragment markup
|
||||
push('<!--[-->')
|
||||
push(ctx.ssrVNodes.getBuffer())
|
||||
push('<!--]-->')
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
export default NuxtClientFallbackServer
|
@ -1,5 +1,7 @@
|
||||
import { defineComponent, h } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
// eslint-disable-next-line
|
||||
import { isString, isPromise, isArray } from '@vue/shared'
|
||||
|
||||
const Fragment = defineComponent({
|
||||
name: 'FragmentWrapper',
|
||||
@ -16,3 +18,35 @@ const Fragment = defineComponent({
|
||||
export const _wrapIf = (component: Component, props: any, slots: any) => {
|
||||
return { default: () => props ? h(component, props === true ? {} : props, slots) : h(Fragment, {}, slots) }
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
export type SSRBuffer = SSRBufferItem[] & { hasAsync?: boolean }
|
||||
export type SSRBufferItem = string | SSRBuffer | Promise<SSRBuffer>
|
||||
|
||||
/**
|
||||
* create buffer retrieved from @vue/server-renderer
|
||||
*
|
||||
* @see https://github.com/vuejs/core/blob/9617dd4b2abc07a5dc40de6e5b759e851b4d0da1/packages/server-renderer/src/render.ts#L57
|
||||
* @private
|
||||
*/
|
||||
export function createBuffer () {
|
||||
let appendable = false
|
||||
const buffer: SSRBuffer = []
|
||||
return {
|
||||
getBuffer (): SSRBuffer {
|
||||
return buffer
|
||||
},
|
||||
push (item: SSRBufferItem) {
|
||||
const isStringItem = isString(item)
|
||||
if (appendable && isStringItem) {
|
||||
buffer[buffer.length - 1] += item as string
|
||||
} else {
|
||||
buffer.push(item)
|
||||
}
|
||||
appendable = isStringItem
|
||||
if (isPromise(item) || (isArray(item) && item.hasAsync)) {
|
||||
buffer.hasAsync = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
53
packages/nuxt/src/components/client-fallback-auto-id.ts
Normal file
53
packages/nuxt/src/components/client-fallback-auto-id.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { createUnplugin } from 'unplugin'
|
||||
import type { ComponentsOptions } from '@nuxt/schema'
|
||||
import MagicString from 'magic-string'
|
||||
import { isAbsolute, relative } from 'pathe'
|
||||
import { hash } from 'ohash'
|
||||
import { isVueTemplate } from './helpers'
|
||||
interface LoaderOptions {
|
||||
sourcemap?: boolean
|
||||
transform?: ComponentsOptions['transform'],
|
||||
rootDir: string
|
||||
}
|
||||
const CLIENT_FALLBACK_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/
|
||||
const CLIENT_FALLBACK_GLOBAL_RE = /<(NuxtClientFallback|nuxt-client-fallback)( [^>]*)?>/g
|
||||
export const clientFallbackAutoIdPlugin = createUnplugin((options: LoaderOptions) => {
|
||||
const exclude = options.transform?.exclude || []
|
||||
const include = options.transform?.include || []
|
||||
|
||||
return {
|
||||
name: 'nuxt:client-fallback-auto-id',
|
||||
enforce: 'pre',
|
||||
transformInclude (id) {
|
||||
if (exclude.some(pattern => id.match(pattern))) {
|
||||
return false
|
||||
}
|
||||
if (include.some(pattern => id.match(pattern))) {
|
||||
return true
|
||||
}
|
||||
return isVueTemplate(id)
|
||||
},
|
||||
transform (code, id) {
|
||||
if (!CLIENT_FALLBACK_RE.test(code)) { return }
|
||||
|
||||
const s = new MagicString(code)
|
||||
const relativeID = isAbsolute(id) ? relative(options.rootDir, id) : id
|
||||
let count = 0
|
||||
|
||||
s.replace(CLIENT_FALLBACK_GLOBAL_RE, (full, name, attrs) => {
|
||||
count++
|
||||
if (/ :?uid=/g.test(attrs)) { return full }
|
||||
return `<${name} :uid="'${hash(relativeID)}' + JSON.stringify($props) + '${count}'" ${attrs ?? ''}>`
|
||||
})
|
||||
|
||||
if (s.hasChanged()) {
|
||||
return {
|
||||
code: s.toString(),
|
||||
map: options.sourcemap
|
||||
? s.generateMap({ source: id, includeContent: true })
|
||||
: undefined
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
29
packages/nuxt/src/components/helpers.ts
Normal file
29
packages/nuxt/src/components/helpers.ts
Normal file
@ -0,0 +1,29 @@
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { parseQuery, parseURL } from 'ufo'
|
||||
|
||||
export function isVueTemplate (id: string) {
|
||||
// Bare `.vue` file (in Vite)
|
||||
if (id.endsWith('.vue')) {
|
||||
return true
|
||||
}
|
||||
|
||||
const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
||||
if (!search) {
|
||||
return false
|
||||
}
|
||||
|
||||
const query = parseQuery(search)
|
||||
|
||||
// Macro
|
||||
if (query.macro) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Non-Vue or Styles
|
||||
if (!('vue' in query) || query.type === 'style') {
|
||||
return false
|
||||
}
|
||||
|
||||
// Query `?vue&type=template` (in Webpack or external template)
|
||||
return true
|
||||
}
|
@ -1,11 +1,10 @@
|
||||
import { pathToFileURL } from 'node:url'
|
||||
import { createUnplugin } from 'unplugin'
|
||||
import { parseQuery, parseURL } from 'ufo'
|
||||
import { genDynamicImport, genImport } from 'knitwork'
|
||||
import MagicString from 'magic-string'
|
||||
import { pascalCase } from 'scule'
|
||||
import { resolve } from 'pathe'
|
||||
import { distDir } from '../dirs'
|
||||
import { isVueTemplate } from './helpers'
|
||||
import type { Component, ComponentsOptions } from 'nuxt/schema'
|
||||
|
||||
interface LoaderOptions {
|
||||
@ -16,33 +15,6 @@ interface LoaderOptions {
|
||||
experimentalComponentIslands?: boolean
|
||||
}
|
||||
|
||||
function isVueTemplate (id: string) {
|
||||
// Bare `.vue` file (in Vite)
|
||||
if (id.endsWith('.vue')) {
|
||||
return true
|
||||
}
|
||||
|
||||
const { search } = parseURL(decodeURIComponent(pathToFileURL(id).href))
|
||||
if (!search) {
|
||||
return false
|
||||
}
|
||||
|
||||
const query = parseQuery(search)
|
||||
|
||||
// Macro
|
||||
if (query.macro) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Non-Vue or Styles
|
||||
if (!('vue' in query) || query.type === 'style') {
|
||||
return false
|
||||
}
|
||||
|
||||
// Query `?vue&type=template` (in webpack or external template)
|
||||
return true
|
||||
}
|
||||
|
||||
export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
|
||||
const exclude = options.transform?.exclude || []
|
||||
const include = options.transform?.include || []
|
||||
@ -86,7 +58,7 @@ export const loaderPlugin = createUnplugin((options: LoaderOptions) => {
|
||||
return identifier
|
||||
}
|
||||
|
||||
const isClientOnly = component.mode === 'client'
|
||||
const isClientOnly = component.mode === 'client' && component.pascalName !== 'NuxtClientFallback'
|
||||
if (isClientOnly) {
|
||||
imports.add(genImport('#app/components/client-only', [{ name: 'createClientOnly' }]))
|
||||
identifier += '_client'
|
||||
|
@ -2,6 +2,7 @@ import { statSync } from 'node:fs'
|
||||
import { relative, resolve } from 'pathe'
|
||||
import { defineNuxtModule, resolveAlias, addTemplate, addPluginTemplate, updateTemplates } from '@nuxt/kit'
|
||||
import { distDir } from '../dirs'
|
||||
import { clientFallbackAutoIdPlugin } from './client-fallback-auto-id'
|
||||
import { componentsPluginTemplate, componentsTemplate, componentsIslandsTemplate, componentsTypeTemplate } from './templates'
|
||||
import { scanComponents } from './scan'
|
||||
import { loaderPlugin } from './loader'
|
||||
@ -198,6 +199,10 @@ export default defineNuxtModule<ComponentsOptions>({
|
||||
getComponents
|
||||
}))
|
||||
}
|
||||
config.plugins.push(clientFallbackAutoIdPlugin.vite({
|
||||
sourcemap: nuxt.options.sourcemap[mode],
|
||||
rootDir: nuxt.options.rootDir
|
||||
}))
|
||||
config.plugins.push(loaderPlugin.vite({
|
||||
sourcemap: nuxt.options.sourcemap[mode],
|
||||
getComponents,
|
||||
@ -216,6 +221,10 @@ export default defineNuxtModule<ComponentsOptions>({
|
||||
getComponents
|
||||
}))
|
||||
}
|
||||
config.plugins.push(clientFallbackAutoIdPlugin.webpack({
|
||||
sourcemap: nuxt.options.sourcemap[mode],
|
||||
rootDir: nuxt.options.rootDir
|
||||
}))
|
||||
config.plugins.push(loaderPlugin.webpack({
|
||||
sourcemap: nuxt.options.sourcemap[mode],
|
||||
getComponents,
|
||||
|
@ -220,6 +220,23 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
filePath: resolve(nuxt.options.appDir, 'components/nuxt-loading-indicator')
|
||||
})
|
||||
|
||||
// Add <NuxtClientFallback>
|
||||
if (nuxt.options.experimental.clientFallback) {
|
||||
addComponent({
|
||||
name: 'NuxtClientFallback',
|
||||
priority: 10, // built-in that we do not expect the user to override
|
||||
filePath: resolve(nuxt.options.appDir, 'components/client-fallback.client'),
|
||||
mode: 'client'
|
||||
})
|
||||
|
||||
addComponent({
|
||||
name: 'NuxtClientFallback',
|
||||
priority: 10, // built-in that we do not expect the user to override
|
||||
filePath: resolve(nuxt.options.appDir, 'components/client-fallback.server'),
|
||||
mode: 'server'
|
||||
})
|
||||
}
|
||||
|
||||
// Add <NuxtIsland>
|
||||
if (nuxt.options.experimental.componentIslands) {
|
||||
addComponent({
|
||||
|
@ -127,6 +127,12 @@ export default defineUntypedSchema({
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Whether to enable the experimental `<NuxtClientFallback>` component for rendering content on the client
|
||||
* if there's an error in SSR.
|
||||
*/
|
||||
clientFallback: false,
|
||||
|
||||
/** Enable cross-origin prefetch using the Speculation Rules API. */
|
||||
crossOriginPrefetch: false,
|
||||
|
||||
|
@ -280,6 +280,37 @@ describe('pages', () => {
|
||||
expect(html).not.toContain('client only script')
|
||||
await expectNoClientErrors('/client-only-explicit-import')
|
||||
})
|
||||
|
||||
it('client-fallback', async () => {
|
||||
const classes = [
|
||||
'clientfallback-non-stateful-setup',
|
||||
'clientfallback-non-stateful',
|
||||
'clientfallback-stateful-setup',
|
||||
'clientfallback-stateful'
|
||||
]
|
||||
const html = await $fetch('/client-fallback')
|
||||
// ensure failed components are not rendered server-side
|
||||
expect(html).not.toContain('This breaks in server-side setup.')
|
||||
classes.forEach(c => expect(html).not.toContain(c))
|
||||
// ensure not failed component not be rendered
|
||||
expect(html).not.toContain('Sugar Counter 12 x 0 = 0')
|
||||
// ensure NuxtClientFallback is being rendered with its fallback tag and attributes
|
||||
expect(html).toContain('<span class="break-in-ssr">this failed to render</span>')
|
||||
// ensure Fallback slot is being rendered server side
|
||||
expect(html).toContain('Hello world !')
|
||||
|
||||
// ensure not failed component are correctly rendered
|
||||
expect(html).not.toContain('<p></p>')
|
||||
expect(html).toContain('hi')
|
||||
|
||||
await expectNoClientErrors('/client-fallback')
|
||||
|
||||
const page = await createPage('/client-fallback')
|
||||
await page.waitForLoadState('networkidle')
|
||||
// ensure components reactivity once mounted
|
||||
await page.locator('#increment-count').click()
|
||||
expect(await page.locator('#sugar-counter').innerHTML()).toContain('Sugar Counter 12 x 1 = 12')
|
||||
})
|
||||
})
|
||||
|
||||
describe('nuxt links', () => {
|
||||
|
@ -40,7 +40,7 @@ describe.skipIf(isWindows)('minimal nuxt application', () => {
|
||||
|
||||
it('default server bundle size', async () => {
|
||||
stats.server = await analyzeSizes(['**/*.mjs', '!node_modules'], serverDir)
|
||||
expect(stats.server.totalBytes).toBeLessThan(94400)
|
||||
expect(stats.server.totalBytes).toBeLessThan(94450)
|
||||
|
||||
const modules = await analyzeSizes('node_modules/**/*', serverDir)
|
||||
expect(modules.totalBytes).toBeLessThan(2713000)
|
||||
|
10
test/fixtures/basic/components/BreakInSetup.vue
vendored
Normal file
10
test/fixtures/basic/components/BreakInSetup.vue
vendored
Normal file
@ -0,0 +1,10 @@
|
||||
<script setup>
|
||||
// break server-side
|
||||
const data = window.__NUXT__
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
This breaks in server-side setup. {{ data.serverRendered }}
|
||||
</div>
|
||||
</template>
|
14
test/fixtures/basic/components/clientFallback/NonStateful.vue
vendored
Normal file
14
test/fixtures/basic/components/clientFallback/NonStateful.vue
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
<template>
|
||||
<div>
|
||||
non stateful
|
||||
<NuxtClientFallback>
|
||||
<BreakInSetup class="clientfallback-non-stateful" />
|
||||
</NuxtClientFallback>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default defineNuxtComponent({
|
||||
name: 'ClientFallbackStateful'
|
||||
})
|
||||
</script>
|
8
test/fixtures/basic/components/clientFallback/NonStatefulSetup.vue
vendored
Normal file
8
test/fixtures/basic/components/clientFallback/NonStatefulSetup.vue
vendored
Normal file
@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div>
|
||||
non stateful setup
|
||||
<NuxtClientFallback>
|
||||
<BreakInSetup class="clientfallback-non-stateful-setup" />
|
||||
</NuxtClientFallback>
|
||||
</div>
|
||||
</template>
|
20
test/fixtures/basic/components/clientFallback/Stateful.vue
vendored
Normal file
20
test/fixtures/basic/components/clientFallback/Stateful.vue
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
<template>
|
||||
<div>
|
||||
stateful test {{ state }}
|
||||
<NuxtClientFallback>
|
||||
<BreakInSetup class="clientfallback-stateful" />
|
||||
</NuxtClientFallback>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default defineNuxtComponent({
|
||||
name: 'ClientFallbackStateful',
|
||||
setup () {
|
||||
const state = ref(0)
|
||||
return {
|
||||
state
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
13
test/fixtures/basic/components/clientFallback/StatefulSetup.vue
vendored
Normal file
13
test/fixtures/basic/components/clientFallback/StatefulSetup.vue
vendored
Normal file
@ -0,0 +1,13 @@
|
||||
|
||||
<template>
|
||||
<div>
|
||||
stateful setup {{ state }}
|
||||
<NuxtClientFallback>
|
||||
<BreakInSetup class="clientfallback-stateful-setup" />
|
||||
</NuxtClientFallback>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const state = ref(1)
|
||||
</script>
|
1
test/fixtures/basic/nuxt.config.ts
vendored
1
test/fixtures/basic/nuxt.config.ts
vendored
@ -173,6 +173,7 @@ export default defineNuxtConfig({
|
||||
}
|
||||
},
|
||||
experimental: {
|
||||
clientFallback: true,
|
||||
restoreState: true,
|
||||
inlineSSRStyles: id => !!id && !id.includes('assets.vue'),
|
||||
componentIslands: true,
|
||||
|
50
test/fixtures/basic/pages/client-fallback.vue
vendored
Normal file
50
test/fixtures/basic/pages/client-fallback.vue
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
<template>
|
||||
<div>
|
||||
Hello World
|
||||
<div id="locator-for-playwright">
|
||||
<!-- single child -->
|
||||
<NuxtClientFallback fallback-tag="span" class="break-in-ssr" fallback="this failed to render">
|
||||
<BreakInSetup />
|
||||
</NuxtClientFallback>
|
||||
<!-- multi child -->
|
||||
<NuxtClientFallback>
|
||||
<BreakInSetup class="broke-in-ssr" />
|
||||
<BreakInSetup />
|
||||
</NuxtClientFallback>
|
||||
<!-- don't render if one child fails in ssr -->
|
||||
<NuxtClientFallback>
|
||||
<BreakInSetup />
|
||||
<SugarCounter id="sugar-counter" :multiplier="multiplier" />
|
||||
</NuxtClientFallback>
|
||||
<!-- nested children fails -->
|
||||
<NuxtClientFallback>
|
||||
<div>
|
||||
<BreakInSetup />
|
||||
</div>
|
||||
<SugarCounter :multiplier="multiplier" />
|
||||
</NuxtClientFallback>
|
||||
<!-- should be rendered -->
|
||||
<NuxtClientFallback fallback-tag="p">
|
||||
<FunctionalComponent />
|
||||
</NuxtClientFallback>
|
||||
<!-- fallback -->
|
||||
<NuxtClientFallback>
|
||||
<BreakInSetup />
|
||||
<template #fallback>
|
||||
<div>Hello world !</div>
|
||||
</template>
|
||||
</NuxtClientFallback>
|
||||
<ClientFallbackStateful />
|
||||
<ClientFallbackStatefulSetup />
|
||||
<ClientFallbackNonStatefulSetup />
|
||||
<ClientFallbackNonStateful />
|
||||
</div>
|
||||
<button id="increment-count" @click="multiplier++">
|
||||
increment count
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const multiplier = ref(0)
|
||||
</script>
|
Loading…
Reference in New Issue
Block a user