From: Eduardo San Martin Morote Date: Thu, 4 Nov 2021 17:37:23 +0000 (+0100) Subject: feat(view): handle empty components as pass through X-Git-Tag: v4.1.0~137 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=e07c46938d0cc2ca2bc2b1d07fd543465121b60a;p=thirdparty%2Fvuejs%2Frouter.git feat(view): handle empty components as pass through --- diff --git a/__tests__/RouterView.spec.ts b/__tests__/RouterView.spec.ts index 2ba6c6c9..4d44fc3e 100644 --- a/__tests__/RouterView.spec.ts +++ b/__tests__/RouterView.spec.ts @@ -202,6 +202,33 @@ const routes = createRoutes({ }, ], }, + + passthrough: { + fullPath: '/foo', + name: undefined, + path: '/foo', + query: {}, + params: {}, + hash: '', + meta: {}, + matched: [ + { + // @ts-ignore: FIXME: + components: null, + instances: {}, + enterCallbacks: {}, + path: '/', + props, + }, + { + components: { default: components.Foo }, + instances: {}, + enterCallbacks: {}, + path: 'foo', + props, + }, + ], + }, }) describe('RouterView', () => { @@ -308,6 +335,11 @@ describe('RouterView', () => { expect(wrapper.html()).toBe(`
id:2;other:page
`) }) + it('pass through with empty children', async () => { + const { wrapper } = await factory(routes.passthrough) + expect(wrapper.html()).toBe(`
Foo
`) + }) + describe('warnings', () => { it('does not warn RouterView is wrapped', () => { const route = createMockedRoute(routes.root) diff --git a/__tests__/guards/extractComponentsGuards.spec.ts b/__tests__/guards/extractComponentsGuards.spec.ts index 8df52b48..64d61ca1 100644 --- a/__tests__/guards/extractComponentsGuards.spec.ts +++ b/__tests__/guards/extractComponentsGuards.spec.ts @@ -12,9 +12,9 @@ const to = START_LOCATION_NORMALIZED const from = START_LOCATION_NORMALIZED const NoGuard: RouteRecordRaw = { path: '/', component: components.Home } +// @ts-expect-error const InvalidRoute: RouteRecordRaw = { path: '/', - // @ts-expect-error component: null, } const WrongLazyRoute: RouteRecordRaw = { @@ -88,11 +88,8 @@ describe('extractComponentsGuards', () => { it('throws if component is null', async () => { // @ts-expect-error - await expect(checkGuards([InvalidRoute], 2)).rejects.toHaveProperty( - 'message', - expect.stringMatching('Invalid route component') - ) - expect('is not a valid component').toHaveBeenWarned() + await expect(checkGuards([InvalidRoute], 0)) + expect('either missing a "component(s)" or "children"').toHaveBeenWarned() }) it('warns wrong lazy component', async () => { diff --git a/__tests__/matcher/resolve.spec.ts b/__tests__/matcher/resolve.spec.ts index e26829be..c286a762 100644 --- a/__tests__/matcher/resolve.spec.ts +++ b/__tests__/matcher/resolve.spec.ts @@ -6,11 +6,20 @@ import { MatcherLocationRaw, MatcherLocation, } from '../../src/types' -import { MatcherLocationNormalizedLoose } from '../utils' +import { MatcherLocationNormalizedLoose, RouteRecordViewLoose } from '../utils' import { mockWarn } from 'jest-mock-warn' +import { defineComponent } from '@vue/runtime-core' -// @ts-expect-error -const component: RouteComponent = null +const component: RouteComponent = defineComponent({}) + +const baseRouteRecordNormalized: RouteRecordViewLoose = { + instances: {}, + enterCallbacks: {}, + aliasOf: undefined, + components: null, + path: '', + props: {}, +} // for normalized records const components = { default: component } @@ -232,6 +241,7 @@ describe('RouterMatcher.resolve', () => { matched: [ { path: '/p', + // @ts-expect-error: doesn't matter children, components, aliasOf: expect.objectContaining({ path: '/parent' }), @@ -573,6 +583,7 @@ describe('RouterMatcher.resolve', () => { matched: [ { path: '/parent', + // @ts-expect-error children, components, aliasOf: undefined, @@ -1025,7 +1036,10 @@ describe('RouterMatcher.resolve', () => { name: 'child-b', path: '/foo/b', params: {}, - matched: [Foo, { ...ChildB, path: `${Foo.path}/${ChildB.path}` }], + matched: [ + Foo as any, + { ...ChildB, path: `${Foo.path}/${ChildB.path}` }, + ], } ) }) @@ -1045,7 +1059,7 @@ describe('RouterMatcher.resolve', () => { name: 'nested', path: '/foo', params: {}, - matched: [Foo, { ...Nested, path: `${Foo.path}` }], + matched: [Foo as any, { ...Nested, path: `${Foo.path}` }], } ) }) @@ -1072,7 +1086,7 @@ describe('RouterMatcher.resolve', () => { path: '/foo', params: {}, matched: [ - Foo, + Foo as any, { ...Nested, path: `${Foo.path}` }, { ...NestedNested, path: `${Foo.path}` }, ], @@ -1095,7 +1109,7 @@ describe('RouterMatcher.resolve', () => { path: '/foo/nested/a', params: {}, matched: [ - Foo, + Foo as any, { ...Nested, path: `${Foo.path}/${Nested.path}` }, { ...NestedChildA, @@ -1121,7 +1135,7 @@ describe('RouterMatcher.resolve', () => { path: '/foo/nested/a', params: {}, matched: [ - Foo, + Foo as any, { ...Nested, path: `${Foo.path}/${Nested.path}` }, { ...NestedChildA, @@ -1147,7 +1161,7 @@ describe('RouterMatcher.resolve', () => { path: '/foo/nested/a', params: {}, matched: [ - Foo, + Foo as any, { ...Nested, path: `${Foo.path}/${Nested.path}` }, { ...NestedChildA, @@ -1180,7 +1194,7 @@ describe('RouterMatcher.resolve', () => { path: '/foo/nested/a/b', params: { p: 'b', n: 'a' }, matched: [ - Foo, + Foo as any, { ...NestedWithParam, path: `${Foo.path}/${NestedWithParam.path}`, @@ -1209,7 +1223,7 @@ describe('RouterMatcher.resolve', () => { path: '/foo/nested/b/a', params: { p: 'a', n: 'b' }, matched: [ - Foo, + Foo as any, { ...NestedWithParam, path: `${Foo.path}/${NestedWithParam.path}`, @@ -1257,7 +1271,7 @@ describe('RouterMatcher.resolve', () => { name: 'nested', path: '/nested', params: {}, - matched: [Parent, { ...Nested, path: `/nested` }], + matched: [Parent as any, { ...Nested, path: `/nested` }], } ) }) @@ -1277,7 +1291,7 @@ describe('RouterMatcher.resolve', () => { name: 'nested', path: '/parent/nested', params: {}, - matched: [Parent, { ...Nested, path: `/parent/nested` }], + matched: [Parent as any, { ...Nested, path: `/parent/nested` }], } ) }) diff --git a/__tests__/utils.ts b/__tests__/utils.ts index 8eddc12c..35f7d633 100644 --- a/__tests__/utils.ts +++ b/__tests__/utils.ts @@ -51,13 +51,15 @@ export function nextNavigation(router: Router) { export interface RouteRecordViewLoose extends Pick< RouteRecordMultipleViews, - 'path' | 'name' | 'components' | 'children' | 'meta' | 'beforeEnter' + 'path' | 'name' | 'meta' | 'beforeEnter' > { leaveGuards?: any instances: Record enterCallbacks: Record props: Record aliasOf: RouteRecordViewLoose | undefined + children?: RouteRecordViewLoose[] + components: Record | null | undefined } // @ts-expect-error we are intentionally overriding the type diff --git a/src/RouterView.ts b/src/RouterView.ts index d8d48fc6..4c2eca48 100644 --- a/src/RouterView.ts +++ b/src/RouterView.ts @@ -5,6 +5,7 @@ import { defineComponent, PropType, ref, + unref, ComponentPublicInstance, VNodeProps, getCurrentInstance, @@ -61,12 +62,29 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({ const injectedRoute = inject(routerViewLocationKey)! const routeToDisplay = computed(() => props.route || injectedRoute.value) - const depth = inject(viewDepthKey, 0) + const injectedDepth = inject(viewDepthKey, 0) + // The depth changes based on empty components option, which allows passthrough routes e.g. routes with children + // that are used to reuse the `path` property + const depth = computed(() => { + let initialDepth = unref(injectedDepth) + const { matched } = routeToDisplay.value + let matchedRoute: RouteLocationMatched | undefined + while ( + (matchedRoute = matched[initialDepth]) && + !matchedRoute.components + ) { + initialDepth++ + } + return initialDepth + }) const matchedRouteRef = computed( - () => routeToDisplay.value.matched[depth] + () => routeToDisplay.value.matched[depth.value] ) - provide(viewDepthKey, depth + 1) + provide( + viewDepthKey, + computed(() => depth.value + 1) + ) provide(matchedRouteKey, matchedRouteRef) provide(routerViewLocationKey, routeToDisplay) @@ -117,7 +135,7 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({ return () => { const route = routeToDisplay.value const matchedRoute = matchedRouteRef.value - const ViewComponent = matchedRoute && matchedRoute.components[props.name] + const ViewComponent = matchedRoute && matchedRoute.components![props.name] // we need the value at the time we render because when we unmount, we // navigated to a different location so the value is different const currentName = props.name @@ -158,7 +176,7 @@ export const RouterViewImpl = /*#__PURE__*/ defineComponent({ ) { // TODO: can display if it's an alias, its props const info: RouterViewDevtoolsContext = { - depth, + depth: depth.value, name: matchedRoute.name, path: matchedRoute.path, meta: matchedRoute.meta, diff --git a/src/injectionSymbols.ts b/src/injectionSymbols.ts index 0d345ea1..60838fdf 100644 --- a/src/injectionSymbols.ts +++ b/src/injectionSymbols.ts @@ -32,7 +32,7 @@ export const matchedRouteKey = /*#__PURE__*/ PolySymbol( */ export const viewDepthKey = /*#__PURE__*/ PolySymbol( __DEV__ ? 'router view depth' : 'rvd' -) as InjectionKey +) as InjectionKey | number> /** * Allows overriding the router instance returned by `useRouter` in tests. r diff --git a/src/matcher/index.ts b/src/matcher/index.ts index ca31e86f..ce0bce66 100644 --- a/src/matcher/index.ts +++ b/src/matcher/index.ts @@ -350,6 +350,7 @@ export function normalizeRouteRecord( aliasOf: undefined, beforeEnter: record.beforeEnter, props: normalizeRecordProps(record), + // @ts-expect-error: record.children only exists in some cases children: record.children || [], instances: {}, leaveGuards: new Set(), @@ -357,8 +358,8 @@ export function normalizeRouteRecord( enterCallbacks: {}, components: 'components' in record - ? record.components || {} - : { default: record.component! }, + ? record.components || null + : record.component && { default: record.component }, } } diff --git a/src/matcher/types.ts b/src/matcher/types.ts index 3939e935..a0c33b36 100644 --- a/src/matcher/types.ts +++ b/src/matcher/types.ts @@ -27,7 +27,7 @@ export interface RouteRecordNormalized { /** * {@inheritDoc RouteRecordMultipleViews.components} */ - components: RouteRecordMultipleViews['components'] + components: RouteRecordMultipleViews['components'] | null | undefined /** * {@inheritDoc _RouteRecordBase.components} */ diff --git a/src/navigationGuards.ts b/src/navigationGuards.ts index 1da42085..6ce87477 100644 --- a/src/navigationGuards.ts +++ b/src/navigationGuards.ts @@ -236,6 +236,12 @@ export function extractComponentsGuards( const guards: Array<() => Promise> = [] for (const record of matched) { + if (__DEV__ && !record.components && !record.children.length) { + warn( + `Record with path "${record.path}" is either missing a "component(s)"` + + ` or "children" property.` + ) + } for (const name in record.components) { let rawComponent = record.components[name] if (__DEV__) { @@ -312,7 +318,8 @@ export function extractComponentsGuards( ? resolved.default : resolved // replace the function with the resolved component - record.components[name] = resolvedComponent + // cannot be null or undefined because we went into the for loop + record.components![name] = resolvedComponent // __vccOpts is added by vue-class-component and contain the regular options const options: ComponentOptions = (resolvedComponent as any).__vccOpts || resolvedComponent diff --git a/src/router.ts b/src/router.ts index dee1f9b9..49ccae3c 100644 --- a/src/router.ts +++ b/src/router.ts @@ -50,6 +50,7 @@ import { reactive, unref, computed, + ref, } from 'vue' import { RouteRecord, RouteRecordNormalized } from './matcher/types' import { diff --git a/src/types/index.ts b/src/types/index.ts index 49c43991..786cece5 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -1,6 +1,6 @@ import { LocationQuery, LocationQueryRaw } from '../query' import { PathParserOptions } from '../matcher' -import { Ref, ComponentPublicInstance, Component } from 'vue' +import { Ref, ComponentPublicInstance, Component, DefineComponent } from 'vue' import { RouteRecord, RouteRecordNormalized } from '../matcher/types' import { HistoryState } from '../history/common' import { NavigationFailure } from '../errors' @@ -97,7 +97,7 @@ export type RouteLocationRaw = export interface RouteLocationMatched extends RouteRecordNormalized { // components cannot be Lazy - components: Record + components: Record | null | undefined } /** @@ -182,7 +182,7 @@ export interface RouteLocationNormalized extends _RouteLocationBase { /** * Allowed Component in {@link RouteLocationMatched} */ -export type RouteComponent = Component +export type RouteComponent = Component | DefineComponent /** * Allowed Component definitions in route records provided by the user */ @@ -214,26 +214,26 @@ export interface _RouteRecordBase extends PathParserOptions { * @example `/users/:id` matches `/users/1` as well as `/users/posva`. */ path: string + /** * Where to redirect if the route is directly matched. The redirection happens * before any navigation guard and triggers a new navigation with the new * target location. */ redirect?: RouteRecordRedirectOption - /** - * Array of nested routes. - */ - children?: RouteRecordRaw[] + /** * Aliases for the record. Allows defining extra paths that will behave like a * copy of the record. Allows having paths shorthands like `/users/:id` and * `/u/:id`. All `alias` and `path` values must share the same params. */ alias?: string | string[] + /** * Name for the route record. */ name?: RouteRecordName + /** * Before Enter guard specific to this record. Note `beforeEnter` has no * effect if the record has a `redirect` property. @@ -241,6 +241,7 @@ export interface _RouteRecordBase extends PathParserOptions { beforeEnter?: | NavigationGuardWithThis | NavigationGuardWithThis[] + /** * Arbitrary data attached to the record. */ @@ -287,6 +288,27 @@ export interface RouteRecordSingleView extends _RouteRecordBase { props?: _RouteRecordProps } +/** + * Route Record defining one single component with a nested view. + */ +export interface RouteRecordSingleViewWithChildren extends _RouteRecordBase { + /** + * Component to display when the URL matches this route. + */ + component?: RawRouteComponent | null | undefined + components?: never + + /** + * Array of nested routes. + */ + children: RouteRecordRaw[] + + /** + * Allow passing down params as props to the component rendered by `router-view`. + */ + props?: _RouteRecordProps +} + /** * Route Record defining multiple named components with the `components` option. */ @@ -296,6 +318,27 @@ export interface RouteRecordMultipleViews extends _RouteRecordBase { */ components: Record component?: never + + /** + * Allow passing down params as props to the component rendered by + * `router-view`. Should be an object with the same keys as `components` or a + * boolean to be applied to every component. + */ + props?: Record | boolean +} + +/** + * Route Record defining multiple named components with the `components` option and children. + */ +export interface RouteRecordMultipleViewsWithChildren extends _RouteRecordBase { + /** + * Components to display when the URL matches this route. Allow using named views. + */ + components?: Record | null | undefined + component?: never + + children: RouteRecordRaw[] + /** * Allow passing down params as props to the component rendered by * `router-view`. Should be an object with the same keys as `components` or a @@ -316,7 +359,9 @@ export interface RouteRecordRedirect extends _RouteRecordBase { export type RouteRecordRaw = | RouteRecordSingleView + | RouteRecordSingleViewWithChildren | RouteRecordMultipleViews + | RouteRecordMultipleViewsWithChildren | RouteRecordRedirect /**