]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat(view): handle empty components as pass through
authorEduardo San Martin Morote <posva13@gmail.com>
Thu, 4 Nov 2021 17:37:23 +0000 (18:37 +0100)
committerEduardo San Martin Morote <posva@users.noreply.github.com>
Thu, 30 Jun 2022 07:59:00 +0000 (09:59 +0200)
__tests__/RouterView.spec.ts
__tests__/guards/extractComponentsGuards.spec.ts
__tests__/matcher/resolve.spec.ts
__tests__/utils.ts
src/RouterView.ts
src/injectionSymbols.ts
src/matcher/index.ts
src/matcher/types.ts
src/navigationGuards.ts
src/router.ts
src/types/index.ts

index 2ba6c6c9363928ab84d1c1be0fca7f9a742cf7a2..4d44fc3e910005c1f773a73aa17632741c089e3e 100644 (file)
@@ -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(`<div>id:2;other:page</div>`)
   })
 
+  it('pass through with empty children', async () => {
+    const { wrapper } = await factory(routes.passthrough)
+    expect(wrapper.html()).toBe(`<div>Foo</div>`)
+  })
+
   describe('warnings', () => {
     it('does not warn RouterView is wrapped', () => {
       const route = createMockedRoute(routes.root)
index 8df52b48beb0004660ac54875215bc264746ae4a..64d61ca100bfbf514bd29e99747a1920a33a8658 100644 (file)
@@ -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 () => {
index e26829be1fea9b562c5484410701bc0c9c526c6c..c286a76239c445b01fa702e59bcc16781921c0b7 100644 (file)
@@ -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` }],
         }
       )
     })
index 8eddc12cca9bb4f022b307488afd0c2e0096bf68..35f7d6334de41c7df02fc0c0c2ba236ccf50233c 100644 (file)
@@ -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<string, any>
   enterCallbacks: Record<string, Function[]>
   props: Record<string, _RouteRecordProps>
   aliasOf: RouteRecordViewLoose | undefined
+  children?: RouteRecordViewLoose[]
+  components: Record<string, RouteComponent> | null | undefined
 }
 
 // @ts-expect-error we are intentionally overriding the type
index d8d48fc64d03f24bdac1ece8a326232383995376..4c2eca482c0f48a6762e09364c9ce683a67c24bf 100644 (file)
@@ -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<number>(() => {
+      let initialDepth = unref(injectedDepth)
+      const { matched } = routeToDisplay.value
+      let matchedRoute: RouteLocationMatched | undefined
+      while (
+        (matchedRoute = matched[initialDepth]) &&
+        !matchedRoute.components
+      ) {
+        initialDepth++
+      }
+      return initialDepth
+    })
     const matchedRouteRef = computed<RouteLocationMatched | undefined>(
-      () => 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,
index 0d345ea1f9ad12ba7df2027675cfa21c81d8520f..60838fdff705ca7e05fa368aa7b2ef12405c6fd6 100644 (file)
@@ -32,7 +32,7 @@ export const matchedRouteKey = /*#__PURE__*/ PolySymbol(
  */
 export const viewDepthKey = /*#__PURE__*/ PolySymbol(
   __DEV__ ? 'router view depth' : 'rvd'
-) as InjectionKey<number>
+) as InjectionKey<Ref<number> | number>
 
 /**
  * Allows overriding the router instance returned by `useRouter` in tests. r
index ca31e86fd48513c7651bfd5339afd01d0fad89e4..ce0bce664455c139ed6623d6011955354058b004 100644 (file)
@@ -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 },
   }
 }
 
index 3939e9354503d80e960138c04ccf68c1dc451025..a0c33b36175a62730be503580fe3a3bf21a78ce0 100644 (file)
@@ -27,7 +27,7 @@ export interface RouteRecordNormalized {
   /**
    * {@inheritDoc RouteRecordMultipleViews.components}
    */
-  components: RouteRecordMultipleViews['components']
+  components: RouteRecordMultipleViews['components'] | null | undefined
   /**
    * {@inheritDoc _RouteRecordBase.components}
    */
index 1da420853c1538eae32f09bda59604b165688061..6ce874779434883ee269012af200b18d96393dc8 100644 (file)
@@ -236,6 +236,12 @@ export function extractComponentsGuards(
   const guards: Array<() => Promise<void>> = []
 
   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
index dee1f9b9df4af4e993b91a4768d35395ff64a253..49ccae3cce80953e8743a44edc02fe47963223a0 100644 (file)
@@ -50,6 +50,7 @@ import {
   reactive,
   unref,
   computed,
+  ref,
 } from 'vue'
 import { RouteRecord, RouteRecordNormalized } from './matcher/types'
 import {
index 49c4399151c67a2d7040a7db56af45ca54df1171..786cece51618147abc6848413d08a9d15963a0dd 100644 (file)
@@ -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<RouteComponent>
-  components: Record<string, RouteComponent>
+  components: Record<string, RouteComponent> | 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<undefined>
     | NavigationGuardWithThis<undefined>[]
+
   /**
    * 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<string, RawRouteComponent>
   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<string, _RouteRecordProps> | 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<string, RawRouteComponent> | 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
 
 /**