fix(nuxt3)!: key routes by interpolated path (#2976)

This commit is contained in:
Daniel Roe 2022-02-07 11:32:04 +00:00 committed by GitHub
parent 5b50e69e6c
commit b3e9cf6fd6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 152 additions and 57 deletions

View File

@ -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>
```

View File

@ -1,6 +1,6 @@
<template>
<div>
Parent
<NuxtNestedPage />
<NuxtPage />
</div>
</template>

View File

@ -7,7 +7,4 @@
<script setup>
const reloads = useState('reload', () => 0)
onMounted(() => { reloads.value++ })
definePageMeta({
key: route => route.path
})
</script>

View File

@ -7,4 +7,7 @@
<script setup>
const reloads = useState('static', () => 0)
onMounted(() => { reloads.value++ })
definePageMeta({
key: 'static'
})
</script>

View File

@ -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
}

View File

@ -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>

View File

@ -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()
})
}
}

View File

@ -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

View File

@ -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 }) {

View File

@ -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)
})
}
})