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

Commit

Permalink
feat(nuxt): add setLayout utility (#6826)
Browse files Browse the repository at this point in the history
  • Loading branch information
HomWang committed Aug 26, 2022
1 parent 56c7b61 commit cad564c
Show file tree
Hide file tree
Showing 12 changed files with 97 additions and 4 deletions.
16 changes: 16 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,16 @@
# `setLayout`

`setLayout` allows you to dynamically change the layout of a page.

`setLayout` relies on access to the Nuxt context and can only be called within component setup functions, plugins, and route middleware.

```js
export default defineNuxtRouteMiddleware(to => {
// Set the layout on the route you are navigating _to_
setLayout('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. (In other words, within a plugin or route middleware.) Otherwise there will be 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(() => {
setLayout('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="setLayout('default')">
Change to default layout
</NButton>
<NButton @click="setLayout('custom')">
Change to custom layout
</NButton>
<NButton @click="setLayout('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, setLayout, navigateTo, useRoute, useActiveRoute, useRouter } from './router'
export type { AddRouteMiddlewareOptions, RouteMiddleware } from './router'
export { preloadComponents, prefetchComponents } from './preload'
16 changes: 15 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,17 @@ export const abortNavigation = (err?: Error | string) => {
}
return false
}

export const setLayout = (layout: string) => {
if (process.server) {
useState('_layout').value = layout
}
if (isProcessingMiddleware()) {
const unsubscribe = useRouter().beforeResolve((to) => {
to.meta.layout = layout
unsubscribe()
})
} else {
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 @@ -25,6 +25,7 @@ const appPreset = defineUnimportPreset({
'useAsyncData',
'useLazyAsyncData',
'refreshNuxtData',
'setLayout',
'defineNuxtComponent',
'useNuxtApp',
'defineNuxtPlugin',
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 @@ -265,6 +265,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))
setLayout('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>

0 comments on commit cad564c

Please sign in to comment.