From: Eduardo San Martin Morote Date: Fri, 3 May 2019 19:43:14 +0000 (+0200) Subject: feat(matcher): handle redirect in rout records X-Git-Tag: v4.0.0-alpha.0~398 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=d131d25dcc3fb89c39d56e2a71353e6109b5740b;p=thirdparty%2Fvuejs%2Frouter.git feat(matcher): handle redirect in rout records --- diff --git a/__tests__/matcher.spec.js b/__tests__/matcher.spec.js index 8e8521f7..4a21698e 100644 --- a/__tests__/matcher.spec.js +++ b/__tests__/matcher.spec.js @@ -6,14 +6,19 @@ const { START_LOCATION_NORMALIZED } = require('../src/types') const component = null +/** @typedef {import('../src/types').RouteRecord} RouteRecord */ +/** @typedef {import('../src/types').MatcherLocation} MatcherLocation */ +/** @typedef {import('../src/types').MatcherLocationRedirect} MatcherLocationRedirect */ +/** @typedef {import('../src/types').MatcherLocationNormalized} MatcherLocationNormalized */ + describe('Router Matcher', () => { describe('resolve', () => { /** * - * @param {import('../src/types').RouteRecord} record - * @param {import('../src/types').MatcherLocation} location - * @param {Partial} resolved - * @param {import('../src/types').MatcherLocationNormalized} start + * @param {RouteRecord | RouteRecord[]} record Record or records we are testing the matcher against + * @param {MatcherLocation} location location we want to reolve against + * @param {Partial} resolved Expected resolved location given by the matcher + * @param {MatcherLocationNormalized} [start] Optional currentLocation used when resolving */ function assertRecordMatch( record, @@ -21,7 +26,8 @@ describe('Router Matcher', () => { resolved, start = START_LOCATION_NORMALIZED ) { - const matcher = new RouterMatcher([record]) + record = Array.isArray(record) ? record : [record] + const matcher = new RouterMatcher(record) const targetLocation = {} // add location if provided as it should be the same value @@ -29,8 +35,12 @@ describe('Router Matcher', () => { resolved.path = location.path } - // use one single record - if (!('matched' in resolved)) resolved.matched = [record] + if ('redirect' in record) { + } else { + // use one single record + // @ts-ignore + if (!('matched' in resolved)) resolved.matched = record + } // allows not passing params if ('params' in location) { @@ -52,9 +62,9 @@ describe('Router Matcher', () => { /** * - * @param {import('../src/types').RouteRecord} record - * @param {import('../src/types').MatcherLocation} location - * @param {import('../src/types').MatcherLocationNormalized} start + * @param {RouteRecord | RouteRecord[]} record Record or records we are testing the matcher against + * @param {MatcherLocation} location location we want to reolve against + * @param {MatcherLocationNormalized} [start] Optional currentLocation used when resolving * @returns {any} error */ function assertErrorMatch( @@ -232,9 +242,164 @@ describe('Router Matcher', () => { ) }) + describe('redirects', () => { + /** + * + * @param {RouteRecord[]} records Record or records we are testing the matcher against + * @param {MatcherLocation} location location we want to reolve against + * @param {MatcherLocationNormalized | MatcherLocationRedirect} expected Expected resolved location given by the matcher + * @param {MatcherLocationNormalized} [currentLocation] Optional currentLocation used when resolving + */ + function assertRedirect( + records, + location, + expected, + currentLocation = START_LOCATION_NORMALIZED + ) { + const matcher = new RouterMatcher(records) + const resolved = matcher.resolve(location, currentLocation) + expect(resolved).toEqual(expected) + return resolved + } + + // FIXME: refactor the tests into the function, probably use a common set of routes + // tests named routes and relatives routes + // move to different folder + + it('resolves a redirect string', () => { + const records = [ + { path: '/home', component }, + { path: '/redirect', redirect: '/home' }, + ] + assertRedirect( + records, + { + name: undefined, + path: '/redirect', + }, + { + redirect: '/home', + normalizedLocation: { + path: '/redirect', + params: {}, + name: undefined, + matched: [], + }, + } + ) + }) + + it('resolves a redirect function that returns a string', () => { + const redirect = () => '/home' + const records = [ + { path: '/home', component }, + { path: '/redirect', redirect }, + ] + assertRedirect( + records, + { + name: undefined, + path: '/redirect', + }, + { + redirect, + normalizedLocation: { + path: '/redirect', + params: {}, + name: undefined, + matched: [], + }, + } + ) + }) + + it('resolves a redirect function that returns an object route', () => { + const redirect = () => { + path: '/home' + } + const records = [ + { path: '/home', component }, + { path: '/redirect', redirect }, + ] + assertRedirect( + records, + { + name: undefined, + path: '/redirect', + }, + { + redirect, + normalizedLocation: { + path: '/redirect', + params: {}, + name: undefined, + matched: [], + }, + } + ) + }) + + it('resolves a redirect as an object', () => { + const records = [ + { path: '/home', component }, + { path: '/redirect', redirect: { path: 'home' } }, + ] + assertRedirect( + records, + { + name: undefined, + path: '/redirect', + }, + { + redirect: { path: 'home' }, + normalizedLocation: { + path: '/redirect', + params: {}, + name: undefined, + matched: [], + }, + } + ) + }) + + it('normalize a location when redirecting', () => { + const redirect = to => ({ name: 'b', params: to.params }) + const records = [ + { path: '/home', component }, + { + path: '/a/:a', + name: 'a', + redirect, + }, + { path: '/b/:a', name: 'b', component }, + ] + assertRedirect( + records, + { + name: undefined, + path: '/a/foo', + }, + { + redirect, + normalizedLocation: { + path: '/a/foo', + params: { a: 'foo' }, + name: 'a', + matched: [], + }, + } + ) + }) + }) + it('throws if the current named route does not exists', () => { const record = { path: '/', component } - const start = { name: 'home', params: {}, path: '/', matched: [record] } + const start = { + name: 'home', + params: {}, + path: '/', + matched: [record], + } // the property should be non enumerable Object.defineProperty(start, 'matched', { enumerable: false }) expect(assertErrorMatch(record, {}, start)).toMatchInlineSnapshot( diff --git a/__tests__/router.spec.js b/__tests__/router.spec.js index b43682da..888c2872 100644 --- a/__tests__/router.spec.js +++ b/__tests__/router.spec.js @@ -10,9 +10,11 @@ function mockHistory() { return new HTML5History() } +/** @type {import('../src/types').RouteRecord[]} */ const routes = [ { path: '/', component: components.Home }, { path: '/foo', component: components.Foo }, + { path: '/to-foo', redirect: '/foo' }, ] describe('Router', () => { @@ -59,4 +61,6 @@ describe('Router', () => { hash: '', }) }) + + // it('redirects with route record redirect') }) diff --git a/src/matcher.ts b/src/matcher.ts index 1ff6ed46..3385f53a 100644 --- a/src/matcher.ts +++ b/src/matcher.ts @@ -4,10 +4,10 @@ import { RouteParams, MatcherLocation, MatcherLocationNormalized, + MatcherLocationRedirect, } from './types/index' import { NoRouteMatchError } from './errors' -// TODO: rename interface RouteMatcher { re: RegExp resolve: (params?: RouteParams) => string @@ -46,8 +46,7 @@ export class RouterMatcher { resolve( location: Readonly, currentLocation: Readonly - // TODO: return type is wrong, should contain fullPath and record/matched - ): MatcherLocationNormalized { + ): MatcherLocationNormalized | MatcherLocationRedirect { let matcher: RouteMatcher | void // TODO: refactor with type guards @@ -56,11 +55,6 @@ export class RouterMatcher { matcher = this.matchers.find(m => m.re.test(location.path)) if (!matcher) throw new NoRouteMatchError(currentLocation, location) - // TODO: build up the array with children based on current location - - if ('redirect' in matcher.record) throw new Error('TODO') - - const matched = [matcher.record] const params: RouteParams = {} const result = matcher.re.exec(location.path) @@ -81,6 +75,28 @@ export class RouterMatcher { params[key] = value } + if ('redirect' in matcher.record) { + const { redirect } = matcher.record + return { + redirect, + normalizedLocation: { + name: matcher.record.name, + path: location.path, + matched: [], + params, + }, + } + // if redirect is a function we do not have enough information, so we throw + // TODO: not use a throw + // throw new RedirectInRecord(typeof redirect === 'function' ? { + // redirect, + // route: { name: matcher.record.name, path: location.path, params, matched: [] } + // } : redirect) + } + + // TODO: build up the array with children based on current location + const matched = [matcher.record] + return { name: matcher.record.name, /// no need to resolve the path with the matcher as it was provided diff --git a/src/router.ts b/src/router.ts index ae3e1c0d..91e8e15d 100644 --- a/src/router.ts +++ b/src/router.ts @@ -11,6 +11,7 @@ import { TODO, PostNavigationGuard, Lazy, + MatcherLocation, } from './types/index' import { guardToPromiseFn, extractComponentsGuards } from './utils' @@ -36,7 +37,7 @@ export class Router { this.history.listen((to, from, info) => { // TODO: check navigation guards - const matchedRoute = this.matcher.resolve(to, this.currentRoute) + const matchedRoute = this.matchLocation(to, this.currentRoute) // console.log({ to, matchedRoute }) // TODO: navigate @@ -47,6 +48,43 @@ export class Router { }) } + private matchLocation( + location: MatcherLocation, + currentLocation: MatcherLocationNormalized + ): MatcherLocationNormalized { + const matchedRoute = this.matcher.resolve(location, currentLocation) + if ('redirect' in matchedRoute) { + const { redirect, normalizedLocation } = matchedRoute + // TODO: add from to a redirect stack? + if (typeof redirect === 'string') { + // match the redirect instead + return this.matchLocation( + this.history.utils.normalizeLocation(redirect), + currentLocation + ) + } else if (typeof redirect === 'function') { + const url = this.history.utils.normalizeLocation(normalizedLocation) + const newLocation = redirect({ + ...normalizedLocation, + ...url, + }) + + if (typeof newLocation === 'string') { + return this.matchLocation( + this.history.utils.normalizeLocation(newLocation), + currentLocation + ) + } + + return this.matchLocation(newLocation, currentLocation) + } else { + return this.matchLocation(redirect, currentLocation) + } + } else { + return matchedRoute + } + } + /** * Trigger a navigation, adding an entry to the history stack. Also apply all navigation * guards first @@ -58,11 +96,11 @@ export class Router { if (typeof to === 'string' || 'path' in to) { url = this.history.utils.normalizeLocation(to) // TODO: should allow a non matching url to allow dynamic routing to work - location = this.matcher.resolve(url, this.currentRoute) + location = this.matchLocation(url, this.currentRoute) } else { // named or relative route // we need to resolve first - location = this.matcher.resolve(to, this.currentRoute) + location = this.matchLocation(to, this.currentRoute) // intentionally drop current query and hash url = this.history.utils.normalizeLocation({ query: to.query ? this.history.utils.normalizeQuery(to.query) : {}, diff --git a/src/types/index.ts b/src/types/index.ts index 2c09ecfa..593530eb 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -100,9 +100,11 @@ interface RouteRecordCommon { beforeEnter?: NavigationGuard } -type DynamicRedirect = (to: RouteLocationNormalized) => RouteLocation +export type RouteRecordRedirectOption = + | RouteLocation + | ((to: RouteLocationNormalized) => RouteLocation) interface RouteRecordRedirect extends RouteRecordCommon { - redirect: RouteLocation | DynamicRedirect + redirect: RouteRecordRedirectOption } interface RouteRecordSingleView extends RouteRecordCommon { @@ -155,6 +157,15 @@ export interface MatcherLocationNormalized { matched: MatchedRouteRecord[] } +// used when the route records requires a redirection +// with a function call. The matcher isn't able to do it +// by itself, so it dispatches the information so the router +// can pick it up +export interface MatcherLocationRedirect { + redirect: RouteRecordRedirectOption + normalizedLocation: MatcherLocationNormalized +} + export interface NavigationGuardCallback { (): void (location: RouteLocation): void