Skip to content

Commit

Permalink
fix(guards): call beforeRouteEnter once per named view
Browse files Browse the repository at this point in the history
  • Loading branch information
posva committed Jul 21, 2020
1 parent c481d3a commit f2846ff
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 38 deletions.
24 changes: 12 additions & 12 deletions __tests__/RouterView.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ const routes = createRoutes({
{
components: { default: components.Home },
instances: {},
enterCallbacks: [],
enterCallbacks: {},
path: '/',
props,
},
Expand All @@ -57,7 +57,7 @@ const routes = createRoutes({
{
components: { default: components.Foo },
instances: {},
enterCallbacks: [],
enterCallbacks: {},
path: '/foo',
props,
},
Expand All @@ -75,14 +75,14 @@ const routes = createRoutes({
{
components: { default: components.Nested },
instances: {},
enterCallbacks: [],
enterCallbacks: {},
path: '/',
props,
},
{
components: { default: components.Foo },
instances: {},
enterCallbacks: [],
enterCallbacks: {},
path: 'a',
props,
},
Expand All @@ -100,21 +100,21 @@ const routes = createRoutes({
{
components: { default: components.Nested },
instances: {},
enterCallbacks: [],
enterCallbacks: {},
path: '/',
props,
},
{
components: { default: components.Nested },
instances: {},
enterCallbacks: [],
enterCallbacks: {},
path: 'a',
props,
},
{
components: { default: components.Foo },
instances: {},
enterCallbacks: [],
enterCallbacks: {},
path: 'b',
props,
},
Expand All @@ -132,7 +132,7 @@ const routes = createRoutes({
{
components: { foo: components.Foo },
instances: {},
enterCallbacks: [],
enterCallbacks: {},
path: '/',
props,
},
Expand All @@ -151,7 +151,7 @@ const routes = createRoutes({
components: { default: components.User },

instances: {},
enterCallbacks: [],
enterCallbacks: {},
path: '/users/:id',
props: { default: true },
},
Expand All @@ -170,7 +170,7 @@ const routes = createRoutes({
components: { default: components.WithProps },

instances: {},
enterCallbacks: [],
enterCallbacks: {},
path: '/props/:id',
props: { default: { id: 'foo', other: 'fixed' } },
},
Expand All @@ -190,7 +190,7 @@ const routes = createRoutes({
components: { default: components.WithProps },

instances: {},
enterCallbacks: [],
enterCallbacks: {},
path: '/props/:id',
props: {
default: (to: RouteLocationNormalized) => ({
Expand Down Expand Up @@ -263,7 +263,7 @@ describe('RouterView', () => {
{
components: { default: components.User },
instances: {},
enterCallbacks: [],
enterCallbacks: {},
path: '/users/:id',
props,
},
Expand Down
94 changes: 94 additions & 0 deletions __tests__/guards/beforeRouteEnterCallback.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/**
* @jest-environment jsdom
*/
import { defineComponent, h } from 'vue'
import { mount } from '../mount'
import {
createRouter,
RouterView,
createMemoryHistory,
RouterOptions,
} from '../../src'

const nextCallbacks = {
Default: jest.fn(),
Other: jest.fn(),
}
const Default = defineComponent({
beforeRouteEnter(to, from, next) {
next(nextCallbacks.Default)
},
name: 'Default',
setup() {
return () => h('div', 'Default content')
},
})

const Other = defineComponent({
beforeRouteEnter(to, from, next) {
next(nextCallbacks.Other)
},
name: 'Other',
setup() {
return () => h('div', 'Other content')
},
})

const Third = defineComponent({
name: 'Third',
setup() {
return () => h('div', 'Third content')
},
})

beforeEach(() => {
for (const key in nextCallbacks) {
nextCallbacks[key as keyof typeof nextCallbacks].mockClear()
}
})

describe('beforeRouteEnter next callback', () => {
async function factory(options: Partial<RouterOptions>) {
const history = createMemoryHistory()
const router = createRouter({
history,
routes: [],
...options,
})

const wrapper = await mount(
{
template: `
<div>
<router-view/>
<router-view name="other"/>
</div>
`,
components: { RouterView },
},
{ router }
)

return { wrapper, router }
}

it('calls each beforeRouteEnter callback once', async () => {
const { router } = await factory({
routes: [
{
path: '/:p(.*)',
components: {
default: Default,
other: Other,
third: Third,
},
},
],
})

await router.isReady()

expect(nextCallbacks.Default).toHaveBeenCalledTimes(1)
expect(nextCallbacks.Other).toHaveBeenCalledTimes(1)
})
})
4 changes: 4 additions & 0 deletions __tests__/mount.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ import { compile } from '@vue/compiler-dom'
import * as runtimeDom from '@vue/runtime-dom'
import { RouteLocationNormalizedLoose } from './utils'
import { routeLocationKey } from '../src/injectionSymbols'
import { Router } from '../src'

export interface MountOptions {
propsData: Record<string, any>
provide: Record<string | symbol, any>
components: ComponentOptions['components']
slots: Record<string, string>
router?: Router
}

interface Wrapper {
Expand Down Expand Up @@ -134,6 +136,8 @@ export function mount(
return rootEl.querySelector(selector)
}

if (options.router) app.use(options.router)

app.mount(rootEl)

activeWrapperRemovers.push(() => {
Expand Down
2 changes: 1 addition & 1 deletion __tests__/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ export interface RouteRecordViewLoose
> {
leaveGuards?: any
instances: Record<string, any>
enterCallbacks: Function[]
enterCallbacks: Record<string, Function[]>
props: Record<string, _RouteRecordProps>
aliasOf: RouteRecordViewLoose | undefined
}
Expand Down
2 changes: 1 addition & 1 deletion src/RouterView.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ export const RouterViewImpl = defineComponent({
const currentName = props.name
const onVnodeMounted = () => {
matchedRoute.instances[currentName] = viewRef.value
matchedRoute.enterCallbacks.forEach(callback =>
;(matchedRoute.enterCallbacks[currentName] || []).forEach(callback =>
callback(viewRef.value!)
)
}
Expand Down
2 changes: 1 addition & 1 deletion src/matcher/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -321,7 +321,7 @@ export function normalizeRouteRecord(
instances: {},
leaveGuards: [],
updateGuards: [],
enterCallbacks: [],
enterCallbacks: {},
components:
'components' in record
? record.components || {}
Expand Down
2 changes: 1 addition & 1 deletion src/matcher/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ export interface RouteRecordNormalized {
beforeEnter: RouteRecordMultipleViews['beforeEnter']
leaveGuards: NavigationGuard[]
updateGuards: NavigationGuard[]
enterCallbacks: NavigationGuardNextCallback[]
enterCallbacks: Record<string, NavigationGuardNextCallback[]>
// having the instances on the record mean beforeRouteUpdate and
// beforeRouteLeave guards can only be invoked with the latest mounted app
// instance if there are multiple application instances rendering the same
Expand Down
47 changes: 26 additions & 21 deletions src/navigationGuards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
NavigationFailure,
NavigationRedirectError,
} from './errors'
import { ComponentPublicInstance, ComponentOptions } from 'vue'
import { ComponentOptions } from 'vue'
import { inject, getCurrentInstance, warn } from 'vue'
import { matchedRouteKey } from './injectionSymbols'
import { RouteRecordNormalized } from './matcher/types'
Expand Down Expand Up @@ -87,15 +87,31 @@ export function onBeforeRouteUpdate(updateGuard: NavigationGuard) {
)
}

export function guardToPromiseFn(
guard: NavigationGuard,
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded
): () => Promise<void>
export function guardToPromiseFn(
guard: NavigationGuard,
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded,
instance?: ComponentPublicInstance | undefined | null,
record?: RouteRecordNormalized
record: RouteRecordNormalized,
name: string
): () => Promise<void>
export function guardToPromiseFn(
guard: NavigationGuard,
to: RouteLocationNormalized,
from: RouteLocationNormalizedLoaded,
record?: RouteRecordNormalized,
name?: string
): () => Promise<void> {
// keep a reference to the enterCallbackArray to prevent pushing callbacks if a new navigation took place
const enterCallbackArray = record && record.enterCallbacks
const enterCallbackArray =
record &&
// name is defined if record is because of the function overload
(record.enterCallbacks[name!] = record.enterCallbacks[name!] || [])

return () =>
new Promise((resolve, reject) => {
const next: NavigationGuardNext = (
Expand Down Expand Up @@ -125,8 +141,9 @@ export function guardToPromiseFn(
)
} else {
if (
record &&
record.enterCallbacks === enterCallbackArray &&
enterCallbackArray &&
// since enterCallbackArray is truthy, both record and name also are
record!.enterCallbacks[name!] === enterCallbackArray &&
typeof valid === 'function'
)
enterCallbackArray.push(valid)
Expand All @@ -137,7 +154,7 @@ export function guardToPromiseFn(
// wrapping with Promise.resolve allows it to work with both async and sync guards
Promise.resolve(
guard.call(
instance,
record && record.instances[name!],
to,
from,
__DEV__ ? canOnlyBeCalledOnce(next, to, from) : next
Expand Down Expand Up @@ -188,10 +205,7 @@ export function extractComponentsGuards(
let options: ComponentOptions =
(rawComponent as any).__vccOpts || rawComponent
const guard = options[guardType]
guard &&
guards.push(
guardToPromiseFn(guard, to, from, record.instances[name], record)
)
guard && guards.push(guardToPromiseFn(guard, to, from, record, name))
} else {
// start requesting the chunk already
let componentPromise: Promise<RouteComponent | null> = (rawComponent as Lazy<
Expand Down Expand Up @@ -222,16 +236,7 @@ export function extractComponentsGuards(
record.components[name] = resolvedComponent
// @ts-ignore: the options types are not propagated to Component
const guard: NavigationGuard = resolvedComponent[guardType]
return (
guard &&
guardToPromiseFn(
guard,
to,
from,
record.instances[name],
record
)()
)
return guard && guardToPromiseFn(guard, to, from, record, name)()
})
)
}
Expand Down
3 changes: 2 additions & 1 deletion src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -625,7 +625,7 @@ export function createRouter(options: RouterOptions): Router {
// NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>

// clear existing enterCallbacks, these are added by extractComponentsGuards
to.matched.forEach(record => (record.enterCallbacks = []))
to.matched.forEach(record => (record.enterCallbacks = {}))

// check in-component beforeRouteEnter
guards = extractComponentsGuards(
Expand Down Expand Up @@ -693,6 +693,7 @@ export function createRouter(options: RouterOptions): Router {
record.leaveGuards = []
// free the references
record.instances = {}
record.enterCallbacks = {}
}

// only consider as push if it's not the first navigation
Expand Down

0 comments on commit f2846ff

Please sign in to comment.