feat(nuxt3)!: enable using <NuxtLayout> without pages integration (#3610)

This commit is contained in:
Daniel Roe 2022-03-14 10:47:24 +00:00 committed by GitHub
parent 5c69ba64a4
commit 7bf338da8c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 101 additions and 63 deletions

View File

@ -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>

View File

@ -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()
}
}

View 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) }
}

View File

@ -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, '')
}

View File

@ -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',

View File

@ -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')
}
}

View File

@ -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') })

View File

@ -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)

View File

@ -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)

View File

@ -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 }
}

View File

@ -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,

View File

@ -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

View File

@ -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[]
}