]> git.ipfire.org Git - thirdparty/vuejs/router.git/commitdiff
feat: call beforeRouteLeave
authorEduardo San Martin Morote <posva13@gmail.com>
Thu, 2 May 2019 10:05:50 +0000 (12:05 +0200)
committerEduardo San Martin Morote <posva13@gmail.com>
Thu, 2 May 2019 10:05:50 +0000 (12:05 +0200)
__tests__/guards/component-beforeRouteLeave.spec.js [new file with mode: 0644]
explorations/html5.ts
src/router.ts
src/types/index.ts
tsconfig.json

diff --git a/__tests__/guards/component-beforeRouteLeave.spec.js b/__tests__/guards/component-beforeRouteLeave.spec.js
new file mode 100644 (file)
index 0000000..ad25fde
--- /dev/null
@@ -0,0 +1,73 @@
+// @ts-check
+require('../helper')
+const expect = require('expect')
+const { HTML5History } = require('../../src/history/html5')
+const { Router } = require('../../src/router')
+const fakePromise = require('faked-promise')
+const { NAVIGATION_TYPES, createDom } = require('../utils')
+
+/**
+ * @param {Partial<import('../../src/router').RouterOptions> & { routes: import('../../src/types').RouteRecord[]}} options
+ */
+function createRouter(options) {
+  return new Router({
+    history: new HTML5History(),
+    ...options,
+  })
+}
+
+const Home = { template: `<div>Home</div>` }
+const Foo = { template: `<div>Foo</div>` }
+
+const beforeRouteLeave = jest.fn()
+/** @type {import('../../src/types').RouteRecord[]} */
+const routes = [
+  { path: '/', component: Home },
+  { path: '/foo', component: Foo },
+  {
+    path: '/guard',
+    component: {
+      ...Foo,
+      beforeRouteLeave,
+    },
+  },
+]
+
+beforeEach(() => {
+  beforeRouteLeave.mockReset()
+})
+
+describe('beforeRouteLeave', () => {
+  beforeAll(() => {
+    createDom()
+  })
+
+  NAVIGATION_TYPES.forEach(navigationMethod => {
+    describe(navigationMethod, () => {
+      it('calls beforeRouteLeave guard on navigation', async () => {
+        const router = createRouter({ routes })
+        beforeRouteLeave.mockImplementationOnce((to, from, next) => {
+          if (to.path === 'foo') next(false)
+          else next()
+        })
+        await router.push('/guard')
+        expect(beforeRouteLeave).not.toHaveBeenCalled()
+
+        await router[navigationMethod]('/foo')
+        expect(beforeRouteLeave).toHaveBeenCalledTimes(1)
+      })
+
+      it('can cancel navigation', async () => {
+        const router = createRouter({ routes })
+        beforeRouteLeave.mockImplementationOnce(async (to, from, next) => {
+          next(false)
+        })
+        await router.push('/guard')
+        const p = router[navigationMethod]('/')
+        expect(router.currentRoute.fullPath).toBe('/guard')
+        await p.catch(err => {}) // catch the navigation abortion
+        expect(router.currentRoute.fullPath).toBe('/guard')
+      })
+    })
+  })
+})
index 0028a17d3fbc1bf0206b4e3160592f03e48eac0a..c40800808de59cff9b3d6ad6de68e73736dbdfb1 100644 (file)
@@ -1,9 +1,20 @@
 import { Router, HTML5History } from '../src'
+import { RouteComponent } from '../src/types'
 
-const component = {
+const component: RouteComponent = {
   template: `<div>A component</div>`,
 }
 
+const GuardedWithLeave: RouteComponent = {
+  template: `<div>
+    <p>try to leave</p>
+  </div>`,
+  beforeRouteLeave(to, from, next) {
+    if (window.confirm()) next()
+    else next(false)
+  },
+}
+
 const r = new Router({
   history: new HTML5History(),
   routes: [
@@ -14,11 +25,12 @@ const r = new Router({
       path: '/with-guard/:n',
       name: 'guarded',
       component,
-      beforeEnter: (to, from, next) => {
+      beforeEnter(to, from, next) {
         if (to.params.n !== 'valid') next(false)
         next()
       },
     },
+    { path: '/cant-leave', component: GuardedWithLeave },
     // { path: /^\/about\/?$/, component },
   ],
 })
index 4ba45d1d29b3e4da47316d0ffd1792bd5289a02c..1090913914587234636cbf9c3dd09f2c3ffa27ca 100644 (file)
@@ -100,7 +100,30 @@ export class Router {
     // elements and other stuff
     let guards: Array<() => Promise<any>> = []
 
+    // TODO: ensure we are leaving since we could just be changing params or not changing anything
+    // TODO: is it okay to resolve all matched component or should we do it in order
+    await Promise.all(
+      from.matched.map(async ({ component }) => {
+        // TODO: cache async routes per record
+        const resolvedComponent = await (typeof component === 'function'
+          ? component()
+          : component)
+        if (resolvedComponent.beforeRouteLeave) {
+          // TODO: handle the next callback
+          guards.push(
+            guardToPromiseFn(resolvedComponent.beforeRouteLeave, to, from)
+          )
+        }
+      })
+    )
+
+    // run the queue of per route beforeEnter guards
+    for (const guard of guards) {
+      await guard()
+    }
+
     // check global guards first
+    guards = []
     for (const guard of this.beforeGuards) {
       guards.push(guardToPromiseFn(guard, to, from))
     }
index 536743d92d1c58a09f121a60e1f87737f3df3d34..078bb9c55700cb36e7c1293ada1f1982e4a996bb 100644 (file)
@@ -58,8 +58,10 @@ export interface RouteLocationNormalized
 //   params: ReturnType<PT>
 // }
 
+// TODO: type this for beforeRouteUpdate and beforeRouteLeave
 export interface RouteComponentInterface {
   beforeRouteEnter?: NavigationGuard
+  beforeRouteLeave?: NavigationGuard<void>
 }
 // TODO: have a real type with augmented properties
 // export type RouteComponent = TODO & RouteComponentInterface
@@ -120,8 +122,10 @@ export interface NavigationGuardCallback {
   (): void
   (valid: false): void
 }
-export interface NavigationGuard {
+
+export interface NavigationGuard<V = void> {
   (
+    this: V,
     to: RouteLocationNormalized,
     from: RouteLocationNormalized,
     next: NavigationGuardCallback
index 02bf6e575f22638c56ef1a1ff5994caa10ee2f33..6331e3a745d07e035c73ab8094c775ac9b2a2110 100644 (file)
@@ -1,5 +1,5 @@
 {
-  "include": ["src/**/*.ts"],
+  "include": ["src/**/*.ts", "explorations/**/*.ts"],
   "compilerOptions": {
     /* Basic Options */
     "target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */,