]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat(router): allow functional components for routes
authorEduardo San Martin Morote <posva13@gmail.com>
Thu, 7 May 2020 13:59:26 +0000 (15:59 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Thu, 7 May 2020 13:59:26 +0000 (15:59 +0200)
__tests__/lazyLoading.spec.ts
__tests__/warnings.spec.ts
src/install.ts
src/navigationGuards.ts
src/types/index.ts

index df75c71dc4e79d26bd85f2a9854d879bf871144b..429909c5d83d31d9e8d476f700a876f76ec7c40e 100644 (file)
@@ -3,6 +3,7 @@ import { createRouter, createMemoryHistory } from '../src'
 import { RouterOptions } from '../src/router'
 import { RouteComponent } from '../src/types'
 import { ticks } from './utils'
+import { FunctionalComponent, h } from 'vue'
 
 function newRouter(options: Partial<RouterOptions> = {}) {
   let history = createMemoryHistory()
@@ -278,4 +279,15 @@ describe('Lazy Loading', () => {
       matched: [],
     })
   })
+
+  it('works with functional components', async () => {
+    const Functional: FunctionalComponent = () => h('div', 'functional')
+    Functional.displayName = 'Functional'
+
+    const { router } = newRouter({
+      routes: [{ path: '/foo', component: Functional }],
+    })
+
+    await expect(router.push('/foo')).resolves.toBe(undefined)
+  })
 })
index 12f5b455af7bb25a4288302556255a705718c760..c321d38f1945b639f890896afe46f512b1d7fe6c 100644 (file)
@@ -1,6 +1,6 @@
 import { mockWarn } from 'jest-mock-warn'
 import { createMemoryHistory, createRouter } from '../src'
-import { defineComponent } from 'vue'
+import { defineComponent, FunctionalComponent, h } from 'vue'
 
 let component = defineComponent({})
 
@@ -113,4 +113,17 @@ describe('warnings', () => {
 
     router.push('/b')
   })
+
+  it('warns if a non valid function is passed as a component', async () => {
+    const Functional: FunctionalComponent = () => h('div', 'functional')
+    // Functional should have a displayName to avoid the warning
+
+    const router = createRouter({
+      history: createMemoryHistory(),
+      routes: [{ path: '/foo', component: Functional }],
+    })
+
+    await expect(router.push('/foo')).resolves.toBe(undefined)
+    expect('with path "/foo" is a function').toHaveBeenWarned()
+  })
 })
index 619647984103beee7609cd26cf61ea0cb40df675..61f156e117030c9b074171eb89da4c095f73fdec 100644 (file)
@@ -10,6 +10,7 @@ import {
   NavigationGuard,
 } from './types'
 import { routerKey, routeLocationKey } from './injectionSymbols'
+import { warn } from './warning'
 
 declare module '@vue/runtime-core' {
   interface ComponentCustomOptions {
@@ -86,8 +87,7 @@ export function applyRouterPlugin(app: App, router: Router) {
     // @ts-ignore: see above
     router._started = true
     router.push(router.history.location.fullPath).catch(err => {
-      if (__DEV__)
-        console.error('Unhandled error when starting the router', err)
+      if (__DEV__) warn('Unexpected error when starting the router:', err)
     })
   }
 
index d906e36504e0846b495d05138807cff6b4951667..9695dd718ad897ccf07ae0fa71415761c8c58926 100644 (file)
@@ -8,6 +8,7 @@ import {
   isRouteLocation,
   Lazy,
   RouteComponent,
+  RawRouteComponent,
 } from './types'
 
 import {
@@ -16,7 +17,7 @@ import {
   NavigationFailure,
   NavigationRedirectError,
 } from './errors'
-import { ComponentPublicInstance } from 'vue'
+import { ComponentPublicInstance, ComponentOptions } from 'vue'
 import { inject, getCurrentInstance, warn } from 'vue'
 import { matchedRouteKey } from './injectionSymbols'
 import { RouteRecordNormalized } from './matcher/types'
@@ -166,11 +167,28 @@ export function extractComponentsGuards(
   for (const record of matched) {
     for (const name in record.components) {
       const rawComponent = record.components[name]
-      if (typeof rawComponent === 'function') {
+      if (isRouteComponent(rawComponent)) {
+        // __vccOpts is added by vue-class-component and contain the regular options
+        let options: ComponentOptions =
+          (rawComponent as any).__vccOpts || rawComponent
+        const guard = options[guardType]
+        guard &&
+          guards.push(guardToPromiseFn(guard, to, from, record.instances[name]))
+      } else {
         // start requesting the chunk already
-        const componentPromise = (rawComponent as Lazy<RouteComponent>)().catch(
-          () => null
-        )
+        let componentPromise: Promise<RouteComponent | null> = (rawComponent as Lazy<
+          RouteComponent
+        >)()
+
+        if (__DEV__ && !('catch' in componentPromise)) {
+          warn(
+            `Component "${name}" at record with path "${record.path}" is a function that does not return a Promise. If you were passing a functional component, make sure to add a "displayName" to the component. This will break in production if not fixed.`
+          )
+          componentPromise = Promise.resolve(componentPromise as RouteComponent)
+        } else {
+          componentPromise = componentPromise.catch(() => null)
+        }
+
         guards.push(() =>
           componentPromise.then(resolved => {
             if (!resolved)
@@ -187,20 +205,29 @@ export function extractComponentsGuards(
             // @ts-ignore: the options types are not propagated to Component
             const guard: NavigationGuard = resolvedComponent[guardType]
             return (
-              // @ts-ignore: the guards matched the instance type
               guard &&
               guardToPromiseFn(guard, to, from, record.instances[name])()
             )
           })
         )
-      } else {
-        const guard = rawComponent[guardType]
-        guard &&
-          // @ts-ignore: the guards matched the instance type
-          guards.push(guardToPromiseFn(guard, to, from, record.instances[name]))
       }
     }
   }
 
   return guards
 }
+
+/**
+ * Allows differentiating lazy components from functional components and vue-class-component
+ * @param component
+ */
+function isRouteComponent(
+  component: RawRouteComponent
+): component is RouteComponent {
+  return (
+    typeof component === 'object' ||
+    'displayName' in component ||
+    'props' in component ||
+    '__vccOpts' in component
+  )
+}
index d15753d5d46ae0cc4df94705ec897ba2c8f3d1b4..04fb2f7d47d2496b92847be38be5de2d3eca00f9 100644 (file)
@@ -1,6 +1,6 @@
 import { LocationQuery, LocationQueryRaw } from '../query'
 import { PathParserOptions } from '../matcher'
-import { Ref, ComputedRef, ComponentOptions } from 'vue'
+import { Ref, ComputedRef, Component } from 'vue'
 import { RouteRecord, RouteRecordNormalized } from '../matcher/types'
 import { HistoryState } from '../history/common'
 import { NavigationFailure } from '../errors'
@@ -135,7 +135,7 @@ export interface RouteLocationNormalized extends _RouteLocationBase {
   matched: RouteRecordNormalized[] // non-enumerable
 }
 
-export type RouteComponent = ComponentOptions
+export type RouteComponent = Component
 export type RawRouteComponent = RouteComponent | Lazy<RouteComponent>
 
 export type RouteRecordName = string | symbol