mirror of
https://github.com/nuxt/nuxt.git
synced 2024-11-22 13:45:18 +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'
|
||||
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 { 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
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
// TODO: remove in 4.x
|
||||
export { default } from './nuxt-layout'
|
||||
|
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
|
||||
}
|
||||
|
||||
export const setPageLayout = (layout: string) => {
|
||||
export const setPageLayout = (layout: unknown extends PageMeta['layout'] ? string : PageMeta['layout']) => {
|
||||
if (process.server) {
|
||||
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.')
|
||||
|
@ -200,7 +200,7 @@ async function initNuxt (nuxt: Nuxt) {
|
||||
addComponent({
|
||||
name: 'NuxtLayout',
|
||||
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>
|
||||
|
@ -353,11 +353,11 @@ export default defineNuxtModule({
|
||||
getContents: ({ app }: { app: NuxtApp }) => {
|
||||
const composablesFile = resolve(runtimeDir, 'composables')
|
||||
return [
|
||||
'import { ComputedRef, Ref } from \'vue\'',
|
||||
'import { ComputedRef, MaybeRef } from \'vue\'',
|
||||
`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>',
|
||||
' layout?: MaybeRef<LayoutKey | false> | ComputedRef<LayoutKey | false>',
|
||||
' }',
|
||||
'}'
|
||||
].join('\n')
|
||||
|
Loading…
Reference in New Issue
Block a user