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
|
## 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:
|
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]
|
```html{}[pages/parent.vue]
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1>I am the parent view</h1>
|
<h1>I am the parent view</h1>
|
||||||
<NuxtNestedPage :foobar="123" />
|
<NuxtPage :foobar="123" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
|
||||||
### Child route keys
|
### 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]
|
```html{}[pages/parent.vue]
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h1>I am the parent view</h1>
|
<h1>I am the parent view</h1>
|
||||||
<NuxtNestedPage :child-key="someKey" />
|
<NuxtPage :page-key="someKey" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
```
|
```
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
Parent
|
Parent
|
||||||
<NuxtNestedPage />
|
<NuxtPage />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -7,7 +7,4 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const reloads = useState('reload', () => 0)
|
const reloads = useState('reload', () => 0)
|
||||||
onMounted(() => { reloads.value++ })
|
onMounted(() => { reloads.value++ })
|
||||||
definePageMeta({
|
|
||||||
key: route => route.path
|
|
||||||
})
|
|
||||||
</script>
|
</script>
|
||||||
|
@ -7,4 +7,7 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
const reloads = useState('static', () => 0)
|
const reloads = useState('static', () => 0)
|
||||||
onMounted(() => { reloads.value++ })
|
onMounted(() => { reloads.value++ })
|
||||||
|
definePageMeta({
|
||||||
|
key: 'static'
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
@ -14,7 +14,7 @@ export interface PageMeta {
|
|||||||
[key: string]: any
|
[key: string]: any
|
||||||
pageTransition?: boolean | TransitionProps
|
pageTransition?: boolean | TransitionProps
|
||||||
layoutTransition?: boolean | TransitionProps
|
layoutTransition?: boolean | TransitionProps
|
||||||
key?: string | ((route: RouteLocationNormalizedLoaded) => string)
|
key?: false | string | ((route: RouteLocationNormalizedLoaded) => string)
|
||||||
keepalive?: boolean | KeepAliveProps
|
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 { defineComponent, h, Suspense, Transition } from 'vue'
|
||||||
import { RouterView } from 'vue-router'
|
import { RouteLocationNormalizedLoaded, RouterView } from 'vue-router'
|
||||||
import { wrapIf, wrapInKeepAlive } from './utils'
|
|
||||||
import { useNuxtApp } from '#app'
|
|
||||||
|
|
||||||
type InstanceOf<T> = T extends new (...args: any[]) => infer R ? R : never
|
import { generateRouteKey, RouterViewSlotProps, wrapIf, wrapInKeepAlive } from './utils'
|
||||||
type RouterViewSlotProps = Parameters<InstanceOf<typeof RouterView>['$slots']['default']>[0]
|
import { useNuxtApp } from '#app'
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
name: 'NuxtPage',
|
name: 'NuxtPage',
|
||||||
setup () {
|
props: {
|
||||||
|
pageKey: {
|
||||||
|
type: [Function, String] as unknown as () => string | ((route: RouteLocationNormalizedLoaded) => string),
|
||||||
|
default: null
|
||||||
|
}
|
||||||
|
},
|
||||||
|
setup (props) {
|
||||||
const nuxtApp = useNuxtApp()
|
const nuxtApp = useNuxtApp()
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
return h(RouterView, {}, {
|
return h(RouterView, {}, {
|
||||||
default: ({ Component, route }: RouterViewSlotProps) => Component &&
|
default: (routeProps: RouterViewSlotProps) => routeProps.Component &&
|
||||||
wrapIf(Transition, route.meta.pageTransition ?? defaultPageTransition,
|
wrapIf(Transition, routeProps.route.meta.pageTransition ?? defaultPageTransition,
|
||||||
wrapInKeepAlive(route.meta.keepalive, h(Suspense, {
|
wrapInKeepAlive(routeProps.route.meta.keepalive, h(Suspense, {
|
||||||
onPending: () => nuxtApp.callHook('page:start', Component),
|
onPending: () => nuxtApp.callHook('page:start', routeProps.Component),
|
||||||
onResolve: () => nuxtApp.callHook('page:finish', Component)
|
onResolve: () => nuxtApp.callHook('page:finish', routeProps.Component)
|
||||||
}, { default: () => h(Component) }))).default()
|
}, { default: () => h(routeProps.Component, { key: generateRouteKey(props.pageKey, routeProps) }) }))).default()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,7 +6,6 @@ import {
|
|||||||
RouterLink,
|
RouterLink,
|
||||||
NavigationGuard
|
NavigationGuard
|
||||||
} from 'vue-router'
|
} from 'vue-router'
|
||||||
import NuxtNestedPage from './nested-page.vue'
|
|
||||||
import NuxtPage from './page'
|
import NuxtPage from './page'
|
||||||
import NuxtLayout from './layout'
|
import NuxtLayout from './layout'
|
||||||
import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig } from '#app'
|
import { callWithNuxt, defineNuxtPlugin, useRuntimeConfig } from '#app'
|
||||||
@ -17,20 +16,23 @@ import { globalMiddleware, namedMiddleware } from '#build/middleware'
|
|||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
NuxtNestedPage: typeof NuxtNestedPage
|
|
||||||
NuxtPage: typeof NuxtPage
|
NuxtPage: typeof NuxtPage
|
||||||
NuxtLayout: typeof NuxtLayout
|
NuxtLayout: typeof NuxtLayout
|
||||||
NuxtLink: typeof RouterLink
|
NuxtLink: typeof RouterLink
|
||||||
|
/** @deprecated */
|
||||||
|
NuxtNestedPage: typeof NuxtPage
|
||||||
|
/** @deprecated */
|
||||||
|
NuxtChild: typeof NuxtPage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defineNuxtPlugin((nuxtApp) => {
|
export default defineNuxtPlugin((nuxtApp) => {
|
||||||
nuxtApp.vueApp.component('NuxtNestedPage', NuxtNestedPage)
|
|
||||||
nuxtApp.vueApp.component('NuxtPage', NuxtPage)
|
nuxtApp.vueApp.component('NuxtPage', NuxtPage)
|
||||||
nuxtApp.vueApp.component('NuxtLayout', NuxtLayout)
|
nuxtApp.vueApp.component('NuxtLayout', NuxtLayout)
|
||||||
nuxtApp.vueApp.component('NuxtLink', RouterLink)
|
nuxtApp.vueApp.component('NuxtLink', RouterLink)
|
||||||
// TODO: remove before release - present for backwards compatibility & intentionally undocumented
|
// 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 { baseURL } = useRuntimeConfig().app
|
||||||
const routerHistory = process.client
|
const routerHistory = process.client
|
||||||
|
@ -1,4 +1,21 @@
|
|||||||
import { Component, KeepAlive, h } from 'vue'
|
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 = {
|
const Fragment = {
|
||||||
setup (_props, { slots }) {
|
setup (_props, { slots }) {
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { expect, describe, it } from 'vitest'
|
import { expect, describe, it } from 'vitest'
|
||||||
import { generateRoutesFromFiles } from '../src/pages/utils'
|
import { generateRoutesFromFiles } from '../src/pages/utils'
|
||||||
|
import { generateRouteKey } from '../src/pages/runtime/utils'
|
||||||
|
|
||||||
describe('pages:generateRoutesFromFiles', () => {
|
describe('pages:generateRoutesFromFiles', () => {
|
||||||
const pagesDir = 'pages'
|
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