feat(pages): page layouts (#276)

This commit is contained in:
Daniel Roe 2021-06-30 17:32:22 +01:00 committed by GitHub
parent 8faf069778
commit b1948b1921
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 274 additions and 14 deletions

View File

@ -2,8 +2,7 @@
The new `app.vue` file, is the main component in your Nuxt3 applications.
If you would like to customize default layout of website, create `app.vue` in your project directory. Nuxt will automatically detect it and load it as the parent of all other pages within your application.
If you would like to customize the global website entrypoint, you can create `app.vue` in your project directory. Nuxt will automatically detect it and load it as the parent of all other pages within your application. This will be primarily useful for adding code that should be run on every single page of your site (see [page layouts](/app/pages#layouts) creating dynamic layouts).
**Note:** Don't forget to use `<NuxtPage>` component somewhere inside `app.vue`.

View File

@ -2,9 +2,11 @@
Nuxt will automatically integrate [Vue Router](https://next.router.vuejs.org/) and map `pages/` directory into the routes of your application.
## Dynamic Routes
If you place anything within square brackets, it will be turned into a [dynamic route](https://next.router.vuejs.org/guide/essentials/dynamic-matching.html) parameter. You can mix and match multiple parameters and even non-dynamic text within a file name or directory.
## Example
### Example
```bash
-| pages/
@ -21,3 +23,59 @@ Given the example above, you can access group/userid within your component via t
{{ $route.params.id }}
</template>
```
## Layouts
Nuxt provides a customizable layouts framework you can use throughout your application, ideal for extracting common UI or code patterns into reusable layout components.
Page layouts are placed in the `layouts/` directory and will be automatically loaded via asynchronous import when used. If you create a `layouts/default.vue` this will be used for all pages in your app. Other layouts are used by setting a `layout` property as part of your component options.
If you have only single layout for application, you can alternatively use (app entry)[/app].
### Example: a custom layout
```bash
-| layouts/
---| custom.vue
```
In your layout files, you'll need to use `<slot />` to define where the page content of your layout will be loaded. For example:
```vue
<template>
<div>
Some shared layout content:
<slot />
</div>
</template>
```
Given the example above, you can use a custom layout like this:
```vue
<script>
export default {
layout: "custom",
};
</script>
```
### Example: using with slots
You can also take full control (for example, with slots) by using the `<NuxtLayout>` component (which is globally available throughout your application) and set `layout: false` in your component options.
```vue
<template>
<NuxtLayout name="custom">
<template #header> Some header template content. </template>
The rest of the page
</NuxtLayout>
</template>
<script>
export default {
layout: false,
};
</script>
```

View File

@ -0,0 +1,18 @@
<template>
<div>
<strong>Custom layout</strong>
Header slot:
<slot name="header">
Default slot content
</slot>
<slot />
</div>
</template>
<script>
export default {
created () {
console.log('layout created')
}
}
</script>

View File

@ -0,0 +1,6 @@
<template>
<div>
Default layout
<slot />
</div>
</template>

View File

@ -0,0 +1,6 @@
<template>
<div>
Other
<slot />
</div>
</template>

View File

@ -0,0 +1,12 @@
{
"name": "example-with-layouts",
"private": true,
"devDependencies": {
"nuxt3": "latest"
},
"scripts": {
"dev": "nu dev",
"build": "nu build",
"start": "node .output/server"
}
}

View File

@ -0,0 +1,20 @@
<template>
<div>
Welcome to Nuxt Layouts 👋
<NuxtLink to="/manual">
Manual layout
</NuxtLink>
<NuxtLink to="/same">
Same layout
</NuxtLink>
</div>
</template>
<script>
import { defineNuxtComponent } from '@nuxt/app'
export default defineNuxtComponent({
layout: 'custom'
})
</script>

View File

@ -0,0 +1,27 @@
<template>
<div>
Manual control
<NuxtLayout :name="layout">
Default slot
<button @click="layout ? layout = null : layout = 'custom'">
Switch layout
</button>
<template #header>
Header slot
</template>
</NuxtLayout>
<NuxtLink to="/">
Back to home
</NuxtLink>
</div>
</template>
<script>
export default {
layout: false,
data: () => ({
layout: 'custom'
})
}
</script>

View File

@ -0,0 +1,14 @@
<template>
<div>
Same layout as home
<NuxtLink to="/">
Back to home
</NuxtLink>
</div>
</template>
<script>
export default {
layout: 'custom'
}
</script>

View File

@ -14,6 +14,7 @@
"dependencies": {
"@nuxt/kit": "^0.6.4",
"globby": "^11.0.4",
"scule": "^0.2.1",
"ufo": "^0.7.5",
"upath": "^2.0.1",
"vue-router": "^4.0.10"

View File

@ -1,7 +1,7 @@
import { existsSync } from 'fs'
import { defineNuxtModule } from '@nuxt/kit'
import { resolve } from 'upath'
import { resolvePagesRoutes } from './utils'
import { resolveLayouts, resolvePagesRoutes } from './utils'
export default defineNuxtModule({
name: 'router',
@ -12,7 +12,8 @@ export default defineNuxtModule({
nuxt.hook('builder:watch', async (event, path) => {
// Regenerate templates when adding or removing pages (plugin and routes)
if (event !== 'change' && path.startsWith('pages/')) {
const pathPattern = new RegExp(`^(${nuxt.options.dir.pages}|${nuxt.options.dir.layouts})/`)
if (event !== 'change' && path.match(pathPattern)) {
await nuxt.callHook('builder:generateApp')
}
})
@ -54,6 +55,22 @@ export default defineNuxtModule({
return `export default ${JSON.stringify(serializedRoutes, null, 2).replace(/"{(.+)}"/g, '$1')}`
}
})
const layouts = await resolveLayouts(nuxt)
// Add routes.js
app.templates.push({
path: 'layouts.js',
compile: () => {
const layoutsObject = Object.fromEntries(layouts.map(({ name, file }) => {
return [name, `{defineAsyncComponent({ suspensible: false, loader: () => import('${file}') })}`]
}))
return [
'import { defineAsyncComponent } from \'vue\'',
`export default ${JSON.stringify(layoutsObject, null, 2).replace(/"{(.+)}"/g, '$1')}
`].join('\n')
}
})
})
}
})

View File

@ -0,0 +1,24 @@
import { defineComponent, h } from 'vue'
import layouts from '#build/layouts'
export default defineComponent({
props: {
name: {
type: [String, Boolean],
default: 'default'
}
},
setup (props, context) {
return () => {
const layout = props.name
if (!layouts[layout]) {
if (process.dev && layout && layout !== 'default') {
console.warn(`Invalid layout \`${layout}\` selected.`)
}
return context.slots.default()
}
return h(layouts[layout], props, context.slots)
}
}
})

View File

@ -1,17 +1,53 @@
<template>
<RouterView v-slot="{ Component }">
<NuxtLayout :name="layout || updatedComponentLayout || Component.type.layout">
<transition name="page" mode="out-in">
<!-- <keep-alive> -->
<Suspense @pending="$nuxt.callHook('page:start')" @resolve="$nuxt.callHook('page:finish')">
<Suspense @pending="() => onSuspensePending(Component)" @resolve="() => onSuspenseResolved(Component)">
<component :is="Component" :key="$route.path" />
</Suspense>
<!-- <keep-alive -->
</transition>
</NuxtLayout>
</RouterView>
</template>
<script>
import { getCurrentInstance, ref } from 'vue'
import NuxtLayout from './layout'
export default {
name: 'NuxtPage'
name: 'NuxtPage',
components: { NuxtLayout },
props: {
layout: {
type: String,
default: null
}
},
setup () {
// Disable HMR reactivity in production
const updatedComponentLayout = process.dev ? ref(null) : null
const { $nuxt } = getCurrentInstance().proxy
function onSuspensePending (Component) {
if (process.dev) {
updatedComponentLayout.value = Component.type.layout || null
}
return $nuxt.callHook('page:start', Component)
}
function onSuspenseResolved (Component) {
return $nuxt.callHook('page:finish', Component)
}
return {
updatedComponentLayout,
onSuspensePending,
onSuspenseResolved
}
}
}
</script>

View File

@ -8,6 +8,7 @@ import {
// @ts-ignore
import { defineNuxtPlugin } from '@nuxt/app'
import NuxtPage from './page.vue'
import NuxtLayout from './layout'
// @ts-ignore
import routes from '#build/routes'
@ -15,6 +16,7 @@ export default defineNuxtPlugin((nuxt) => {
const { app } = nuxt
app.component('NuxtPage', NuxtPage)
app.component('NuxtLayout', NuxtLayout)
app.component('NuxtLink', RouterLink)
const routerHistory = process.client

View File

@ -1,6 +1,7 @@
import { extname, relative, resolve } from 'upath'
import { basename, extname, relative, resolve } from 'upath'
import { encodePath } from 'ufo'
import { Nuxt, resolveFiles } from '@nuxt/kit'
import { kebabCase } from 'scule'
export interface NuxtRoute {
name?: string
@ -215,3 +216,13 @@ function prepareRoutes (routes: NuxtRoute[], parent?: NuxtRoute) {
return routes
}
export async function resolveLayouts (nuxt: Nuxt) {
const layoutDir = resolve(nuxt.options.srcDir, nuxt.options.dir.layouts)
const files = await resolveFiles(layoutDir, `*{${nuxt.options.extensions.join(',')}}`)
return files.map((file) => {
const name = kebabCase(basename(file).replace(extname(file), '')).replace(/["']/g, '')
return { name, file }
})
}

View File

@ -1782,6 +1782,7 @@ __metadata:
dependencies:
"@nuxt/kit": ^0.6.4
globby: ^11.0.4
scule: ^0.2.1
ufo: ^0.7.5
unbuild: ^0.3.1
upath: ^2.0.1
@ -6007,6 +6008,14 @@ __metadata:
languageName: unknown
linkType: soft
"example-with-layouts@workspace:examples/with-layouts":
version: 0.0.0-use.local
resolution: "example-with-layouts@workspace:examples/with-layouts"
dependencies:
nuxt3: latest
languageName: unknown
linkType: soft
"example-with-vite@workspace:examples/with-vite":
version: 0.0.0-use.local
resolution: "example-with-vite@workspace:examples/with-vite"