diff --git a/packages/nuxt/src/app/components/layout.ts b/packages/nuxt/src/app/components/layout.ts index bb1b149265..b88def1a94 100644 --- a/packages/nuxt/src/app/components/layout.ts +++ b/packages/nuxt/src/app/components/layout.ts @@ -1,150 +1,2 @@ -import type { Ref, VNode } from 'vue' -import { Suspense, Transition, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, provide, ref, unref } from 'vue' -import type { RouteLocationNormalizedLoaded } from 'vue-router' -import { _wrapIf } from './utils' -import { LayoutMetaSymbol, PageRouteSymbol } from './injections' - -import { useRoute } from '#app/composables/router' -// @ts-expect-error virtual file -import { useRoute as useVueRouterRoute } from '#build/pages' -// @ts-expect-error virtual file -import layouts from '#build/layouts' -// @ts-expect-error virtual file -import { appLayoutTransition as defaultLayoutTransition } from '#build/nuxt.config.mjs' -import { useNuxtApp } from '#app' - -// TODO: revert back to defineAsyncComponent when https://github.com/vuejs/core/issues/6638 is resolved -const LayoutLoader = defineComponent({ - name: 'LayoutLoader', - inheritAttrs: false, - props: { - name: String, - layoutProps: Object - }, - async setup (props, context) { - const LayoutComponent = await layouts[props.name]().then((r: any) => r.default || r) - - return () => h(LayoutComponent, props.layoutProps, context.slots) - } -}) - -export default defineComponent({ - name: 'NuxtLayout', - inheritAttrs: false, - props: { - name: { - type: [String, Boolean, Object] as unknown as () => string | false | Ref, - default: null - } - }, - setup (props, context) { - const nuxtApp = useNuxtApp() - // Need to ensure (if we are not a child of ``) that we use synchronous route (not deferred) - const injectedRoute = inject(PageRouteSymbol) - const route = injectedRoute === useRoute() ? useVueRouterRoute() : injectedRoute - - const layout = computed(() => unref(props.name) ?? route.meta.layout as string ?? 'default') - - const layoutRef = ref() - context.expose({ layoutRef }) - - const done = nuxtApp.deferHydration() - - return () => { - const hasLayout = layout.value && layout.value in layouts - if (process.dev && layout.value && !hasLayout && layout.value !== 'default') { - console.warn(`Invalid layout \`${layout.value}\` selected.`) - } - - const transitionProps = route.meta.layoutTransition ?? defaultLayoutTransition - - // We avoid rendering layout transition if there is no layout to render - return _wrapIf(Transition, hasLayout && transitionProps, { - default: () => h(Suspense, { suspensible: true, onResolve: () => { nextTick(done) } }, { - default: () => h( - // @ts-expect-error seems to be an issue in vue types - LayoutProvider, - { - layoutProps: mergeProps(context.attrs, { ref: layoutRef }), - key: layout.value, - name: layout.value, - shouldProvide: !props.name, - hasTransition: !!transitionProps - }, context.slots) - }) - }).default() - } - } -}) - -const LayoutProvider = defineComponent({ - name: 'NuxtLayoutProvider', - inheritAttrs: false, - props: { - name: { - type: [String, Boolean] - }, - layoutProps: { - type: Object - }, - hasTransition: { - type: Boolean - }, - shouldProvide: { - type: Boolean - } - }, - setup (props, context) { - // Prevent reactivity when the page will be rerendered in a different suspense fork - // eslint-disable-next-line vue/no-setup-props-destructure - const name = props.name - if (props.shouldProvide) { - provide(LayoutMetaSymbol, { - isCurrent: (route: RouteLocationNormalizedLoaded) => name === (route.meta.layout ?? 'default') - }) - } - - let vnode: VNode | undefined - if (process.dev && process.client) { - onMounted(() => { - nextTick(() => { - if (['#comment', '#text'].includes(vnode?.el?.nodeName)) { - if (name) { - console.warn(`[nuxt] \`${name}\` layout does not have a single root node and will cause errors when navigating between routes.`) - } else { - console.warn('[nuxt] `` needs to be passed a single root node in its default slot.') - } - } - }) - }) - } - - return () => { - if (!name || (typeof name === 'string' && !(name in layouts))) { - if (process.dev && process.client && props.hasTransition) { - vnode = context.slots.default?.() as VNode | undefined - return vnode - } - return context.slots.default?.() - } - - if (process.dev && process.client && props.hasTransition) { - vnode = h( - // @ts-expect-error seems to be an issue in vue types - LayoutLoader, - { key: name, layoutProps: props.layoutProps, name }, - context.slots - ) - - return vnode - } - - return h( - // @ts-expect-error seems to be an issue in vue types - LayoutLoader, - { key: name, layoutProps: props.layoutProps, name }, - context.slots - ) - } - } -}) +// TODO: remove in 4.x +export { default } from './nuxt-layout' diff --git a/packages/nuxt/src/app/components/nuxt-layout.ts b/packages/nuxt/src/app/components/nuxt-layout.ts new file mode 100644 index 0000000000..b18cc84a38 --- /dev/null +++ b/packages/nuxt/src/app/components/nuxt-layout.ts @@ -0,0 +1,153 @@ +import type { DefineComponent, MaybeRef, VNode } from 'vue' +import { Suspense, Transition, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, provide, ref, unref } from 'vue' +import type { RouteLocationNormalizedLoaded } from 'vue-router' +import { _wrapIf } from './utils' +import { LayoutMetaSymbol, PageRouteSymbol } from './injections' +import type { PageMeta } from '#app' + +import { useRoute } from '#app/composables/router' +import { useNuxtApp } from '#app/nuxt' +// @ts-expect-error virtual file +import { useRoute as useVueRouterRoute } from '#build/pages' +// @ts-expect-error virtual file +import layouts from '#build/layouts' +// @ts-expect-error virtual file +import { appLayoutTransition as defaultLayoutTransition } from '#build/nuxt.config.mjs' + +// TODO: revert back to defineAsyncComponent when https://github.com/vuejs/core/issues/6638 is resolved +const LayoutLoader = defineComponent({ + name: 'LayoutLoader', + inheritAttrs: false, + props: { + name: String, + layoutProps: Object + }, + async setup (props, context) { + const LayoutComponent = await layouts[props.name]().then((r: any) => r.default || r) + + return () => h(LayoutComponent, props.layoutProps, context.slots) + } +}) + +export default defineComponent({ + name: 'NuxtLayout', + inheritAttrs: false, + props: { + name: { + type: [String, Boolean, Object] as unknown as () => unknown extends PageMeta['layout'] ? MaybeRef : PageMeta['layout'], + default: null + } + }, + setup (props, context) { + const nuxtApp = useNuxtApp() + // Need to ensure (if we are not a child of ``) that we use synchronous route (not deferred) + const injectedRoute = inject(PageRouteSymbol) + const route = injectedRoute === useRoute() ? useVueRouterRoute() : injectedRoute + + const layout = computed(() => unref(props.name) ?? route.meta.layout as string ?? 'default') + + const layoutRef = ref() + context.expose({ layoutRef }) + + const done = nuxtApp.deferHydration() + + return () => { + const hasLayout = layout.value && layout.value in layouts + if (process.dev && layout.value && !hasLayout && layout.value !== 'default') { + console.warn(`Invalid layout \`${layout.value}\` selected.`) + } + + const transitionProps = route.meta.layoutTransition ?? defaultLayoutTransition + + // We avoid rendering layout transition if there is no layout to render + return _wrapIf(Transition, hasLayout && transitionProps, { + default: () => h(Suspense, { suspensible: true, onResolve: () => { nextTick(done) } }, { + default: () => h( + // @ts-expect-error seems to be an issue in vue types + LayoutProvider, + { + layoutProps: mergeProps(context.attrs, { ref: layoutRef }), + key: layout.value, + name: layout.value, + shouldProvide: !props.name, + hasTransition: !!transitionProps + }, context.slots) + }) + }).default() + } + } +}) as unknown as DefineComponent<{ + name: unknown extends PageMeta['layout'] ? MaybeRef : PageMeta['layout'] +}> + +const LayoutProvider = defineComponent({ + name: 'NuxtLayoutProvider', + inheritAttrs: false, + props: { + name: { + type: [String, Boolean] + }, + layoutProps: { + type: Object + }, + hasTransition: { + type: Boolean + }, + shouldProvide: { + type: Boolean + } + }, + setup (props, context) { + // Prevent reactivity when the page will be rerendered in a different suspense fork + // eslint-disable-next-line vue/no-setup-props-destructure + const name = props.name + if (props.shouldProvide) { + provide(LayoutMetaSymbol, { + isCurrent: (route: RouteLocationNormalizedLoaded) => name === (route.meta.layout ?? 'default') + }) + } + + let vnode: VNode | undefined + if (process.dev && process.client) { + onMounted(() => { + nextTick(() => { + if (['#comment', '#text'].includes(vnode?.el?.nodeName)) { + if (name) { + console.warn(`[nuxt] \`${name}\` layout does not have a single root node and will cause errors when navigating between routes.`) + } else { + console.warn('[nuxt] `` needs to be passed a single root node in its default slot.') + } + } + }) + }) + } + + return () => { + if (!name || (typeof name === 'string' && !(name in layouts))) { + if (process.dev && process.client && props.hasTransition) { + vnode = context.slots.default?.() as VNode | undefined + return vnode + } + return context.slots.default?.() + } + + if (process.dev && process.client && props.hasTransition) { + vnode = h( + // @ts-expect-error seems to be an issue in vue types + LayoutLoader, + { key: name, layoutProps: props.layoutProps, name }, + context.slots + ) + + return vnode + } + + return h( + // @ts-expect-error seems to be an issue in vue types + LayoutLoader, + { key: name, layoutProps: props.layoutProps, name }, + context.slots + ) + } + } +}) diff --git a/packages/nuxt/src/app/composables/router.ts b/packages/nuxt/src/app/composables/router.ts index 905ffe63a8..126381ce49 100644 --- a/packages/nuxt/src/app/composables/router.ts +++ b/packages/nuxt/src/app/composables/router.ts @@ -221,7 +221,7 @@ export const abortNavigation = (err?: string | Partial) => { throw err } -export const setPageLayout = (layout: string) => { +export const setPageLayout = (layout: unknown extends PageMeta['layout'] ? string : PageMeta['layout']) => { if (process.server) { if (process.dev && getCurrentInstance() && useState('_layout').value !== layout) { console.warn('[warn] [nuxt] `setPageLayout` should not be called to change the layout on the server within a component as this will cause hydration errors.') diff --git a/packages/nuxt/src/core/nuxt.ts b/packages/nuxt/src/core/nuxt.ts index 2588a1949c..110b60655b 100644 --- a/packages/nuxt/src/core/nuxt.ts +++ b/packages/nuxt/src/core/nuxt.ts @@ -200,7 +200,7 @@ async function initNuxt (nuxt: Nuxt) { addComponent({ name: 'NuxtLayout', priority: 10, // built-in that we do not expect the user to override - filePath: resolve(nuxt.options.appDir, 'components/layout') + filePath: resolve(nuxt.options.appDir, 'components/nuxt-layout') }) // Add diff --git a/packages/nuxt/src/pages/module.ts b/packages/nuxt/src/pages/module.ts index 6cd94aded2..dd56da61fb 100644 --- a/packages/nuxt/src/pages/module.ts +++ b/packages/nuxt/src/pages/module.ts @@ -353,11 +353,11 @@ export default defineNuxtModule({ getContents: ({ app }: { app: NuxtApp }) => { const composablesFile = resolve(runtimeDir, 'composables') return [ - 'import { ComputedRef, Ref } from \'vue\'', + 'import { ComputedRef, MaybeRef } from \'vue\'', `export type LayoutKey = ${Object.keys(app.layouts).map(name => genString(name)).join(' | ') || 'string'}`, `declare module ${genString(composablesFile)} {`, ' interface PageMeta {', - ' layout?: false | LayoutKey | Ref | ComputedRef', + ' layout?: MaybeRef | ComputedRef', ' }', '}' ].join('\n')