mirror of
https://github.com/nuxt/nuxt.git
synced 2025-01-18 01:15:58 +00:00
fix(nuxt3)!: key routes by interpolated path (#2976)
This commit is contained in:
parent
5b50e69e6c
commit
b3e9cf6fd6
@ -93,7 +93,7 @@ If you want to know more about `<RouterLink>`, read the [Vue Router documentati
|
||||
|
||||
## Nested Routes
|
||||
|
||||
We provide a semantic alias for `RouterView`, the `<NuxtNestedPage>` 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 `<NuxtPage>`.
|
||||
|
||||
Example:
|
||||
|
||||
@ -123,26 +123,26 @@ This file tree will generate these routes:
|
||||
]
|
||||
```
|
||||
|
||||
To display the `child.vue` component, you have to insert the `<NuxtNestedPage>` component inside `pages/parent.vue`:
|
||||
To display the `child.vue` component, you have to insert the `<NuxtPage>` component inside `pages/parent.vue`:
|
||||
|
||||
```html{}[pages/parent.vue]
|
||||
<template>
|
||||
<div>
|
||||
<h1>I am the parent view</h1>
|
||||
<NuxtNestedPage :foobar="123" />
|
||||
<NuxtPage :foobar="123" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
||||
### Child route keys
|
||||
|
||||
If you want more control over when the `<NuxtNestedPage>` 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 `<NuxtPage>` 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]
|
||||
<template>
|
||||
<div>
|
||||
<h1>I am the parent view</h1>
|
||||
<NuxtNestedPage :child-key="someKey" />
|
||||
<NuxtPage :page-key="someKey" />
|
||||
</div>
|
||||
</template>
|
||||
```
|
||||
|
@ -1,6 +1,6 @@
|
||||
<template>
|
||||
<div>
|
||||
Parent
|
||||
<NuxtNestedPage />
|
||||
<NuxtPage />
|
||||
</div>
|
||||
</template>
|
||||
|
@ -7,7 +7,4 @@
|
||||
<script setup>
|
||||
const reloads = useState('reload', () => 0)
|
||||
onMounted(() => { reloads.value++ })
|
||||
definePageMeta({
|
||||
key: route => route.path
|
||||
})
|
||||
</script>
|
||||
|
@ -7,4 +7,7 @@
|
||||
<script setup>
|
||||
const reloads = useState('static', () => 0)
|
||||
onMounted(() => { reloads.value++ })
|
||||
definePageMeta({
|
||||
key: 'static'
|
||||
})
|
||||
</script>
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -1,31 +0,0 @@
|
||||
<template>
|
||||
<RouterView v-slot="{ Component }">
|
||||
<component :is="Component" :key="key" />
|
||||
</RouterView>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
import { useRoute } from 'vue-router'
|
||||
import { computed } from 'vue'
|
||||
|
||||
export default {
|
||||
name: 'NuxtNestedPage',
|
||||
props: {
|
||||
childKey: {
|
||||
type: [Function, String] as unknown as () => string | ((route: RouteLocationNormalizedLoaded) => string),
|
||||
default: null
|
||||
}
|
||||
},
|
||||
setup (props) {
|
||||
const route = useRoute()
|
||||
const key = computed(() => {
|
||||
const source = props.childKey ?? route.meta.key
|
||||
return typeof source === 'function' ? source(route) : source
|
||||
})
|
||||
return {
|
||||
key
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
@ -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> = T extends new (...args: any[]) => infer R ? R : never
|
||||
type RouterViewSlotProps = Parameters<InstanceOf<typeof RouterView>['$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()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -1,4 +1,21 @@
|
||||
import { Component, KeepAlive, h } from 'vue'
|
||||
import { RouterView, RouteLocationMatched, RouteLocationNormalizedLoaded } from 'vue-router'
|
||||
|
||||
type InstanceOf<T> = T extends new (...args: any[]) => infer R ? R : never
|
||||
export type RouterViewSlotProps = Parameters<InstanceOf<typeof RouterView>['$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 }) {
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
Loading…
Reference in New Issue
Block a user