--- /dev/null
+// @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')
+ })
+ })
+ })
+})
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: [
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 },
],
})
// 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))
}
// 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
(): void
(valid: false): void
}
-export interface NavigationGuard {
+
+export interface NavigationGuard<V = void> {
(
+ this: V,
to: RouteLocationNormalized,
from: RouteLocationNormalized,
next: NavigationGuardCallback
{
- "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'. */,