Skip to content
This repository has been archived by the owner on Apr 6, 2023. It is now read-only.

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

Merged
merged 4 commits into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions docs/content/2.guide/3.directory-structure/7.layouts.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,9 +121,8 @@ You can also use a ref or computed property for your layout.
</template>

<script setup>
const route = useRoute()
function enableCustomLayout () {
route.meta.layout = "custom"
setPageLayout('custom')
}
definePageMeta({
layout: false,
Expand Down
14 changes: 14 additions & 0 deletions docs/content/3.api/1.composables/set-layout.md
Original file line number Diff line number Diff line change
@@ -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.
::
4 changes: 4 additions & 0 deletions examples/routing/layouts/middleware/other.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default defineNuxtRouteMiddleware(() => {
setPageLayout('other')
})
12 changes: 12 additions & 0 deletions examples/routing/layouts/pages/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,18 @@
<NuxtLink to="/dynamic">
Dynamic layout
</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>
</template>
</NuxtExampleLayout>
Expand Down
13 changes: 13 additions & 0 deletions examples/routing/layouts/pages/other.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<script setup>
definePageMeta({
middleware: 'other'
})
</script>

<template>
<div>
<NuxtLink to="/">
Back to home
</NuxtLink>
</div>
</template>
2 changes: 1 addition & 1 deletion packages/nuxt/src/app/composables/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'
19 changes: 18 additions & 1 deletion packages/nuxt/src/app/composables/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
}
6 changes: 5 additions & 1 deletion packages/nuxt/src/app/plugins/router.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<RouteGuard>([...globalMiddleware, ...nuxtApp._middleware.global])
Expand Down
1 change: 1 addition & 0 deletions packages/nuxt/src/imports/presets.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ const appPreset = defineUnimportPreset({
'useRequestHeaders',
'useRequestEvent',
'setResponseStatus',
'setPageLayout',
'useRouter',
'useRoute',
'useActiveRoute',
Expand Down
6 changes: 5 additions & 1 deletion packages/nuxt/src/pages/runtime/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions test/basic.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
4 changes: 4 additions & 0 deletions test/fixtures/basic/middleware/sets-layout.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export default defineNuxtRouteMiddleware(async () => {
await new Promise(resolve => setTimeout(resolve, 10))
setPageLayout('custom')
})
11 changes: 11 additions & 0 deletions test/fixtures/basic/pages/with-dynamic-layout.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<script setup>
definePageMeta({
middleware: 'sets-layout'
})
</script>

<template>
<div>
<div>with-dynamic-layout.vue</div>
</div>
</template>