mirror of
https://github.com/nuxt/nuxt.git
synced 2025-02-16 21:58:19 +00:00
feat(nuxt): autocomplete layouts in setPageLayout
/<NuxtLayout>
(#22362)
This commit is contained in:
parent
38d2bb7b95
commit
0991e885fd
@ -1,150 +1,2 @@
|
|||||||
import type { Ref, VNode } from 'vue'
|
// TODO: remove in 4.x
|
||||||
import { Suspense, Transition, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, provide, ref, unref } from 'vue'
|
export { default } from './nuxt-layout'
|
||||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
|
||||||
import { _wrapIf } from './utils'
|
|
||||||
import { LayoutMetaSymbol, PageRouteSymbol } from './injections'
|
|
||||||
|
|
||||||
import { useRoute } from '#app/composables/router'
|
|
||||||
// @ts-expect-error virtual file
|
|
||||||
import { useRoute as useVueRouterRoute } from '#build/pages'
|
|
||||||
// @ts-expect-error virtual file
|
|
||||||
import layouts from '#build/layouts'
|
|
||||||
// @ts-expect-error virtual file
|
|
||||||
import { appLayoutTransition as defaultLayoutTransition } from '#build/nuxt.config.mjs'
|
|
||||||
import { useNuxtApp } from '#app'
|
|
||||||
|
|
||||||
// TODO: revert back to defineAsyncComponent when https://github.com/vuejs/core/issues/6638 is resolved
|
|
||||||
const LayoutLoader = defineComponent({
|
|
||||||
name: 'LayoutLoader',
|
|
||||||
inheritAttrs: false,
|
|
||||||
props: {
|
|
||||||
name: String,
|
|
||||||
layoutProps: Object
|
|
||||||
},
|
|
||||||
async setup (props, context) {
|
|
||||||
const LayoutComponent = await layouts[props.name]().then((r: any) => r.default || r)
|
|
||||||
|
|
||||||
return () => h(LayoutComponent, props.layoutProps, context.slots)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
export default defineComponent({
|
|
||||||
name: 'NuxtLayout',
|
|
||||||
inheritAttrs: false,
|
|
||||||
props: {
|
|
||||||
name: {
|
|
||||||
type: [String, Boolean, Object] as unknown as () => string | false | Ref<string | false>,
|
|
||||||
default: null
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup (props, context) {
|
|
||||||
const nuxtApp = useNuxtApp()
|
|
||||||
// Need to ensure (if we are not a child of `<NuxtPage>`) that we use synchronous route (not deferred)
|
|
||||||
const injectedRoute = inject(PageRouteSymbol)
|
|
||||||
const route = injectedRoute === useRoute() ? useVueRouterRoute() : injectedRoute
|
|
||||||
|
|
||||||
const layout = computed(() => unref(props.name) ?? route.meta.layout as string ?? 'default')
|
|
||||||
|
|
||||||
const layoutRef = ref()
|
|
||||||
context.expose({ layoutRef })
|
|
||||||
|
|
||||||
const done = nuxtApp.deferHydration()
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
const hasLayout = layout.value && layout.value in layouts
|
|
||||||
if (process.dev && layout.value && !hasLayout && layout.value !== 'default') {
|
|
||||||
console.warn(`Invalid layout \`${layout.value}\` selected.`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const transitionProps = route.meta.layoutTransition ?? defaultLayoutTransition
|
|
||||||
|
|
||||||
// We avoid rendering layout transition if there is no layout to render
|
|
||||||
return _wrapIf(Transition, hasLayout && transitionProps, {
|
|
||||||
default: () => h(Suspense, { suspensible: true, onResolve: () => { nextTick(done) } }, {
|
|
||||||
default: () => h(
|
|
||||||
// @ts-expect-error seems to be an issue in vue types
|
|
||||||
LayoutProvider,
|
|
||||||
{
|
|
||||||
layoutProps: mergeProps(context.attrs, { ref: layoutRef }),
|
|
||||||
key: layout.value,
|
|
||||||
name: layout.value,
|
|
||||||
shouldProvide: !props.name,
|
|
||||||
hasTransition: !!transitionProps
|
|
||||||
}, context.slots)
|
|
||||||
})
|
|
||||||
}).default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
const LayoutProvider = defineComponent({
|
|
||||||
name: 'NuxtLayoutProvider',
|
|
||||||
inheritAttrs: false,
|
|
||||||
props: {
|
|
||||||
name: {
|
|
||||||
type: [String, Boolean]
|
|
||||||
},
|
|
||||||
layoutProps: {
|
|
||||||
type: Object
|
|
||||||
},
|
|
||||||
hasTransition: {
|
|
||||||
type: Boolean
|
|
||||||
},
|
|
||||||
shouldProvide: {
|
|
||||||
type: Boolean
|
|
||||||
}
|
|
||||||
},
|
|
||||||
setup (props, context) {
|
|
||||||
// Prevent reactivity when the page will be rerendered in a different suspense fork
|
|
||||||
// eslint-disable-next-line vue/no-setup-props-destructure
|
|
||||||
const name = props.name
|
|
||||||
if (props.shouldProvide) {
|
|
||||||
provide(LayoutMetaSymbol, {
|
|
||||||
isCurrent: (route: RouteLocationNormalizedLoaded) => name === (route.meta.layout ?? 'default')
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
let vnode: VNode | undefined
|
|
||||||
if (process.dev && process.client) {
|
|
||||||
onMounted(() => {
|
|
||||||
nextTick(() => {
|
|
||||||
if (['#comment', '#text'].includes(vnode?.el?.nodeName)) {
|
|
||||||
if (name) {
|
|
||||||
console.warn(`[nuxt] \`${name}\` layout does not have a single root node and will cause errors when navigating between routes.`)
|
|
||||||
} else {
|
|
||||||
console.warn('[nuxt] `<NuxtLayout>` needs to be passed a single root node in its default slot.')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
if (!name || (typeof name === 'string' && !(name in layouts))) {
|
|
||||||
if (process.dev && process.client && props.hasTransition) {
|
|
||||||
vnode = context.slots.default?.() as VNode | undefined
|
|
||||||
return vnode
|
|
||||||
}
|
|
||||||
return context.slots.default?.()
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.dev && process.client && props.hasTransition) {
|
|
||||||
vnode = h(
|
|
||||||
// @ts-expect-error seems to be an issue in vue types
|
|
||||||
LayoutLoader,
|
|
||||||
{ key: name, layoutProps: props.layoutProps, name },
|
|
||||||
context.slots
|
|
||||||
)
|
|
||||||
|
|
||||||
return vnode
|
|
||||||
}
|
|
||||||
|
|
||||||
return h(
|
|
||||||
// @ts-expect-error seems to be an issue in vue types
|
|
||||||
LayoutLoader,
|
|
||||||
{ key: name, layoutProps: props.layoutProps, name },
|
|
||||||
context.slots
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
153
packages/nuxt/src/app/components/nuxt-layout.ts
Normal file
153
packages/nuxt/src/app/components/nuxt-layout.ts
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
import type { DefineComponent, MaybeRef, VNode } from 'vue'
|
||||||
|
import { Suspense, Transition, computed, defineComponent, h, inject, mergeProps, nextTick, onMounted, provide, ref, unref } from 'vue'
|
||||||
|
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||||
|
import { _wrapIf } from './utils'
|
||||||
|
import { LayoutMetaSymbol, PageRouteSymbol } from './injections'
|
||||||
|
import type { PageMeta } from '#app'
|
||||||
|
|
||||||
|
import { useRoute } from '#app/composables/router'
|
||||||
|
import { useNuxtApp } from '#app/nuxt'
|
||||||
|
// @ts-expect-error virtual file
|
||||||
|
import { useRoute as useVueRouterRoute } from '#build/pages'
|
||||||
|
// @ts-expect-error virtual file
|
||||||
|
import layouts from '#build/layouts'
|
||||||
|
// @ts-expect-error virtual file
|
||||||
|
import { appLayoutTransition as defaultLayoutTransition } from '#build/nuxt.config.mjs'
|
||||||
|
|
||||||
|
// TODO: revert back to defineAsyncComponent when https://github.com/vuejs/core/issues/6638 is resolved
|
||||||
|
const LayoutLoader = defineComponent({
|
||||||
|
name: 'LayoutLoader',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
name: String,
|
||||||
|
layoutProps: Object
|
||||||
|
},
|
||||||
|
async setup (props, context) {
|
||||||
|
const LayoutComponent = await layouts[props.name]().then((r: any) => r.default || r)
|
||||||
|
|
||||||
|
return () => h(LayoutComponent, props.layoutProps, context.slots)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
export default defineComponent({
|
||||||
|
name: 'NuxtLayout',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: [String, Boolean, Object] as unknown as () => unknown extends PageMeta['layout'] ? MaybeRef<string | false> : PageMeta['layout'],
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup (props, context) {
|
||||||
|
const nuxtApp = useNuxtApp()
|
||||||
|
// Need to ensure (if we are not a child of `<NuxtPage>`) that we use synchronous route (not deferred)
|
||||||
|
const injectedRoute = inject(PageRouteSymbol)
|
||||||
|
const route = injectedRoute === useRoute() ? useVueRouterRoute() : injectedRoute
|
||||||
|
|
||||||
|
const layout = computed(() => unref(props.name) ?? route.meta.layout as string ?? 'default')
|
||||||
|
|
||||||
|
const layoutRef = ref()
|
||||||
|
context.expose({ layoutRef })
|
||||||
|
|
||||||
|
const done = nuxtApp.deferHydration()
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
const hasLayout = layout.value && layout.value in layouts
|
||||||
|
if (process.dev && layout.value && !hasLayout && layout.value !== 'default') {
|
||||||
|
console.warn(`Invalid layout \`${layout.value}\` selected.`)
|
||||||
|
}
|
||||||
|
|
||||||
|
const transitionProps = route.meta.layoutTransition ?? defaultLayoutTransition
|
||||||
|
|
||||||
|
// We avoid rendering layout transition if there is no layout to render
|
||||||
|
return _wrapIf(Transition, hasLayout && transitionProps, {
|
||||||
|
default: () => h(Suspense, { suspensible: true, onResolve: () => { nextTick(done) } }, {
|
||||||
|
default: () => h(
|
||||||
|
// @ts-expect-error seems to be an issue in vue types
|
||||||
|
LayoutProvider,
|
||||||
|
{
|
||||||
|
layoutProps: mergeProps(context.attrs, { ref: layoutRef }),
|
||||||
|
key: layout.value,
|
||||||
|
name: layout.value,
|
||||||
|
shouldProvide: !props.name,
|
||||||
|
hasTransition: !!transitionProps
|
||||||
|
}, context.slots)
|
||||||
|
})
|
||||||
|
}).default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}) as unknown as DefineComponent<{
|
||||||
|
name: unknown extends PageMeta['layout'] ? MaybeRef<string | false> : PageMeta['layout']
|
||||||
|
}>
|
||||||
|
|
||||||
|
const LayoutProvider = defineComponent({
|
||||||
|
name: 'NuxtLayoutProvider',
|
||||||
|
inheritAttrs: false,
|
||||||
|
props: {
|
||||||
|
name: {
|
||||||
|
type: [String, Boolean]
|
||||||
|
},
|
||||||
|
layoutProps: {
|
||||||
|
type: Object
|
||||||
|
},
|
||||||
|
hasTransition: {
|
||||||
|
type: Boolean
|
||||||
|
},
|
||||||
|
shouldProvide: {
|
||||||
|
type: Boolean
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup (props, context) {
|
||||||
|
// Prevent reactivity when the page will be rerendered in a different suspense fork
|
||||||
|
// eslint-disable-next-line vue/no-setup-props-destructure
|
||||||
|
const name = props.name
|
||||||
|
if (props.shouldProvide) {
|
||||||
|
provide(LayoutMetaSymbol, {
|
||||||
|
isCurrent: (route: RouteLocationNormalizedLoaded) => name === (route.meta.layout ?? 'default')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
let vnode: VNode | undefined
|
||||||
|
if (process.dev && process.client) {
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(() => {
|
||||||
|
if (['#comment', '#text'].includes(vnode?.el?.nodeName)) {
|
||||||
|
if (name) {
|
||||||
|
console.warn(`[nuxt] \`${name}\` layout does not have a single root node and will cause errors when navigating between routes.`)
|
||||||
|
} else {
|
||||||
|
console.warn('[nuxt] `<NuxtLayout>` needs to be passed a single root node in its default slot.')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (!name || (typeof name === 'string' && !(name in layouts))) {
|
||||||
|
if (process.dev && process.client && props.hasTransition) {
|
||||||
|
vnode = context.slots.default?.() as VNode | undefined
|
||||||
|
return vnode
|
||||||
|
}
|
||||||
|
return context.slots.default?.()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.dev && process.client && props.hasTransition) {
|
||||||
|
vnode = h(
|
||||||
|
// @ts-expect-error seems to be an issue in vue types
|
||||||
|
LayoutLoader,
|
||||||
|
{ key: name, layoutProps: props.layoutProps, name },
|
||||||
|
context.slots
|
||||||
|
)
|
||||||
|
|
||||||
|
return vnode
|
||||||
|
}
|
||||||
|
|
||||||
|
return h(
|
||||||
|
// @ts-expect-error seems to be an issue in vue types
|
||||||
|
LayoutLoader,
|
||||||
|
{ key: name, layoutProps: props.layoutProps, name },
|
||||||
|
context.slots
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -221,7 +221,7 @@ export const abortNavigation = (err?: string | Partial<NuxtError>) => {
|
|||||||
throw err
|
throw err
|
||||||
}
|
}
|
||||||
|
|
||||||
export const setPageLayout = (layout: string) => {
|
export const setPageLayout = (layout: unknown extends PageMeta['layout'] ? string : PageMeta['layout']) => {
|
||||||
if (process.server) {
|
if (process.server) {
|
||||||
if (process.dev && getCurrentInstance() && useState('_layout').value !== layout) {
|
if (process.dev && getCurrentInstance() && useState('_layout').value !== layout) {
|
||||||
console.warn('[warn] [nuxt] `setPageLayout` should not be called to change the layout on the server within a component as this will cause hydration errors.')
|
console.warn('[warn] [nuxt] `setPageLayout` should not be called to change the layout on the server within a component as this will cause hydration errors.')
|
||||||
|
@ -200,7 +200,7 @@ async function initNuxt (nuxt: Nuxt) {
|
|||||||
addComponent({
|
addComponent({
|
||||||
name: 'NuxtLayout',
|
name: 'NuxtLayout',
|
||||||
priority: 10, // built-in that we do not expect the user to override
|
priority: 10, // built-in that we do not expect the user to override
|
||||||
filePath: resolve(nuxt.options.appDir, 'components/layout')
|
filePath: resolve(nuxt.options.appDir, 'components/nuxt-layout')
|
||||||
})
|
})
|
||||||
|
|
||||||
// Add <NuxtErrorBoundary>
|
// Add <NuxtErrorBoundary>
|
||||||
|
@ -353,11 +353,11 @@ export default defineNuxtModule({
|
|||||||
getContents: ({ app }: { app: NuxtApp }) => {
|
getContents: ({ app }: { app: NuxtApp }) => {
|
||||||
const composablesFile = resolve(runtimeDir, 'composables')
|
const composablesFile = resolve(runtimeDir, 'composables')
|
||||||
return [
|
return [
|
||||||
'import { ComputedRef, Ref } from \'vue\'',
|
'import { ComputedRef, MaybeRef } from \'vue\'',
|
||||||
`export type LayoutKey = ${Object.keys(app.layouts).map(name => genString(name)).join(' | ') || 'string'}`,
|
`export type LayoutKey = ${Object.keys(app.layouts).map(name => genString(name)).join(' | ') || 'string'}`,
|
||||||
`declare module ${genString(composablesFile)} {`,
|
`declare module ${genString(composablesFile)} {`,
|
||||||
' interface PageMeta {',
|
' interface PageMeta {',
|
||||||
' layout?: false | LayoutKey | Ref<LayoutKey> | ComputedRef<LayoutKey>',
|
' layout?: MaybeRef<LayoutKey | false> | ComputedRef<LayoutKey | false>',
|
||||||
' }',
|
' }',
|
||||||
'}'
|
'}'
|
||||||
].join('\n')
|
].join('\n')
|
||||||
|
Loading…
Reference in New Issue
Block a user