From 44ef8a56fc99a7112396b7bb8eec4c22cadfa24e Mon Sep 17 00:00:00 2001 From: Chad Hietala Date: Tue, 2 Oct 2018 14:56:56 -0400 Subject: [PATCH 1/2] Add recognize and recognizeAndLoad --- lib/router/route-info.ts | 26 +- lib/router/router.ts | 48 +++- tests/router_test.ts | 512 ++++++++++++++++++++++++++++++++++++++- 3 files changed, 579 insertions(+), 7 deletions(-) diff --git a/lib/router/route-info.ts b/lib/router/route-info.ts index 098fbe78..46d4f322 100644 --- a/lib/router/route-info.ts +++ b/lib/router/route-info.ts @@ -49,15 +49,20 @@ export interface RouteInfo { ): RouteInfo | undefined; } +export interface RouteInfoWithAttributes extends RouteInfo { + attributes: Dict; +} + let ROUTE_INFOS = new WeakMap, RouteInfo>(); export function toReadOnlyRouteInfo( routeInfos: InternalRouteInfo[], - queryParams: Dict = {} + queryParams: Dict = {}, + includeAttributes = false ) { return routeInfos.map((info, i) => { - let { name, params, paramNames } = info; - let publicRouteInfo = new class implements RouteInfo { + let { name, params, paramNames, context } = info; + let RoutInfoImpl = class RoutInfoImpl { find( predicate: (this: any, routeInfo: RouteInfo, i?: number, arr?: RouteInfo[]) => boolean, thisArg: any @@ -119,7 +124,20 @@ export function toReadOnlyRouteInfo( get queryParams() { return queryParams; } - }(); + }; + + let publicRouteInfo; + + if (includeAttributes) { + class RouteInfoWithAttributes extends RoutInfoImpl { + get attributes() { + return context; + } + } + publicRouteInfo = new RouteInfoWithAttributes(); + } else { + publicRouteInfo = new RoutInfoImpl(); + } ROUTE_INFOS.set(info, publicRouteInfo); diff --git a/lib/router/router.ts b/lib/router/router.ts index b85ef7a4..00a89662 100644 --- a/lib/router/router.ts +++ b/lib/router/router.ts @@ -1,7 +1,12 @@ import RouteRecognizer, { MatchCallback, Params } from 'route-recognizer'; import { Promise } from 'rsvp'; -import { Dict, Maybe } from './core'; -import InternalRouteInfo, { Route, toReadOnlyRouteInfo } from './route-info'; +import { Dict, Maybe, Option } from './core'; +import InternalRouteInfo, { + Route, + RouteInfo, + RouteInfoWithAttributes, + toReadOnlyRouteInfo, +} from './route-info'; import InternalTransition, { logAbort, OpaqueTransition, @@ -150,6 +155,45 @@ export default abstract class Router { } } + recognize(url: string): Option { + let intent = new URLTransitionIntent(this, url); + let newState = this.generateNewState(intent); + + if (newState === null) { + return newState; + } + + let readonlyInfos = toReadOnlyRouteInfo(newState.routeInfos, newState.queryParams); + return readonlyInfos[readonlyInfos.length - 1]; + } + + recognizeAndLoad(url: string): Promise { + let intent = new URLTransitionIntent(this, url); + let newState = this.generateNewState(intent); + + if (newState === null) { + return Promise.reject(`URL ${url} was not recognized`); + } + + let newTransition: OpaqueTransition = new InternalTransition(this, intent, newState, undefined); + return newTransition.then(() => { + let routeInfosWithAttributes = toReadOnlyRouteInfo( + newState!.routeInfos, + newTransition.queryParams, + true + ) as RouteInfoWithAttributes[]; + return routeInfosWithAttributes[routeInfosWithAttributes.length - 1]; + }); + } + + private generateNewState(intent: TransitionIntent): Option> { + try { + return intent.applyToState(this.state!, false); + } catch (e) { + return null; + } + } + private getTransitionByIntent( intent: TransitionIntent, isIntermediate: boolean diff --git a/tests/router_test.ts b/tests/router_test.ts index 3b0da4a1..e6ac34c5 100644 --- a/tests/router_test.ts +++ b/tests/router_test.ts @@ -1,7 +1,7 @@ import { MatchCallback } from 'route-recognizer'; import Router, { Route, Transition } from 'router'; import { Dict, Maybe } from 'router/core'; -import RouteInfo from 'router/route-info'; +import RouteInfo, { RouteInfoWithAttributes } from 'router/route-info'; import { SerializerFunc } from 'router/router'; import { logAbort } from 'router/transition'; import { TransitionError } from 'router/transition-state'; @@ -527,6 +527,516 @@ scenarios.forEach(function(scenario) { }); }); + test('top-level recognizeAndLoad url', function(assert) { + map(assert, function(match) { + match('/').to('index'); + }); + + routes = { + index: createHandler('index', { + model() { + return { name: 'index', data: 1 }; + }, + }), + }; + assert.notOk(router.activeTransition, 'Does not start with an active transition'); + + router.replaceURL = () => { + assert.ok(false, 'Should not replace the URL'); + }; + + router.updateURL = () => { + assert.ok(false, 'Should not update the URL'); + }; + + router.recognizeAndLoad('/').then((routeInfoWithAttributes: RouteInfoWithAttributes) => { + assert.notOk(router.activeTransition, 'Does not create an active transition'); + if (routeInfoWithAttributes === null) { + assert.ok(false); + return; + } + assert.equal(routeInfoWithAttributes.name, 'index'); + assert.equal(routeInfoWithAttributes.localName, 'index'); + assert.equal(routeInfoWithAttributes.parent, null); + assert.equal(routeInfoWithAttributes.child, null); + assert.deepEqual(routeInfoWithAttributes.attributes, { name: 'index', data: 1 }); + assert.deepEqual(routeInfoWithAttributes.queryParams, {}); + assert.deepEqual(routeInfoWithAttributes.params, {}); + assert.deepEqual(routeInfoWithAttributes.paramNames, []); + }); + }); + + test('top-level parameterized recognizeAndLoad', function(assert) { + map(assert, function(match) { + match('/posts/:id').to('posts'); + }); + + routes = { + posts: createHandler('posts', { + model(params: { id: string }) { + return { name: 'posts', data: params.id }; + }, + }), + }; + assert.notOk(router.activeTransition, 'Does not start with an active transition'); + + router.replaceURL = () => { + assert.ok(false, 'Should not replace the URL'); + }; + + router.updateURL = () => { + assert.ok(false, 'Should not update the URL'); + }; + + router + .recognizeAndLoad('/posts/123') + .then((routeInfoWithAttributes: RouteInfoWithAttributes) => { + assert.notOk(router.activeTransition, 'Does not create an active transition'); + if (routeInfoWithAttributes === null) { + assert.ok(false); + return; + } + assert.equal(routeInfoWithAttributes.name, 'posts'); + assert.equal(routeInfoWithAttributes.localName, 'posts'); + assert.equal(routeInfoWithAttributes.parent, null); + assert.equal(routeInfoWithAttributes.child, null); + assert.deepEqual(routeInfoWithAttributes.attributes, { name: 'posts', data: '123' }); + assert.deepEqual(routeInfoWithAttributes.queryParams, {}); + assert.deepEqual(routeInfoWithAttributes.params, { id: '123' }); + assert.deepEqual(routeInfoWithAttributes.paramNames, ['id']); + }); + }); + + test('nested recognizeAndLoad', function(assert) { + routes = { + postIndex: createHandler('postIndex'), + showPopularPosts: createHandler('showPopularPosts', { + model() { + return { name: 'showPopularPosts', data: 123 }; + }, + }), + }; + assert.notOk(router.activeTransition, 'Does not start with an active transition'); + + router.replaceURL = () => { + assert.ok(false, 'Should not replace the URL'); + }; + + router.updateURL = () => { + assert.ok(false, 'Should not update the URL'); + }; + + router + .recognizeAndLoad('/posts/popular') + .then((routeInfoWithAttributes: RouteInfoWithAttributes) => { + assert.notOk(router.activeTransition, 'Does not create an active transition'); + if (routeInfoWithAttributes === null) { + assert.ok(false); + return; + } + assert.equal(routeInfoWithAttributes.name, 'showPopularPosts'); + assert.equal(routeInfoWithAttributes.localName, 'showPopularPosts'); + assert.equal(routeInfoWithAttributes.parent!.name, 'postIndex'); + assert.equal(routeInfoWithAttributes.child, null); + assert.deepEqual(routeInfoWithAttributes.attributes, { + name: 'showPopularPosts', + data: 123, + }); + assert.deepEqual(routeInfoWithAttributes.queryParams, {}); + assert.deepEqual(routeInfoWithAttributes.params, {}); + assert.deepEqual(routeInfoWithAttributes.paramNames, []); + }); + }); + + test('nested params recognizeAndLoad', function(assert) { + routes = { + postIndex: createHandler('postIndex'), + showFilteredPosts: createHandler('showFilteredPosts', { + model(params: { filter_id: string }) { + return { name: 'showFilteredPosts', data: params.filter_id }; + }, + }), + }; + assert.notOk(router.activeTransition, 'Does not start with an active transition'); + + router.replaceURL = () => { + assert.ok(false, 'Should not replace the URL'); + }; + + router.updateURL = () => { + assert.ok(false, 'Should not update the URL'); + }; + + router + .recognizeAndLoad('/posts/filter/1') + .then((routeInfoWithAttributes: RouteInfoWithAttributes) => { + assert.notOk(router.activeTransition, 'Does not create an active transition'); + if (routeInfoWithAttributes === null) { + assert.ok(false); + return; + } + assert.equal(routeInfoWithAttributes.name, 'showFilteredPosts'); + assert.equal(routeInfoWithAttributes.localName, 'showFilteredPosts'); + assert.equal(routeInfoWithAttributes.parent!.name, 'postIndex'); + assert.equal(routeInfoWithAttributes.child, null); + assert.deepEqual(routeInfoWithAttributes.attributes, { + name: 'showFilteredPosts', + data: '1', + }); + assert.deepEqual(routeInfoWithAttributes.queryParams, {}); + assert.deepEqual(routeInfoWithAttributes.params, { filter_id: '1' }); + assert.deepEqual(routeInfoWithAttributes.paramNames, ['filter_id']); + }); + }); + + test('top-level QPs recognizeAndLoad', function(assert) { + routes = { + showAllPosts: createHandler('showAllPosts', { + model() { + return { name: 'showAllPosts', data: 'qp' }; + }, + }), + }; + assert.notOk(router.activeTransition, 'Does not start with an active transition'); + + router.replaceURL = () => { + assert.ok(false, 'Should not replace the URL'); + }; + + router.updateURL = () => { + assert.ok(false, 'Should not update the URL'); + }; + + router + .recognizeAndLoad('/posts/?a=b') + .then((routeInfoWithAttributes: RouteInfoWithAttributes) => { + assert.notOk(router.activeTransition, 'Does not create an active transition'); + if (routeInfoWithAttributes === null) { + assert.ok(false); + return; + } + assert.equal(routeInfoWithAttributes.name, 'showAllPosts'); + assert.equal(routeInfoWithAttributes.localName, 'showAllPosts'); + assert.equal(routeInfoWithAttributes.parent!.name, 'postIndex'); + assert.equal(routeInfoWithAttributes.child, null); + assert.deepEqual(routeInfoWithAttributes.attributes, { + name: 'showAllPosts', + data: 'qp', + }); + assert.deepEqual(routeInfoWithAttributes.queryParams, { a: 'b' }); + assert.deepEqual(routeInfoWithAttributes.params, {}); + assert.deepEqual(routeInfoWithAttributes.paramNames, []); + }); + }); + + test('top-level params and QPs recognizeAndLoad', function(assert) { + routes = { + postsIndex: createHandler('postsIndex'), + showFilteredPosts: createHandler('showFilteredPosts', { + model(params: { filter_id: string }) { + return { name: 'showFilteredPosts', data: params.filter_id }; + }, + }), + }; + assert.notOk(router.activeTransition, 'Does not start with an active transition'); + + router.replaceURL = () => { + assert.ok(false, 'Should not replace the URL'); + }; + + router.updateURL = () => { + assert.ok(false, 'Should not update the URL'); + }; + + router + .recognizeAndLoad('/posts/filter/123?a=b') + .then((routeInfoWithAttributes: RouteInfoWithAttributes) => { + assert.notOk(router.activeTransition, 'Does not create an active transition'); + if (routeInfoWithAttributes === null) { + assert.ok(false); + return; + } + assert.equal(routeInfoWithAttributes.name, 'showFilteredPosts'); + assert.equal(routeInfoWithAttributes.localName, 'showFilteredPosts'); + assert.equal(routeInfoWithAttributes.parent!.name, 'postIndex'); + assert.equal(routeInfoWithAttributes.child, null); + assert.deepEqual(routeInfoWithAttributes.attributes, { + name: 'showFilteredPosts', + data: '123', + }); + assert.deepEqual(routeInfoWithAttributes.queryParams, { a: 'b' }); + assert.deepEqual(routeInfoWithAttributes.params, { filter_id: '123' }); + assert.deepEqual(routeInfoWithAttributes.paramNames, ['filter_id']); + }); + }); + + test('unrecognized url rejects', function(assert) { + router.recognizeAndLoad('/fixzzz').then( + () => { + assert.ok(false, 'never here'); + }, + (reason: string) => { + assert.equal(reason, `URL /fixzzz was not recognized`); + } + ); + }); + + test('top-level recognize url', function(assert) { + map(assert, function(match) { + match('/').to('index'); + }); + + routes = { + post: createHandler('post'), + }; + assert.notOk(router.activeTransition, 'Does not start with an active transition'); + let routeInfo = router.recognize('/'); + + assert.notOk(router.activeTransition, 'Does not create an active transition'); + + if (routeInfo === null) { + assert.ok(false); + return; + } + + router.replaceURL = () => { + assert.ok(false, 'Should not replace the URL'); + }; + + router.updateURL = () => { + assert.ok(false, 'Should not update the URL'); + }; + + assert.equal(routeInfo.name, 'index'); + assert.equal(routeInfo.localName, 'index'); + assert.equal(routeInfo.parent, null); + assert.equal(routeInfo.child, null); + assert.deepEqual(routeInfo.queryParams, {}); + assert.deepEqual(routeInfo.params, {}); + assert.deepEqual(routeInfo.paramNames, []); + }); + + test('top-level recognize url with params', function(assert) { + map(assert, function(match) { + match('/posts/:id').to('post'); + }); + + routes = { + post: createHandler('post'), + }; + + assert.notOk(router.activeTransition, 'Does not start with an active transition'); + + let routeInfo = router.recognize('/posts/123'); + + assert.notOk(router.activeTransition, 'Does not create an active transition'); + + if (routeInfo === null) { + assert.ok(false); + return; + } + + router.replaceURL = () => { + assert.ok(false, 'Should not replace the URL'); + }; + + router.updateURL = () => { + assert.ok(false, 'Should not update the URL'); + }; + + assert.equal(routeInfo.name, 'post'); + assert.equal(routeInfo.localName, 'post'); + assert.equal(routeInfo.parent, null); + assert.equal(routeInfo.child, null); + assert.deepEqual(routeInfo.queryParams, {}); + assert.deepEqual(routeInfo.params, { id: '123' }); + assert.deepEqual(routeInfo.paramNames, ['id']); + }); + + test('nested recognize url', function(assert) { + routes = { + postIndex: createHandler('postIndex'), + showPopularPosts: createHandler('showPopularPosts'), + }; + assert.notOk(router.activeTransition, 'Does not start with an active transition'); + + let routeInfo = router.recognize('/posts/popular'); + + assert.notOk(router.activeTransition, 'Does not create an active transition'); + + if (routeInfo === null) { + assert.ok(false); + return; + } + + router.replaceURL = () => { + assert.ok(false, 'Should not replace the URL'); + }; + + router.updateURL = () => { + assert.ok(false, 'Should not update the URL'); + }; + + assert.equal(routeInfo.name, 'showPopularPosts'); + assert.equal(routeInfo.localName, 'showPopularPosts'); + assert.equal(routeInfo.parent!.name, 'postIndex'); + assert.equal(routeInfo.child, null); + assert.deepEqual(routeInfo.queryParams, {}); + assert.deepEqual(routeInfo.params, {}); + assert.deepEqual(routeInfo.paramNames, []); + }); + + test('nested recognize url with params', function(assert) { + routes = { + postIndex: createHandler('postIndex'), + showFilteredPosts: createHandler('showFilteredPosts'), + }; + assert.notOk(router.activeTransition, 'Does not start with an active transition'); + + let routeInfo = router.recognize('/posts/filter/123'); + + assert.notOk(router.activeTransition, 'Does not create an active transition'); + + if (routeInfo === null) { + assert.ok(false); + return; + } + + router.replaceURL = () => { + assert.ok(false, 'Should not replace the URL'); + }; + + router.updateURL = () => { + assert.ok(false, 'Should not update the URL'); + }; + + assert.equal(routeInfo.name, 'showFilteredPosts'); + assert.equal(routeInfo.localName, 'showFilteredPosts'); + assert.equal(routeInfo.parent!.name, 'postIndex'); + assert.equal(routeInfo.child, null); + assert.deepEqual(routeInfo.queryParams, {}); + assert.deepEqual(routeInfo.params, { filter_id: '123' }); + assert.deepEqual(routeInfo.paramNames, ['filter_id']); + }); + + test('top-level recognize url with QPs', function(assert) { + map(assert, function(match) { + match('/').to('index'); + }); + + routes = { + index: createHandler('index'), + }; + + assert.notOk(router.activeTransition, 'Does not start with an active transition'); + + let routeInfo = router.recognize('/?a=123'); + + assert.notOk(router.activeTransition, 'Does not create an active transition'); + + if (routeInfo === null) { + assert.ok(false); + return; + } + + router.replaceURL = () => { + assert.ok(false, 'Should not replace the URL'); + }; + + router.updateURL = () => { + assert.ok(false, 'Should not update the URL'); + }; + + assert.equal(routeInfo.name, 'index'); + assert.equal(routeInfo.localName, 'index'); + assert.equal(routeInfo.parent, null); + assert.equal(routeInfo.child, null); + assert.deepEqual(routeInfo.queryParams, { a: '123' }); + assert.deepEqual(routeInfo.params, {}); + assert.deepEqual(routeInfo.paramNames, []); + }); + + test('nested recognize url with QPs', function(assert) { + routes = { + postIndex: createHandler('postIndex'), + showPopularPosts: createHandler('showPopularPosts'), + }; + + assert.notOk(router.activeTransition, 'Does not start with an active transition'); + + let routeInfo = router.recognize('/posts/popular?fizz=bar'); + + assert.notOk(router.activeTransition, 'Does not create an active transition'); + + if (routeInfo === null) { + assert.ok(false); + return; + } + + router.replaceURL = () => { + assert.ok(false, 'Should not replace the URL'); + }; + + router.updateURL = () => { + assert.ok(false, 'Should not update the URL'); + }; + + assert.equal(routeInfo.name, 'showPopularPosts'); + assert.equal(routeInfo.localName, 'showPopularPosts'); + assert.equal(routeInfo.parent!.name, 'postIndex'); + assert.equal(routeInfo.child, null); + assert.deepEqual(routeInfo.queryParams, { fizz: 'bar' }); + assert.deepEqual(routeInfo.params, {}); + assert.deepEqual(routeInfo.paramNames, []); + }); + + test('nested recognize url with QPs and params', function(assert) { + routes = { + postIndex: createHandler('postIndex'), + showFilteredPosts: createHandler('showFilteredPosts'), + }; + + assert.notOk(router.activeTransition, 'Does not start with an active transition'); + + let routeInfo = router.recognize('/posts/filter/123?fizz=bar'); + + assert.notOk(router.activeTransition, 'Does not create an active transition'); + + if (routeInfo === null) { + assert.ok(false); + return; + } + + router.replaceURL = () => { + assert.ok(false, 'Should not replace the URL'); + }; + + router.updateURL = () => { + assert.ok(false, 'Should not update the URL'); + }; + + assert.equal(routeInfo.name, 'showFilteredPosts'); + assert.equal(routeInfo.localName, 'showFilteredPosts'); + assert.equal(routeInfo.parent!.name, 'postIndex'); + assert.equal(routeInfo.child, null); + assert.deepEqual(routeInfo.queryParams, { fizz: 'bar' }); + assert.deepEqual(routeInfo.params, { filter_id: '123' }); + assert.deepEqual(routeInfo.paramNames, ['filter_id']); + }); + + test('unrecognized url returns null', function(assert) { + map(assert, function(match) { + match('/').to('index'); + match('/posts/:id').to('post'); + }); + + routes = { + post: createHandler('post'), + }; + let routeInfo = router.recognize('/fixzzz'); + assert.equal(routeInfo, null, 'Unrecognized url'); + }); + test('basic route change events with nested params', function(assert) { assert.expect(14); map(assert, function(match) { From 358ca31bebf83f82b2810edc47751ad0b9f38557 Mon Sep 17 00:00:00 2001 From: Chad Hietala Date: Wed, 3 Oct 2018 09:15:50 -0400 Subject: [PATCH 2/2] Make RouteInfo types juat a POJO --- lib/router/route-info.ts | 33 ++++++++++++++------------------- 1 file changed, 14 insertions(+), 19 deletions(-) diff --git a/lib/router/route-info.ts b/lib/router/route-info.ts index 46d4f322..7c4d5839 100644 --- a/lib/router/route-info.ts +++ b/lib/router/route-info.ts @@ -62,7 +62,7 @@ export function toReadOnlyRouteInfo( ) { return routeInfos.map((info, i) => { let { name, params, paramNames, context } = info; - let RoutInfoImpl = class RoutInfoImpl { + let routeInfo: RouteInfo = { find( predicate: (this: any, routeInfo: RouteInfo, i?: number, arr?: RouteInfo[]) => boolean, thisArg: any @@ -82,15 +82,15 @@ export function toReadOnlyRouteInfo( } return undefined; - } + }, get name() { return name; - } + }, get paramNames() { return paramNames; - } + }, get parent() { let parent = routeInfos[i - 1]; @@ -100,7 +100,7 @@ export function toReadOnlyRouteInfo( } return ROUTE_INFOS.get(parent)!; - } + }, get child() { let child = routeInfos[i + 1]; @@ -110,38 +110,33 @@ export function toReadOnlyRouteInfo( } return ROUTE_INFOS.get(child)!; - } + }, get localName() { let parts = this.name.split('.'); return parts[parts.length - 1]; - } + }, get params() { return params; - } + }, get queryParams() { return queryParams; - } + }, }; - let publicRouteInfo; - if (includeAttributes) { - class RouteInfoWithAttributes extends RoutInfoImpl { + routeInfo = Object.assign(routeInfo, { get attributes() { return context; - } - } - publicRouteInfo = new RouteInfoWithAttributes(); - } else { - publicRouteInfo = new RoutInfoImpl(); + }, + }); } - ROUTE_INFOS.set(info, publicRouteInfo); + ROUTE_INFOS.set(info, routeInfo); - return publicRouteInfo; + return routeInfo; }); }