diff --git a/docs/content/2.app/1.app.md b/docs/content/2.app/1.app.md index d7c0e6faf0..fd1c9a1274 100644 --- a/docs/content/2.app/1.app.md +++ b/docs/content/2.app/1.app.md @@ -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 `` component somewhere inside `app.vue`. diff --git a/docs/content/2.app/2.pages.md b/docs/content/2.app/2.pages.md index 25ccf48ace..42f4b6b3b6 100644 --- a/docs/content/2.app/2.pages.md +++ b/docs/content/2.app/2.pages.md @@ -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 }} ``` + +## 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 `` to define where the page content of your layout will be loaded. For example: + +```vue + +``` + +Given the example above, you can use a custom layout like this: + +```vue + +``` + +### Example: using with slots + +You can also take full control (for example, with slots) by using the `` component (which is globally available throughout your application) and set `layout: false` in your component options. + +```vue + + + +``` diff --git a/examples/with-layouts/layouts/custom.vue b/examples/with-layouts/layouts/custom.vue new file mode 100644 index 0000000000..132f626bfa --- /dev/null +++ b/examples/with-layouts/layouts/custom.vue @@ -0,0 +1,18 @@ + + + diff --git a/examples/with-layouts/layouts/default.vue b/examples/with-layouts/layouts/default.vue new file mode 100644 index 0000000000..6d83fd9b55 --- /dev/null +++ b/examples/with-layouts/layouts/default.vue @@ -0,0 +1,6 @@ + diff --git a/examples/with-layouts/layouts/other.vue b/examples/with-layouts/layouts/other.vue new file mode 100644 index 0000000000..e38b23dba7 --- /dev/null +++ b/examples/with-layouts/layouts/other.vue @@ -0,0 +1,6 @@ + diff --git a/examples/with-layouts/package.json b/examples/with-layouts/package.json new file mode 100644 index 0000000000..80abdfbcc0 --- /dev/null +++ b/examples/with-layouts/package.json @@ -0,0 +1,12 @@ +{ + "name": "example-with-layouts", + "private": true, + "devDependencies": { + "nuxt3": "latest" + }, + "scripts": { + "dev": "nu dev", + "build": "nu build", + "start": "node .output/server" + } +} diff --git a/examples/with-layouts/pages/index.vue b/examples/with-layouts/pages/index.vue new file mode 100644 index 0000000000..debfa4df5c --- /dev/null +++ b/examples/with-layouts/pages/index.vue @@ -0,0 +1,20 @@ + + + diff --git a/examples/with-layouts/pages/manual.vue b/examples/with-layouts/pages/manual.vue new file mode 100644 index 0000000000..bf30c099d2 --- /dev/null +++ b/examples/with-layouts/pages/manual.vue @@ -0,0 +1,27 @@ + + + diff --git a/examples/with-layouts/pages/same.vue b/examples/with-layouts/pages/same.vue new file mode 100644 index 0000000000..c5e06d3e1f --- /dev/null +++ b/examples/with-layouts/pages/same.vue @@ -0,0 +1,14 @@ + + + diff --git a/packages/pages/package.json b/packages/pages/package.json index 93497f55de..24d35c2ee9 100644 --- a/packages/pages/package.json +++ b/packages/pages/package.json @@ -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" diff --git a/packages/pages/src/module.ts b/packages/pages/src/module.ts index 1434a4dea2..153f33380b 100644 --- a/packages/pages/src/module.ts +++ b/packages/pages/src/module.ts @@ -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') + } + }) }) } }) diff --git a/packages/pages/src/runtime/layout.ts b/packages/pages/src/runtime/layout.ts new file mode 100644 index 0000000000..1de0e9d87d --- /dev/null +++ b/packages/pages/src/runtime/layout.ts @@ -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) + } + } +}) diff --git a/packages/pages/src/runtime/page.vue b/packages/pages/src/runtime/page.vue index f11ab56b32..8f34d4400d 100644 --- a/packages/pages/src/runtime/page.vue +++ b/packages/pages/src/runtime/page.vue @@ -1,17 +1,53 @@ diff --git a/packages/pages/src/runtime/router.ts b/packages/pages/src/runtime/router.ts index 96e91c506e..396a6678a2 100644 --- a/packages/pages/src/runtime/router.ts +++ b/packages/pages/src/runtime/router.ts @@ -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 diff --git a/packages/pages/src/utils.ts b/packages/pages/src/utils.ts index f33d9a3482..0bf689e2c5 100644 --- a/packages/pages/src/utils.ts +++ b/packages/pages/src/utils.ts @@ -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 } + }) +} diff --git a/yarn.lock b/yarn.lock index 90f2de155b..8ff77374e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"