From: Eduardo San Martin Morote Date: Tue, 21 Jul 2020 11:41:24 +0000 (+0200) Subject: fix(guards): call beforeRouteEnter once per named view X-Git-Tag: v4.0.0-beta.3~12 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=f2846ff2a0796e58a9b04593909f7a30b7b68bb1;p=thirdparty%2Fvuejs%2Frouter.git fix(guards): call beforeRouteEnter once per named view --- diff --git a/__tests__/RouterView.spec.ts b/__tests__/RouterView.spec.ts index a23c5dd4..2f2ba4c3 100644 --- a/__tests__/RouterView.spec.ts +++ b/__tests__/RouterView.spec.ts @@ -39,7 +39,7 @@ const routes = createRoutes({ { components: { default: components.Home }, instances: {}, - enterCallbacks: [], + enterCallbacks: {}, path: '/', props, }, @@ -57,7 +57,7 @@ const routes = createRoutes({ { components: { default: components.Foo }, instances: {}, - enterCallbacks: [], + enterCallbacks: {}, path: '/foo', props, }, @@ -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, }, @@ -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, }, @@ -132,7 +132,7 @@ const routes = createRoutes({ { components: { foo: components.Foo }, instances: {}, - enterCallbacks: [], + enterCallbacks: {}, path: '/', props, }, @@ -151,7 +151,7 @@ const routes = createRoutes({ components: { default: components.User }, instances: {}, - enterCallbacks: [], + enterCallbacks: {}, path: '/users/:id', props: { default: true }, }, @@ -170,7 +170,7 @@ const routes = createRoutes({ components: { default: components.WithProps }, instances: {}, - enterCallbacks: [], + enterCallbacks: {}, path: '/props/:id', props: { default: { id: 'foo', other: 'fixed' } }, }, @@ -190,7 +190,7 @@ const routes = createRoutes({ components: { default: components.WithProps }, instances: {}, - enterCallbacks: [], + enterCallbacks: {}, path: '/props/:id', props: { default: (to: RouteLocationNormalized) => ({ @@ -263,7 +263,7 @@ describe('RouterView', () => { { components: { default: components.User }, instances: {}, - enterCallbacks: [], + enterCallbacks: {}, path: '/users/:id', props, }, diff --git a/__tests__/guards/beforeRouteEnterCallback.spec.ts b/__tests__/guards/beforeRouteEnterCallback.spec.ts new file mode 100644 index 00000000..b223c770 --- /dev/null +++ b/__tests__/guards/beforeRouteEnterCallback.spec.ts @@ -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) { + const history = createMemoryHistory() + const router = createRouter({ + history, + routes: [], + ...options, + }) + + const wrapper = await mount( + { + template: ` +
+ + +
+ `, + 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) + }) +}) diff --git a/__tests__/mount.ts b/__tests__/mount.ts index 1ad19cf2..d47afd40 100644 --- a/__tests__/mount.ts +++ b/__tests__/mount.ts @@ -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 provide: Record components: ComponentOptions['components'] slots: Record + router?: Router } interface Wrapper { @@ -134,6 +136,8 @@ export function mount( return rootEl.querySelector(selector) } + if (options.router) app.use(options.router) + app.mount(rootEl) activeWrapperRemovers.push(() => { diff --git a/__tests__/utils.ts b/__tests__/utils.ts index 719823ed..6b7812f7 100644 --- a/__tests__/utils.ts +++ b/__tests__/utils.ts @@ -53,7 +53,7 @@ export interface RouteRecordViewLoose > { leaveGuards?: any instances: Record - enterCallbacks: Function[] + enterCallbacks: Record props: Record aliasOf: RouteRecordViewLoose | undefined } diff --git a/src/RouterView.ts b/src/RouterView.ts index f51fc953..0b15686a 100644 --- a/src/RouterView.ts +++ b/src/RouterView.ts @@ -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!) ) } diff --git a/src/matcher/index.ts b/src/matcher/index.ts index 4b4d6269..cf2d9213 100644 --- a/src/matcher/index.ts +++ b/src/matcher/index.ts @@ -321,7 +321,7 @@ export function normalizeRouteRecord( instances: {}, leaveGuards: [], updateGuards: [], - enterCallbacks: [], + enterCallbacks: {}, components: 'components' in record ? record.components || {} diff --git a/src/matcher/types.ts b/src/matcher/types.ts index ffef55f4..726f8fe3 100644 --- a/src/matcher/types.ts +++ b/src/matcher/types.ts @@ -22,7 +22,7 @@ export interface RouteRecordNormalized { beforeEnter: RouteRecordMultipleViews['beforeEnter'] leaveGuards: NavigationGuard[] updateGuards: NavigationGuard[] - enterCallbacks: NavigationGuardNextCallback[] + enterCallbacks: Record // 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 diff --git a/src/navigationGuards.ts b/src/navigationGuards.ts index 0585a189..53bee104 100644 --- a/src/navigationGuards.ts +++ b/src/navigationGuards.ts @@ -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' @@ -87,15 +87,31 @@ export function onBeforeRouteUpdate(updateGuard: NavigationGuard) { ) } +export function guardToPromiseFn( + guard: NavigationGuard, + to: RouteLocationNormalized, + from: RouteLocationNormalizedLoaded +): () => Promise export function guardToPromiseFn( guard: NavigationGuard, to: RouteLocationNormalized, from: RouteLocationNormalizedLoaded, - instance?: ComponentPublicInstance | undefined | null, - record?: RouteRecordNormalized + record: RouteRecordNormalized, + name: string +): () => Promise +export function guardToPromiseFn( + guard: NavigationGuard, + to: RouteLocationNormalized, + from: RouteLocationNormalizedLoaded, + record?: RouteRecordNormalized, + name?: string ): () => Promise { // 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 = ( @@ -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) @@ -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 @@ -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 = (rawComponent as Lazy< @@ -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)() }) ) } diff --git a/src/router.ts b/src/router.ts index b7e1b971..8674405a 100644 --- a/src/router.ts +++ b/src/router.ts @@ -625,7 +625,7 @@ export function createRouter(options: RouterOptions): Router { // NOTE: at this point to.matched is normalized and does not contain any () => Promise // 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( @@ -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