diff --git a/docs/content/3.docs/2.directory-structure/10.pages.md b/docs/content/3.docs/2.directory-structure/10.pages.md index c35c582e3b..dd6f2210f0 100644 --- a/docs/content/3.docs/2.directory-structure/10.pages.md +++ b/docs/content/3.docs/2.directory-structure/10.pages.md @@ -93,7 +93,7 @@ If you want to know more about ``, read the [Vue Router documentati ## Nested Routes -We provide a semantic alias for `RouterView`, the `` component, for displaying the children components of a [nested route](https://router.vuejs.org/guide/essentials/nested-routes.html). +It is possible to display [nested routes](https://next.router.vuejs.org/guide/essentials/nested-routes.html) with ``. Example: @@ -123,26 +123,26 @@ This file tree will generate these routes: ] ``` -To display the `child.vue` component, you have to insert the `` component inside `pages/parent.vue`: +To display the `child.vue` component, you have to insert the `` component inside `pages/parent.vue`: ```html{}[pages/parent.vue] ``` ### Child route keys -If you want more control over when the `` component is re-rendered (for example, for transitions), you can either pass a string or function via the `childKey` prop, or you can define a `key` value via `definePageMeta`: +If you want more control over when the `` component is re-rendered (for example, for transitions), you can either pass a string or function via the `pageKey` prop, or you can define a `key` value via `definePageMeta`: ```html{}[pages/parent.vue] ``` diff --git a/examples/with-pages/pages/parent.vue b/examples/with-pages/pages/parent.vue index 15d610e321..dd5a406de2 100644 --- a/examples/with-pages/pages/parent.vue +++ b/examples/with-pages/pages/parent.vue @@ -1,6 +1,6 @@ diff --git a/examples/with-pages/pages/parent/reload-[id].vue b/examples/with-pages/pages/parent/reload-[id].vue index b0e07f7762..baa06935fa 100644 --- a/examples/with-pages/pages/parent/reload-[id].vue +++ b/examples/with-pages/pages/parent/reload-[id].vue @@ -7,7 +7,4 @@ diff --git a/examples/with-pages/pages/parent/static-[id].vue b/examples/with-pages/pages/parent/static-[id].vue index da1aaa7e2e..eaa7085ef9 100644 --- a/examples/with-pages/pages/parent/static-[id].vue +++ b/examples/with-pages/pages/parent/static-[id].vue @@ -7,4 +7,7 @@ diff --git a/packages/nuxt3/src/pages/runtime/composables.ts b/packages/nuxt3/src/pages/runtime/composables.ts index b0f9ba20cc..52b0f4df5e 100644 --- a/packages/nuxt3/src/pages/runtime/composables.ts +++ b/packages/nuxt3/src/pages/runtime/composables.ts @@ -14,7 +14,7 @@ export interface PageMeta { [key: string]: any pageTransition?: boolean | TransitionProps layoutTransition?: boolean | TransitionProps - key?: string | ((route: RouteLocationNormalizedLoaded) => string) + key?: false | string | ((route: RouteLocationNormalizedLoaded) => string) keepalive?: boolean | KeepAliveProps } diff --git a/packages/nuxt3/src/pages/runtime/nested-page.vue b/packages/nuxt3/src/pages/runtime/nested-page.vue deleted file mode 100644 index f1602b4865..0000000000 --- a/packages/nuxt3/src/pages/runtime/nested-page.vue +++ /dev/null @@ -1,31 +0,0 @@ - - - diff --git a/packages/nuxt3/src/pages/runtime/page.ts b/packages/nuxt3/src/pages/runtime/page.ts index c1b1f8116e..eea620e048 100644 --- a/packages/nuxt3/src/pages/runtime/page.ts +++ b/packages/nuxt3/src/pages/runtime/page.ts @@ -1,24 +1,28 @@ import { defineComponent, h, Suspense, Transition } from 'vue' -import { RouterView } from 'vue-router' -import { wrapIf, wrapInKeepAlive } from './utils' -import { useNuxtApp } from '#app' +import { RouteLocationNormalizedLoaded, RouterView } from 'vue-router' -type InstanceOf = T extends new (...args: any[]) => infer R ? R : never -type RouterViewSlotProps = Parameters['$slots']['default']>[0] +import { generateRouteKey, RouterViewSlotProps, wrapIf, wrapInKeepAlive } from './utils' +import { useNuxtApp } from '#app' export default defineComponent({ name: 'NuxtPage', - setup () { + props: { + pageKey: { + type: [Function, String] as unknown as () => string | ((route: RouteLocationNormalizedLoaded) => string), + default: null + } + }, + setup (props) { const nuxtApp = useNuxtApp() return () => { return h(RouterView, {}, { - default: ({ Component, route }: RouterViewSlotProps) => Component && - wrapIf(Transition, route.meta.pageTransition ?? defaultPageTransition, - wrapInKeepAlive(route.meta.keepalive, h(Suspense, { - onPending: () => nuxtApp.callHook('page:start', Component), - onResolve: () => nuxtApp.callHook('page:finish', Component) - }, { default: () => h(Component) }))).default() + default: (routeProps: RouterViewSlotProps) => routeProps.Component && + 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) + }, { default: () => h(routeProps.Component, { key: generateRouteKey(props.pageKey, routeProps) }) }))).default() }) } } diff --git a/packages/nuxt3/src/pages/runtime/router.ts b/packages/nuxt3/src/pages/runtime/router.ts index e0f0a2906e..246f303c2d 100644 --- a/packages/nuxt3/src/pages/runtime/router.ts +++ b/packages/nuxt3/src/pages/runtime/router.ts @@ -6,7 +6,6 @@ import { RouterLink, NavigationGuard } from 'vue-router' -import NuxtNestedPage from './nested-page.vue' import NuxtPage from './page' import NuxtLayout from './layout' import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig } from '#app' @@ -17,20 +16,23 @@ import { globalMiddleware, namedMiddleware } from '#build/middleware' declare module 'vue' { export interface GlobalComponents { - NuxtNestedPage: typeof NuxtNestedPage NuxtPage: typeof NuxtPage NuxtLayout: typeof NuxtLayout NuxtLink: typeof RouterLink + /** @deprecated */ + NuxtNestedPage: typeof NuxtPage + /** @deprecated */ + NuxtChild: typeof NuxtPage } } export default defineNuxtPlugin((nuxtApp) => { - nuxtApp.vueApp.component('NuxtNestedPage', NuxtNestedPage) 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('NuxtChild', NuxtNestedPage) + nuxtApp.vueApp.component('NuxtNestedPage', NuxtPage) + nuxtApp.vueApp.component('NuxtChild', NuxtPage) const { baseURL } = useRuntimeConfig().app const routerHistory = process.client diff --git a/packages/nuxt3/src/pages/runtime/utils.ts b/packages/nuxt3/src/pages/runtime/utils.ts index 3a163e7573..cba7fb0080 100644 --- a/packages/nuxt3/src/pages/runtime/utils.ts +++ b/packages/nuxt3/src/pages/runtime/utils.ts @@ -1,4 +1,21 @@ import { Component, KeepAlive, h } from 'vue' +import { RouterView, RouteLocationMatched, RouteLocationNormalizedLoaded } from 'vue-router' + +type InstanceOf = T extends new (...args: any[]) => infer R ? R : never +export type RouterViewSlotProps = Parameters['$slots']['default']>[0] + +const interpolatePath = (route: RouteLocationNormalizedLoaded, match: RouteLocationMatched) => { + return match.path + .replace(/(?<=:\w+)\([^)]+\)/g, '') + .replace(/(?<=:\w+)[?+*]/g, '') + .replace(/:\w+/g, r => route.params[r.slice(1)]?.toString() || '') +} + +export const generateRouteKey = (override: string | ((route: RouteLocationNormalizedLoaded) => string), routeProps: RouterViewSlotProps) => { + const matchedRoute = routeProps.route.matched.find(m => m.components.default === routeProps.Component.type) + const source = override ?? matchedRoute?.meta.key ?? interpolatePath(routeProps.route, matchedRoute) + return typeof source === 'function' ? source(routeProps.route) : source +} const Fragment = { setup (_props, { slots }) { diff --git a/packages/nuxt3/test/pages.test.ts b/packages/nuxt3/test/pages.test.ts index d69d08333b..def36f0247 100644 --- a/packages/nuxt3/test/pages.test.ts +++ b/packages/nuxt3/test/pages.test.ts @@ -1,5 +1,6 @@ import { expect, describe, it } from 'vitest' import { generateRoutesFromFiles } from '../src/pages/utils' +import { generateRouteKey } from '../src/pages/runtime/utils' describe('pages:generateRoutesFromFiles', () => { const pagesDir = 'pages' @@ -191,3 +192,105 @@ describe('pages:generateRoutesFromFiles', () => { }) } }) + +describe('pages:generateRouteKey', () => { + const defaultComponent = { type: {} } + const getRouteProps = (matchedRoute = {}) => ({ + Component: defaultComponent, + route: { + meta: { key: 'route-meta-key' }, + params: { + id: 'foo', + optional: 'bar', + array: ['a', 'b'] + }, + matched: [ + { + components: { default: {} }, + meta: { key: 'other-meta-key' } + }, + { + components: { default: defaultComponent.type }, + meta: { key: 'matched-meta-key' }, + ...matchedRoute + } + ] + } + }) as any + + const tests = [ + { description: 'should handle overrides', override: 'key', route: getRouteProps(), output: 'key' }, + { description: 'should handle overrides', override: route => route.meta.key as string, route: getRouteProps(), output: 'route-meta-key' }, + { description: 'should handle overrides', override: false as any, route: getRouteProps(), output: false }, + { + description: 'should key dynamic routes without keys', + route: getRouteProps({ + path: '/test/:id', + meta: {} + }), + output: '/test/foo' + }, + { + description: 'should key dynamic routes without keys', + route: getRouteProps({ + path: '/test/:id(\\d+)', + meta: {} + }), + output: '/test/foo' + }, + { + description: 'should key dynamic routes with optional params', + route: getRouteProps({ + path: '/test/:optional?', + meta: {} + }), + output: '/test/bar' + }, + { + description: 'should key dynamic routes with optional params', + route: getRouteProps({ + path: '/test/:optional(\\d+)?', + meta: {} + }), + output: '/test/bar' + }, + { + description: 'should key dynamic routes with optional params', + route: getRouteProps({ + path: '/test/:undefined(\\d+)?', + meta: {} + }), + output: '/test/' + }, + { + description: 'should key dynamic routes with array params', + route: getRouteProps({ + path: '/:array+', + meta: {} + }), + output: '/a,b' + }, + { + description: 'should key dynamic routes with array params', + route: getRouteProps({ + path: '/test/:array*', + meta: {} + }), + output: '/test/a,b' + }, + { + description: 'should key dynamic routes with array params', + route: getRouteProps({ + path: '/test/:other*', + meta: {} + }), + output: '/test/' + } + ] + + for (const test of tests) { + it(test.description, () => { + expect(generateRouteKey(test.override, test.route)).to.deep.equal(test.output) + }) + } +})