From: Eduardo San Martin Morote Date: Thu, 7 May 2020 13:59:26 +0000 (+0200) Subject: feat(router): allow functional components for routes X-Git-Tag: v4.0.0-alpha.11~20 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=096d86498e954345c6bd4d8e82fe54c37d3f869b;p=thirdparty%2Fvuejs%2Frouter.git feat(router): allow functional components for routes --- diff --git a/__tests__/lazyLoading.spec.ts b/__tests__/lazyLoading.spec.ts index df75c71d..429909c5 100644 --- a/__tests__/lazyLoading.spec.ts +++ b/__tests__/lazyLoading.spec.ts @@ -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 = {}) { 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) + }) }) diff --git a/__tests__/warnings.spec.ts b/__tests__/warnings.spec.ts index 12f5b455..c321d38f 100644 --- a/__tests__/warnings.spec.ts +++ b/__tests__/warnings.spec.ts @@ -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() + }) }) diff --git a/src/install.ts b/src/install.ts index 61964798..61f156e1 100644 --- a/src/install.ts +++ b/src/install.ts @@ -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) }) } diff --git a/src/navigationGuards.ts b/src/navigationGuards.ts index d906e365..9695dd71 100644 --- a/src/navigationGuards.ts +++ b/src/navigationGuards.ts @@ -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)().catch( - () => null - ) + let componentPromise: Promise = (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 + ) +} diff --git a/src/types/index.ts b/src/types/index.ts index d15753d5..04fb2f7d 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, 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 export type RouteRecordName = string | symbol