]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
fix(guards): call beforeRouteEnter once per named view
authorEduardo San Martin Morote <posva13@gmail.com>
Tue, 21 Jul 2020 11:41:24 +0000 (13:41 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Tue, 21 Jul 2020 11:41:24 +0000 (13:41 +0200)
__tests__/RouterView.spec.ts
__tests__/guards/beforeRouteEnterCallback.spec.ts [new file with mode: 0644]
__tests__/mount.ts
__tests__/utils.ts
src/RouterView.ts
src/matcher/index.ts
src/matcher/types.ts
src/navigationGuards.ts
src/router.ts

index a23c5dd497f81ce812d23e74f3207644ff0d2129..2f2ba4c310dc9ef0642b8d938abf51ec355e4aaa 100644 (file)
@@ -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 (file)
index 0000000..b223c77
--- /dev/null
@@ -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)
+  })
+})
index 1ad19cf20ec1fe9a39fb5ea57a4a27c4ef72bc74..d47afd403574a011cd642a782a71de035acbf320 100644 (file)
@@ -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 {
@@ -134,6 +136,8 @@ export function mount(
       return rootEl.querySelector(selector)
     }
 
+    if (options.router) app.use(options.router)
+
     app.mount(rootEl)
 
     activeWrapperRemovers.push(() => {
index 719823ed7412e2f50010c1a6dd3ab0449b06db49..6b7812f7a2bd3d1927b9cfa9dbb0787cb35116b3 100644 (file)
@@ -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
 }
index f51fc953d4f1a91e4fdd574e3528f668a77a8e49..0b15686ab7d3e933b3e7e731a1a77abc4f804bec 100644 (file)
@@ -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!)
         )
       }
index 4b4d626950c31ac46a0718b373be58f61e927496..cf2d92134c1ee01bbe28485f16a3d97e7d60080a 100644 (file)
@@ -321,7 +321,7 @@ export function normalizeRouteRecord(
     instances: {},
     leaveGuards: [],
     updateGuards: [],
-    enterCallbacks: [],
+    enterCallbacks: {},
     components:
       'components' in record
         ? record.components || {}
index ffef55f4f587fa356ea794a1b9dc2c343f07f347..726f8fe3be95221d4f68a7d8ce60ca0fcca35efd 100644 (file)
@@ -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
index 0585a189eea9ddb77a17ded567d888f68d361468..53bee1046c320ae9e33002c83a8bb90358ac0cd6 100644 (file)
@@ -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<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 = (
@@ -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<RouteComponent | null> = (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)()
           })
         )
       }
index b7e1b9711ebcf7a36482c1513740657ef58894be..8674405a3d7b0bcb15084f202c9f16e3d99f492f 100644 (file)
@@ -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(
@@ -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