mirror of
https://github.com/nuxt/nuxt.git
synced 2025-02-16 13:48:13 +00:00
feat(nuxt3)!: enable using <NuxtLayout>
without pages integration (#3610)
This commit is contained in:
parent
5c69ba64a4
commit
7bf338da8c
@ -8,20 +8,25 @@ head.title: Layouts directory
|
||||
|
||||
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's options.
|
||||
Layouts are placed in the `layouts/` directory and will be automatically loaded via asynchronous import when used. Layouts are used by setting a `layout` property as part of your page metadata (if you are using the `~/pages` integration), or by using the `<NuxtLayout>` component.
|
||||
|
||||
If you only have a single layout in your application, you can alternatively use [app.vue](/docs/directory-structure/app).
|
||||
If you only have a single layout in your application, it is recommended to use [app.vue](/docs/directory-structure/app) instead.
|
||||
|
||||
## Example: a custom layout
|
||||
::alert{type=warning}
|
||||
Unlike other components, your layouts must have a single root element to allow Nuxt to apply transitions between layout changes.
|
||||
::
|
||||
|
||||
## Example: Enabling layouts with `app.vue`
|
||||
|
||||
```bash
|
||||
-| layouts/
|
||||
---| custom.vue
|
||||
-| app.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:
|
||||
In your layout files, you'll need to use `<slot />` to define where the content of your layout will be loaded. For example:
|
||||
|
||||
```vue
|
||||
```vue{}[layouts/custom.vue]
|
||||
<template>
|
||||
<div>
|
||||
Some shared layout content:
|
||||
@ -30,9 +35,28 @@ In your layout files, you'll need to use `<slot />` to define where the page con
|
||||
</template>
|
||||
```
|
||||
|
||||
Given the example above, you can use a custom layout like this:
|
||||
Here's how you might use that layout in `app.vue`:
|
||||
|
||||
```vue
|
||||
```vue{}[app.vue]
|
||||
<template>
|
||||
<NuxtLayout name="custom">
|
||||
Hello world!
|
||||
</NuxtLayout>
|
||||
</template>
|
||||
```
|
||||
|
||||
## Example: setting the layout with `~/pages`
|
||||
|
||||
```bash
|
||||
-| layouts/
|
||||
---| custom.vue
|
||||
-| pages/
|
||||
---| index.vue
|
||||
```
|
||||
|
||||
You can set your layout within your page components like this:
|
||||
|
||||
```vue{}[pages/index.vue]
|
||||
<script>
|
||||
// This will also work in `<script setup>`
|
||||
definePageMeta({
|
||||
@ -45,9 +69,9 @@ definePageMeta({
|
||||
Learn more about [defining page meta](/docs/directory-structure/pages#page-metadata).
|
||||
::
|
||||
|
||||
## Example: using with slots
|
||||
## Example: manual control with `~/pages`
|
||||
|
||||
You can also take full control (for example, with slots) by using the `<NuxtLayout>` component (which is globally available throughout your application) by setting `layout: false`.
|
||||
Even if you are using the `~/pages` integration, you can take full control by using the `<NuxtLayout>` component (which is globally available throughout your application), by setting `layout: false`.
|
||||
|
||||
```vue
|
||||
<template>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { defineComponent, isRef, Ref, Transition } from 'vue'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { wrapIf } from './utils'
|
||||
import { _wrapIf } from './utils'
|
||||
import { useRoute } from '#app'
|
||||
// @ts-ignore
|
||||
import layouts from '#build/layouts'
|
||||
|
||||
@ -25,8 +25,8 @@ export default defineComponent({
|
||||
}
|
||||
|
||||
// We avoid rendering layout transition if there is no layout to render
|
||||
return wrapIf(Transition, hasLayout && (route.meta.layoutTransition ?? defaultLayoutTransition),
|
||||
wrapIf(layouts[layout], hasLayout, context.slots)
|
||||
return _wrapIf(Transition, hasLayout && (route.meta.layoutTransition ?? defaultLayoutTransition),
|
||||
_wrapIf(layouts[layout], hasLayout, context.slots)
|
||||
).default()
|
||||
}
|
||||
}
|
17
packages/nuxt3/src/app/components/utils.ts
Normal file
17
packages/nuxt3/src/app/components/utils.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import { h } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
const Fragment = {
|
||||
setup (_props, { slots }) {
|
||||
return () => slots.default()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal utility
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
export const _wrapIf = (component: Component, props: any, slots: any) => {
|
||||
return { default: () => props ? h(component, props === true ? {} : props, slots) : h(Fragment, {}, slots) }
|
||||
}
|
@ -1,6 +1,7 @@
|
||||
import { promises as fsp } from 'fs'
|
||||
import { dirname, resolve } from 'pathe'
|
||||
import { dirname, resolve, basename, extname } from 'pathe'
|
||||
import defu from 'defu'
|
||||
import { kebabCase } from 'scule'
|
||||
import type { Nuxt, NuxtApp, NuxtPlugin } from '@nuxt/schema'
|
||||
import { findPath, resolveFiles, normalizePlugin, normalizeTemplate, compileTemplate, templateUtils, tryResolveModule } from '@nuxt/kit'
|
||||
|
||||
@ -70,6 +71,16 @@ export async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
|
||||
app.errorComponent = (await findPath(['~/error'])) || resolve(nuxt.options.appDir, 'components/nuxt-error-page.vue')
|
||||
}
|
||||
|
||||
// Resolve layouts
|
||||
app.layouts = {}
|
||||
for (const config of [nuxt.options, ...nuxt.options._extends.map(layer => layer.config)]) {
|
||||
const layoutFiles = await resolveFiles(config.srcDir, `${config.dir.layouts}/*{${config.extensions.join(',')}}`)
|
||||
for (const file of layoutFiles) {
|
||||
const name = getNameFromPath(file)
|
||||
app.layouts[name] = app.layouts[name] || { name, file }
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve plugins
|
||||
app.plugins = []
|
||||
for (const config of [...nuxt.options._extends.map(layer => layer.config), nuxt.options]) {
|
||||
@ -85,3 +96,7 @@ export async function resolveApp (nuxt: Nuxt, app: NuxtApp) {
|
||||
// Extend app
|
||||
await nuxt.callHook('app:resolve', app)
|
||||
}
|
||||
|
||||
function getNameFromPath (path: string) {
|
||||
return kebabCase(basename(path).replace(extname(path), '')).replace(/["']/g, '')
|
||||
}
|
||||
|
@ -79,6 +79,11 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
filePath: tryResolveModule('@nuxt/ui-templates/templates/welcome.vue')
|
||||
})
|
||||
|
||||
addComponent({
|
||||
name: 'NuxtLayout',
|
||||
filePath: resolve(nuxt.options.appDir, 'components/layout')
|
||||
})
|
||||
|
||||
// Add <ClientOnly>
|
||||
addComponent({
|
||||
name: 'ClientOnly',
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { templateUtils } from '@nuxt/kit'
|
||||
import type { Nuxt, NuxtApp } from '@nuxt/schema'
|
||||
import { genArrayFromRaw, genDynamicImport, genExport, genImport, genString } from 'knitwork'
|
||||
import type { Nuxt, NuxtApp, NuxtTemplate } from '@nuxt/schema'
|
||||
import { genArrayFromRaw, genDynamicImport, genExport, genImport, genObjectFromRawEntries, genString } from 'knitwork'
|
||||
|
||||
import { isAbsolute, join, relative } from 'pathe'
|
||||
import { resolveSchema, generateTypes } from 'untyped'
|
||||
@ -154,3 +154,17 @@ export const schemaTemplate = {
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
|
||||
// Add layouts template
|
||||
export const layoutTemplate: NuxtTemplate = {
|
||||
filename: 'layouts.mjs',
|
||||
getContents ({ app }) {
|
||||
const layoutsObject = genObjectFromRawEntries(Object.values(app.layouts).map(({ name, file }) => {
|
||||
return [name, `defineAsyncComponent({ suspensible: false, loader: ${genDynamicImport(file)} })`]
|
||||
}))
|
||||
return [
|
||||
'import { defineAsyncComponent } from \'vue\'',
|
||||
`export default ${layoutsObject}`
|
||||
].join('\n')
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,7 @@ import { resolve } from 'pathe'
|
||||
import { genDynamicImport, genString, genArrayFromRaw, genImport, genObjectFromRawEntries } from 'knitwork'
|
||||
import escapeRE from 'escape-string-regexp'
|
||||
import { distDir } from '../dirs'
|
||||
import { resolveLayouts, resolvePagesRoutes, normalizeRoutes, resolveMiddleware, getImportName } from './utils'
|
||||
import { resolvePagesRoutes, normalizeRoutes, resolveMiddleware, getImportName } from './utils'
|
||||
import { TransformMacroPlugin, TransformMacroPluginOptions } from './macros'
|
||||
|
||||
export default defineNuxtModule({
|
||||
@ -111,12 +111,11 @@ export default defineNuxtModule({
|
||||
|
||||
addTemplate({
|
||||
filename: 'types/layouts.d.ts',
|
||||
getContents: async () => {
|
||||
getContents: ({ app }) => {
|
||||
const composablesFile = resolve(runtimeDir, 'composables')
|
||||
const layouts = await resolveLayouts(nuxt)
|
||||
return [
|
||||
'import { ComputedRef, Ref } from \'vue\'',
|
||||
`export type LayoutKey = ${layouts.map(layout => genString(layout.name)).join(' | ') || 'string'}`,
|
||||
`export type LayoutKey = ${Object.keys(app.layouts).map(name => genString(name)).join(' | ') || 'string'}`,
|
||||
`declare module ${genString(composablesFile)} {`,
|
||||
' interface PageMeta {',
|
||||
' layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey>',
|
||||
@ -126,22 +125,7 @@ export default defineNuxtModule({
|
||||
}
|
||||
})
|
||||
|
||||
// Add layouts template
|
||||
addTemplate({
|
||||
filename: 'layouts.mjs',
|
||||
async getContents () {
|
||||
const layouts = await resolveLayouts(nuxt)
|
||||
const layoutsObject = genObjectFromRawEntries(layouts.map(({ name, file }) => {
|
||||
return [name, `defineAsyncComponent({ suspensible: false, loader: ${genDynamicImport(file)} })`]
|
||||
}))
|
||||
return [
|
||||
'import { defineAsyncComponent } from \'vue\'',
|
||||
`export default ${layoutsObject}`
|
||||
].join('\n')
|
||||
}
|
||||
})
|
||||
|
||||
// Add declarations for middleware and layout keys
|
||||
// Add declarations for middleware keys
|
||||
nuxt.hook('prepare:types', ({ references }) => {
|
||||
references.push({ path: resolve(nuxt.options.buildDir, 'types/middleware.d.ts') })
|
||||
references.push({ path: resolve(nuxt.options.buildDir, 'types/layouts.d.ts') })
|
||||
|
@ -1,8 +1,9 @@
|
||||
import { defineComponent, h, Suspense, Transition } from 'vue'
|
||||
import { RouteLocationNormalizedLoaded, RouterView } from 'vue-router'
|
||||
|
||||
import { generateRouteKey, RouterViewSlotProps, wrapIf, wrapInKeepAlive } from './utils'
|
||||
import { generateRouteKey, RouterViewSlotProps, wrapInKeepAlive } from './utils'
|
||||
import { useNuxtApp } from '#app'
|
||||
import { _wrapIf } from '#app/components/utils'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'NuxtPage',
|
||||
@ -18,7 +19,7 @@ export default defineComponent({
|
||||
return () => {
|
||||
return h(RouterView, {}, {
|
||||
default: (routeProps: RouterViewSlotProps) => routeProps.Component &&
|
||||
wrapIf(Transition, routeProps.route.meta.pageTransition ?? defaultPageTransition,
|
||||
_wrapIf(Transition, routeProps.route.meta.pageTransition ?? defaultPageTransition,
|
||||
wrapInKeepAlive(routeProps.route.meta.keepalive, h(Suspense, {
|
||||
onPending: () => nuxtApp.callHook('page:start', routeProps.Component),
|
||||
onResolve: () => nuxtApp.callHook('page:finish', routeProps.Component)
|
||||
|
@ -8,7 +8,6 @@ import {
|
||||
} from 'vue-router'
|
||||
import { createError } from 'h3'
|
||||
import NuxtPage from './page'
|
||||
import NuxtLayout from './layout'
|
||||
import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig, NuxtApp, throwError, clearError } from '#app'
|
||||
// @ts-ignore
|
||||
import routes from '#build/routes'
|
||||
@ -18,7 +17,6 @@ import { globalMiddleware, namedMiddleware } from '#build/middleware'
|
||||
declare module 'vue' {
|
||||
export interface GlobalComponents {
|
||||
NuxtPage: typeof NuxtPage
|
||||
NuxtLayout: typeof NuxtLayout
|
||||
NuxtLink: typeof RouterLink
|
||||
/** @deprecated */
|
||||
NuxtNestedPage: typeof NuxtPage
|
||||
@ -29,7 +27,6 @@ declare module 'vue' {
|
||||
|
||||
export default defineNuxtPlugin((nuxtApp) => {
|
||||
nuxtApp.vueApp.component('NuxtPage', NuxtPage)
|
||||
nuxtApp.vueApp.component('NuxtLayout', NuxtLayout)
|
||||
nuxtApp.vueApp.component('NuxtLink', RouterLink)
|
||||
// TODO: remove before release - present for backwards compatibility & intentionally undocumented
|
||||
nuxtApp.vueApp.component('NuxtNestedPage', NuxtPage)
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Component, KeepAlive, h } from 'vue'
|
||||
import { KeepAlive, h } from 'vue'
|
||||
import { RouterView, RouteLocationMatched, RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
type InstanceOf<T> = T extends new (...args: any[]) => infer R ? R : never
|
||||
@ -17,16 +17,6 @@ export const generateRouteKey = (override: string | ((route: RouteLocationNormal
|
||||
return typeof source === 'function' ? source(routeProps.route) : source
|
||||
}
|
||||
|
||||
const Fragment = {
|
||||
setup (_props, { slots }) {
|
||||
return () => slots.default()
|
||||
}
|
||||
}
|
||||
|
||||
export const wrapIf = (component: Component, props: any, slots: any) => {
|
||||
return { default: () => props ? h(component, props === true ? {} : props, slots) : h(Fragment, {}, slots) }
|
||||
}
|
||||
|
||||
export const wrapInKeepAlive = (props: any, children: any) => {
|
||||
return { default: () => process.client && props ? h(KeepAlive, props === true ? {} : props, children) : children }
|
||||
}
|
||||
|
@ -217,15 +217,6 @@ function prepareRoutes (routes: NuxtPage[], parent?: NuxtPage) {
|
||||
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(',')}}`)
|
||||
|
||||
const layouts = files.map(file => ({ name: getNameFromPath(file), file }))
|
||||
await nuxt.callHook('pages:layouts:extend', layouts)
|
||||
return layouts
|
||||
}
|
||||
|
||||
export function normalizeRoutes (routes: NuxtPage[], metaImports: Set<string> = new Set()): { imports: Set<string>, routes: string } {
|
||||
return {
|
||||
imports: metaImports,
|
||||
|
@ -70,7 +70,6 @@ export interface NuxtHooks {
|
||||
'builder:generateApp': () => HookResult
|
||||
'pages:extend': (pages: NuxtPage[]) => HookResult
|
||||
'pages:middleware:extend': (middleware: NuxtMiddleware[]) => HookResult
|
||||
'pages:layouts:extend': (layouts: NuxtLayout[]) => HookResult
|
||||
|
||||
// Auto imports
|
||||
'autoImports:sources': (presets: ImportPresetWithDeperection[]) => HookResult
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { Hookable } from 'hookable'
|
||||
import type { Ignore } from 'ignore'
|
||||
import type { NuxtHooks } from './hooks'
|
||||
import type { NuxtHooks, NuxtLayout } from './hooks'
|
||||
import type { NuxtOptions } from './config'
|
||||
|
||||
export interface Nuxt {
|
||||
@ -57,6 +57,7 @@ export interface NuxtApp {
|
||||
dir: string
|
||||
extensions: string[]
|
||||
plugins: NuxtPlugin[]
|
||||
layouts: Record<string, NuxtLayout>
|
||||
templates: NuxtTemplate[]
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user