feat(nuxt): add setPageLayout utility (#6826) (#7075)

Co-authored-by: HomWang <516310460@qq.com>
This commit is contained in:
Daniel Roe 2022-08-31 09:02:48 +01:00 committed by GitHub
parent c92b6a01bb
commit b90d286631
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 99 additions and 6 deletions

View File

@ -121,9 +121,8 @@ You can also use a ref or computed property for your layout.
</template> </template>
<script setup> <script setup>
const route = useRoute()
function enableCustomLayout () { function enableCustomLayout () {
route.meta.layout = "custom" setPageLayout('custom')
} }
definePageMeta({ definePageMeta({
layout: false, layout: false,

View File

@ -0,0 +1,14 @@
# `setPageLayout`
`setPageLayout` allows you to dynamically change the layout of a page. It relies on access to the Nuxt context and can only be called within components' setup functions, plugins, and route middleware.
```ts
export default defineNuxtRouteMiddleware((to) => {
// Set the layout on the route you are navigating _to_
setPageLayout('other')
})
```
::alert{icon=👉}
If you choose to set the layout dynamically on the server side, you _must_ do so before the layout is rendered by Vue (that is, within a plugin or route middleware) to avoid a hydration mismatch.
::

View File

@ -0,0 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default defineNuxtRouteMiddleware(() => {
setPageLayout('other')
})

View File

@ -11,6 +11,18 @@
<NuxtLink to="/dynamic"> <NuxtLink to="/dynamic">
Dynamic layout Dynamic layout
</NuxtLink> </NuxtLink>
<NuxtLink to="/other">
Other layout
</NuxtLink>
<NButton @click="setPageLayout('default')">
Change to default layout
</NButton>
<NButton @click="setPageLayout('custom')">
Change to custom layout
</NButton>
<NButton @click="setPageLayout('other')">
Change to other layout
</NButton>
</nav> </nav>
</template> </template>
</NuxtExampleLayout> </NuxtExampleLayout>

View File

@ -0,0 +1,13 @@
<script setup>
definePageMeta({
middleware: 'other'
})
</script>
<template>
<div>
<NuxtLink to="/">
Back to home
</NuxtLink>
</div>
</template>

View File

@ -10,6 +10,6 @@ export type { FetchResult, UseFetchOptions } from './fetch'
export { useCookie } from './cookie' export { useCookie } from './cookie'
export type { CookieOptions, CookieRef } from './cookie' export type { CookieOptions, CookieRef } from './cookie'
export { useRequestHeaders, useRequestEvent, setResponseStatus } from './ssr' 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 type { AddRouteMiddlewareOptions, RouteMiddleware } from './router'
export { preloadComponents, prefetchComponents } from './preload' export { preloadComponents, prefetchComponents } from './preload'

View File

@ -2,7 +2,7 @@ import { getCurrentInstance, inject } from 'vue'
import type { Router, RouteLocationNormalizedLoaded, NavigationGuard, RouteLocationNormalized, RouteLocationRaw, NavigationFailure, RouteLocationPathRaw } from 'vue-router' import type { Router, RouteLocationNormalizedLoaded, NavigationGuard, RouteLocationNormalized, RouteLocationRaw, NavigationFailure, RouteLocationPathRaw } from 'vue-router'
import { sendRedirect } from 'h3' import { sendRedirect } from 'h3'
import { hasProtocol, joinURL, parseURL } from 'ufo' import { hasProtocol, joinURL, parseURL } from 'ufo'
import { useNuxtApp, useRuntimeConfig } from '#app' import { useNuxtApp, useRuntimeConfig, useState } from '#app'
export const useRouter = () => { export const useRouter = () => {
return useNuxtApp()?.$router as Router return useNuxtApp()?.$router as Router
@ -114,3 +114,20 @@ export const abortNavigation = (err?: Error | string) => {
} }
return false 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
}
}

View File

@ -1,7 +1,7 @@
import { reactive, h } from 'vue' import { reactive, h } from 'vue'
import { parseURL, stringifyParsedURL, parseQuery, stringifyQuery, withoutBase, isEqual, joinURL } from 'ufo' import { parseURL, stringifyParsedURL, parseQuery, stringifyQuery, withoutBase, isEqual, joinURL } from 'ufo'
import { createError } from 'h3' import { createError } from 'h3'
import { defineNuxtPlugin, clearError, navigateTo, showError, useRuntimeConfig } from '..' import { defineNuxtPlugin, clearError, navigateTo, showError, useRuntimeConfig, useState } from '..'
import { callWithNuxt } from '../nuxt' import { callWithNuxt } from '../nuxt'
// @ts-ignore // @ts-ignore
import { globalMiddleware } from '#build/middleware' import { globalMiddleware } from '#build/middleware'
@ -218,9 +218,13 @@ export default defineNuxtPlugin<{ route: Route, router: Router }>((nuxtApp) => {
named: {} named: {}
} }
const initialLayout = useState('_layout')
nuxtApp.hooks.hookOnce('app:created', async () => { nuxtApp.hooks.hookOnce('app:created', async () => {
router.beforeEach(async (to, from) => { router.beforeEach(async (to, from) => {
to.meta = reactive(to.meta || {}) to.meta = reactive(to.meta || {})
if (nuxtApp.isHydrating) {
to.meta.layout = initialLayout.value ?? to.meta.layout
}
nuxtApp._processingMiddleware = true nuxtApp._processingMiddleware = true
const middlewareEntries = new Set<RouteGuard>([...globalMiddleware, ...nuxtApp._middleware.global]) const middlewareEntries = new Set<RouteGuard>([...globalMiddleware, ...nuxtApp._middleware.global])

View File

@ -36,6 +36,7 @@ const appPreset = defineUnimportPreset({
'useRequestHeaders', 'useRequestHeaders',
'useRequestEvent', 'useRequestEvent',
'setResponseStatus', 'setResponseStatus',
'setPageLayout',
'useRouter', 'useRouter',
'useRoute', 'useRoute',
'useActiveRoute', 'useActiveRoute',

View File

@ -9,7 +9,7 @@ import {
import { createError } from 'h3' import { createError } from 'h3'
import { withoutBase, isEqual } from 'ufo' import { withoutBase, isEqual } from 'ufo'
import NuxtPage from './page' 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 // @ts-ignore
import routes from '#build/routes' import routes from '#build/routes'
// @ts-ignore // @ts-ignore
@ -114,8 +114,12 @@ export default defineNuxtPlugin(async (nuxtApp) => {
callWithNuxt(nuxtApp, showError, [error]) callWithNuxt(nuxtApp, showError, [error])
} }
const initialLayout = useState('_layout')
router.beforeEach(async (to, from) => { router.beforeEach(async (to, from) => {
to.meta = reactive(to.meta) to.meta = reactive(to.meta)
if (nuxtApp.isHydrating) {
to.meta.layout = initialLayout.value ?? to.meta.layout
}
nuxtApp._processingMiddleware = true nuxtApp._processingMiddleware = true
type MiddlewareDef = string | NavigationGuard type MiddlewareDef = string | NavigationGuard

View File

@ -273,6 +273,16 @@ describe('layouts', () => {
expect(html).toContain('with-layout.vue') expect(html).toContain('with-layout.vue')
expect(html).toContain('Custom Layout:') 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', () => { describe('reactivity transform', () => {

View File

@ -0,0 +1,4 @@
export default defineNuxtRouteMiddleware(async () => {
await new Promise(resolve => setTimeout(resolve, 10))
setPageLayout('custom')
})

View File

@ -0,0 +1,11 @@
<script setup>
definePageMeta({
middleware: 'sets-layout'
})
</script>
<template>
<div>
<div>with-dynamic-layout.vue</div>
</div>
</template>