diff --git a/docs/content/2.guide/3.directory-structure/7.layouts.md b/docs/content/2.guide/3.directory-structure/7.layouts.md index 9a1f93acc8c..86264e0bf01 100644 --- a/docs/content/2.guide/3.directory-structure/7.layouts.md +++ b/docs/content/2.guide/3.directory-structure/7.layouts.md @@ -121,9 +121,8 @@ You can also use a ref or computed property for your layout. + + diff --git a/packages/nuxt/src/app/composables/index.ts b/packages/nuxt/src/app/composables/index.ts index b3a31a9f43d..2fd1c252c8a 100644 --- a/packages/nuxt/src/app/composables/index.ts +++ b/packages/nuxt/src/app/composables/index.ts @@ -10,6 +10,6 @@ export type { FetchResult, UseFetchOptions } from './fetch' export { useCookie } from './cookie' export type { CookieOptions, CookieRef } from './cookie' export { useRequestHeaders, useRequestEvent, setResponseStatus } from './ssr' -export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, navigateTo, useRoute, useActiveRoute, useRouter } from './router' +export { abortNavigation, addRouteMiddleware, defineNuxtRouteMiddleware, setPageLayout, navigateTo, useRoute, useActiveRoute, useRouter } from './router' export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router' export { preloadComponents, prefetchComponents } from './preload' diff --git a/packages/nuxt/src/app/composables/router.ts b/packages/nuxt/src/app/composables/router.ts index 188d8c0bca9..eb964e4dacf 100644 --- a/packages/nuxt/src/app/composables/router.ts +++ b/packages/nuxt/src/app/composables/router.ts @@ -2,7 +2,7 @@ import { getCurrentInstance, inject } from 'vue' import type { Router, RouteLocationNormalizedLoaded, NavigationGuard, RouteLocationNormalized, RouteLocationRaw, NavigationFailure, RouteLocationPathRaw } from 'vue-router' import { sendRedirect } from 'h3' import { hasProtocol, joinURL, parseURL } from 'ufo' -import { useNuxtApp, useRuntimeConfig } from '#app' +import { useNuxtApp, useRuntimeConfig, useState } from '#app' export const useRouter = () => { return useNuxtApp()?.$router as Router @@ -114,3 +114,20 @@ export const abortNavigation = (err?: Error | string) => { } return false } + +export const setPageLayout = (layout: string) => { + if (process.server) { + useState('_layout').value = layout + } + const nuxtApp = useNuxtApp() + const inMiddleware = isProcessingMiddleware() + if (inMiddleware || process.server || nuxtApp.isHydrating) { + const unsubscribe = useRouter().beforeResolve((to) => { + to.meta.layout = layout + unsubscribe() + }) + } + if (!inMiddleware) { + useRoute().meta.layout = layout + } +} diff --git a/packages/nuxt/src/app/plugins/router.ts b/packages/nuxt/src/app/plugins/router.ts index 18954954549..64bf5d50a6c 100644 --- a/packages/nuxt/src/app/plugins/router.ts +++ b/packages/nuxt/src/app/plugins/router.ts @@ -1,7 +1,7 @@ import { reactive, h } from 'vue' import { parseURL, stringifyParsedURL, parseQuery, stringifyQuery, withoutBase, isEqual, joinURL } from 'ufo' import { createError } from 'h3' -import { defineNuxtPlugin, clearError, navigateTo, showError, useRuntimeConfig } from '..' +import { defineNuxtPlugin, clearError, navigateTo, showError, useRuntimeConfig, useState } from '..' import { callWithNuxt } from '../nuxt' // @ts-ignore import { globalMiddleware } from '#build/middleware' @@ -218,9 +218,13 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => { named: {} } + const initialLayout = useState('_layout') nuxtApp.hooks.hookOnce('app:created', async () => { router.beforeEach(async (to, from) => { to.meta = reactive(to.meta || {}) + if (nuxtApp.isHydrating) { + to.meta.layout = initialLayout.value ?? to.meta.layout + } nuxtApp._processingMiddleware = true const middlewareEntries = new Set([...globalMiddleware, ...nuxtApp._middleware.global]) diff --git a/packages/nuxt/src/imports/presets.ts b/packages/nuxt/src/imports/presets.ts index 6985af5b27e..b76ebe7a624 100644 --- a/packages/nuxt/src/imports/presets.ts +++ b/packages/nuxt/src/imports/presets.ts @@ -36,6 +36,7 @@ const appPreset = defineUnimportPreset({ 'useRequestHeaders', 'useRequestEvent', 'setResponseStatus', + 'setPageLayout', 'useRouter', 'useRoute', 'useActiveRoute', diff --git a/packages/nuxt/src/pages/runtime/router.ts b/packages/nuxt/src/pages/runtime/router.ts index cb12db1f574..32163890039 100644 --- a/packages/nuxt/src/pages/runtime/router.ts +++ b/packages/nuxt/src/pages/runtime/router.ts @@ -9,7 +9,7 @@ import { import { createError } from 'h3' import { withoutBase, isEqual } from 'ufo' import NuxtPage from './page' -import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig, showError, clearError, navigateTo, useError } from '#app' +import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig, showError, clearError, navigateTo, useError, useState } from '#app' // @ts-ignore import routes from '#build/routes' // @ts-ignore @@ -114,8 +114,12 @@ export default defineNuxtPlugin(async (nuxtApp) => { callWithNuxt(nuxtApp, showError, [error]) } + const initialLayout = useState('_layout') router.beforeEach(async (to, from) => { to.meta = reactive(to.meta) + if (nuxtApp.isHydrating) { + to.meta.layout = initialLayout.value ?? to.meta.layout + } nuxtApp._processingMiddleware = true type MiddlewareDef = string | NavigationGuard diff --git a/test/basic.test.ts b/test/basic.test.ts index 893f40ef7ce..497aa9c6425 100644 --- a/test/basic.test.ts +++ b/test/basic.test.ts @@ -273,6 +273,16 @@ describe('layouts', () => { expect(html).toContain('with-layout.vue') expect(html).toContain('Custom Layout:') }) + it('should work with a dynamically set layout', async () => { + const html = await $fetch('/with-dynamic-layout') + + // Snapshot + // expect(html).toMatchInlineSnapshot() + + expect(html).toContain('with-dynamic-layout') + expect(html).toContain('Custom Layout:') + await expectNoClientErrors('/with-dynamic-layout') + }) }) describe('reactivity transform', () => { diff --git a/test/fixtures/basic/middleware/sets-layout.ts b/test/fixtures/basic/middleware/sets-layout.ts new file mode 100644 index 00000000000..72985a38dcb --- /dev/null +++ b/test/fixtures/basic/middleware/sets-layout.ts @@ -0,0 +1,4 @@ +export default defineNuxtRouteMiddleware(async () => { + await new Promise(resolve => setTimeout(resolve, 10)) + setPageLayout('custom') +}) diff --git a/test/fixtures/basic/pages/with-dynamic-layout.vue b/test/fixtures/basic/pages/with-dynamic-layout.vue new file mode 100644 index 00000000000..6e2d5895489 --- /dev/null +++ b/test/fixtures/basic/pages/with-dynamic-layout.vue @@ -0,0 +1,11 @@ + + +