Skip to content

Commit

Permalink
feat(router): allow functional components for routes
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed May 7, 2020
1 parent 24d3d49 commit 096d864
Show file tree
Hide file tree
Showing 5 changed files with 68 additions and 16 deletions.
12 changes: 12 additions & 0 deletions __tests__/lazyLoading.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down Expand Up @@ -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)
})
})
15 changes: 14 additions & 1 deletion __tests__/warnings.spec.ts
Original file line number Diff line number Diff line change
@@ -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({})

Expand Down Expand Up @@ -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()
})
})
4 changes: 2 additions & 2 deletions src/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
NavigationGuard,
} from './types'
import { routerKey, routeLocationKey } from './injectionSymbols'
import { warn } from './warning'

declare module '@vue/runtime-core' {
interface ComponentCustomOptions {
Expand Down Expand Up @@ -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)
})
}

Expand Down
49 changes: 38 additions & 11 deletions src/navigationGuards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
isRouteLocation,
Lazy,
RouteComponent,
RawRouteComponent,
} from './types'

import {
Expand All @@ -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'
Expand Down Expand Up @@ -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)
Expand All @@ -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
)
}
4 changes: 2 additions & 2 deletions src/types/index.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 096d864

Please sign in to comment.