From d35c109cb9cc40506f09fbf06ba1a596627d1380 Mon Sep 17 00:00:00 2001 From: vsavkin Date: Mon, 25 Apr 2016 16:57:15 -0700 Subject: [PATCH] feat(router): update recognize to support aux routes --- modules/angular2/src/alt_router/constants.ts | 1 + modules/angular2/src/alt_router/recognize.ts | 115 +++++++++++++----- .../test/alt_router/recognize_spec.ts | 113 +++++++++++++++-- 3 files changed, 186 insertions(+), 43 deletions(-) create mode 100644 modules/angular2/src/alt_router/constants.ts diff --git a/modules/angular2/src/alt_router/constants.ts b/modules/angular2/src/alt_router/constants.ts new file mode 100644 index 0000000000..52ecd2e56e --- /dev/null +++ b/modules/angular2/src/alt_router/constants.ts @@ -0,0 +1 @@ +export const DEFAULT_OUTLET_NAME = "__DEFAULT"; \ No newline at end of file diff --git a/modules/angular2/src/alt_router/recognize.ts b/modules/angular2/src/alt_router/recognize.ts index 7c1b79b070..61de5d2fe8 100644 --- a/modules/angular2/src/alt_router/recognize.ts +++ b/modules/angular2/src/alt_router/recognize.ts @@ -1,46 +1,66 @@ -import {RouteSegment, UrlSegment, Tree} from './segments'; +import {RouteSegment, UrlSegment, Tree, TreeNode, rootNode} from './segments'; import {RoutesMetadata, RouteMetadata} from './metadata/metadata'; -import {Type, isPresent, stringify} from 'angular2/src/facade/lang'; +import {Type, isBlank, isPresent, stringify} from 'angular2/src/facade/lang'; +import {ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection'; import {PromiseWrapper} from 'angular2/src/facade/promise'; import {BaseException} from 'angular2/src/facade/exceptions'; import {ComponentResolver} from 'angular2/core'; +import {DEFAULT_OUTLET_NAME} from './constants'; import {reflector} from 'angular2/src/core/reflection/reflection'; export function recognize(componentResolver: ComponentResolver, type: Type, url: Tree): Promise> { - return _recognize(componentResolver, type, url, url.root) - .then(nodes => new Tree(nodes)); + return componentResolver.resolveComponent(type).then(factory => { + let segment = + new RouteSegment([url.root], url.root.parameters, DEFAULT_OUTLET_NAME, type, factory); + return _recognizeMany(componentResolver, type, rootNode(url).children) + .then(children => new Tree(new TreeNode(segment, children))); + }); } -function _recognize(componentResolver: ComponentResolver, type: Type, url: Tree, - current: UrlSegment): Promise { - let metadata = _readMetadata(type); // should read from the factory instead +function _recognize(componentResolver: ComponentResolver, parentType: Type, + url: TreeNode): Promise[]> { + let metadata = _readMetadata(parentType); // should read from the factory instead - let matched; + let match; try { - matched = _match(metadata, url, current); + match = _match(metadata, url); } catch (e) { return PromiseWrapper.reject(e, null); } + let main = _constructSegment(componentResolver, match); + let aux = + _recognizeMany(componentResolver, parentType, match.aux).then(_checkOutletNameUniqueness); + return PromiseWrapper.all([main, aux]).then(ListWrapper.flatten); +} + +function _recognizeMany(componentResolver: ComponentResolver, parentType: Type, + urls: TreeNode[]): Promise[]> { + let recognized = urls.map(u => _recognize(componentResolver, parentType, u)); + return PromiseWrapper.all(recognized).then(ListWrapper.flatten); +} + +function _constructSegment(componentResolver: ComponentResolver, + matched: _MatchResult): Promise[]> { return componentResolver.resolveComponent(matched.route.component) .then(factory => { - let segment = new RouteSegment(matched.consumedUrlSegments, matched.parameters, "", + let segment = new RouteSegment(matched.consumedUrlSegments, matched.parameters, + matched.consumedUrlSegments[0].outlet, matched.route.component, factory); - if (isPresent(matched.leftOver)) { - return _recognize(componentResolver, matched.route.component, url, matched.leftOver) - .then(children => [segment].concat(children)); + if (isPresent(matched.leftOverUrl)) { + return _recognize(componentResolver, matched.route.component, matched.leftOverUrl) + .then(children => [new TreeNode(segment, children)]); } else { - return [segment]; + return [new TreeNode(segment, [])]; } }); } -function _match(metadata: RoutesMetadata, url: Tree, - current: UrlSegment): _MatchingResult { +function _match(metadata: RoutesMetadata, url: TreeNode): _MatchResult { for (let r of metadata.routes) { - let matchingResult = _matchWithParts(r, url, current); + let matchingResult = _matchWithParts(r, url); if (isPresent(matchingResult)) { return matchingResult; } @@ -48,30 +68,63 @@ function _match(metadata: RoutesMetadata, url: Tree, throw new BaseException("Cannot match any routes"); } -function _matchWithParts(route: RouteMetadata, url: Tree, - current: UrlSegment): _MatchingResult { +function _matchWithParts(route: RouteMetadata, url: TreeNode): _MatchResult { let parts = route.path.split("/"); - let parameters = {}; + let positionalParams = {}; let consumedUrlSegments = []; - let u = current; + let lastParent: TreeNode = null; + let lastSegment: TreeNode = null; + + let current = url; for (let i = 0; i < parts.length; ++i) { - consumedUrlSegments.push(u); let p = parts[i]; - if (p.startsWith(":")) { - let segment = u.segment; - parameters[p.substring(1)] = segment; - } else if (p != u.segment) { - return null; + let isLastSegment = i === parts.length - 1; + let isLastParent = i === parts.length - 2; + let isPosParam = p.startsWith(":"); + + if (isBlank(current)) return null; + if (!isPosParam && p != current.value.segment) return null; + if (isLastSegment) { + lastSegment = current; } - u = url.firstChild(u); + if (isLastParent) { + lastParent = current; + } + + if (isPosParam) { + positionalParams[p.substring(1)] = current.value.segment; + } + + consumedUrlSegments.push(current.value); + + current = ListWrapper.first(current.children); } - return new _MatchingResult(route, consumedUrlSegments, parameters, u); + + let parameters = <{[key: string]: string}>StringMapWrapper.merge(lastSegment.value.parameters, + positionalParams); + let axuUrlSubtrees = isPresent(lastParent) ? lastParent.children.slice(1) : []; + return new _MatchResult(route, consumedUrlSegments, parameters, current, axuUrlSubtrees); } -class _MatchingResult { +function _checkOutletNameUniqueness(nodes: TreeNode[]): TreeNode[] { + let names = {}; + nodes.forEach(n => { + let segmentWithSameOutletName = names[n.value.outlet]; + if (isPresent(segmentWithSameOutletName)) { + let p = segmentWithSameOutletName.stringifiedUrlSegments; + let c = n.value.stringifiedUrlSegments; + throw new BaseException(`Two segments cannot have the same outlet name: '${p}' and '${c}'.`); + } + names[n.value.outlet] = n.value; + }); + return nodes; +} + +class _MatchResult { constructor(public route: RouteMetadata, public consumedUrlSegments: UrlSegment[], - public parameters: {[key: string]: string}, public leftOver: UrlSegment) {} + public parameters: {[key: string]: string}, public leftOverUrl: TreeNode, + public aux: TreeNode[]) {} } function _readMetadata(componentType: Type) { diff --git a/modules/angular2/test/alt_router/recognize_spec.ts b/modules/angular2/test/alt_router/recognize_spec.ts index 905b85b790..d161a7f75c 100644 --- a/modules/angular2/test/alt_router/recognize_spec.ts +++ b/modules/angular2/test/alt_router/recognize_spec.ts @@ -19,28 +19,111 @@ import {recognize} from 'angular2/src/alt_router/recognize'; import {Routes, Route} from 'angular2/alt_router'; import {provide, Component, ComponentResolver} from 'angular2/core'; import {UrlSegment, Tree} from 'angular2/src/alt_router/segments'; +import {DefaultRouterUrlParser} from 'angular2/src/alt_router/router_url_parser'; +import {DEFAULT_OUTLET_NAME} from 'angular2/src/alt_router/constants'; export function main() { describe('recognize', () => { it('should handle position args', inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { - recognize(resolver, ComponentA, tree(["b", "paramB", "c", "paramC"])) + recognize(resolver, ComponentA, tree("b/paramB/c/paramC/d")) .then(r => { - let b = r.root; + let a = r.root; + expect(stringifyUrl(a.urlSegments)).toEqual([""]); + expect(a.type).toBe(ComponentA); + + let b = r.firstChild(r.root); expect(stringifyUrl(b.urlSegments)).toEqual(["b", "paramB"]); expect(b.type).toBe(ComponentB); - let c = r.firstChild(r.root); + let c = r.firstChild(r.firstChild(r.root)); expect(stringifyUrl(c.urlSegments)).toEqual(["c", "paramC"]); expect(c.type).toBe(ComponentC); + let d = r.firstChild(r.firstChild(r.firstChild(r.root))); + expect(stringifyUrl(d.urlSegments)).toEqual(["d"]); + expect(d.type).toBe(ComponentD); + + async.done(); + }); + })); + + it('should handle aux routes', + inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { + recognize(resolver, ComponentA, tree("b/paramB(/d//right:d)")) + .then(r => { + let c = r.children(r.root); + expect(stringifyUrl(c[0].urlSegments)).toEqual(["b", "paramB"]); + expect(c[0].outlet).toEqual(DEFAULT_OUTLET_NAME); + expect(c[0].type).toBe(ComponentB); + + expect(stringifyUrl(c[1].urlSegments)).toEqual(["d"]); + expect(c[1].outlet).toEqual("aux"); + expect(c[1].type).toBe(ComponentD); + + expect(stringifyUrl(c[2].urlSegments)).toEqual(["d"]); + expect(c[2].outlet).toEqual("right"); + expect(c[2].type).toBe(ComponentD); + + async.done(); + }); + })); + + it("should error when two segments with the same outlet name", + inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { + recognize(resolver, ComponentA, tree("b/paramB(right:d//right:e)")) + .catch(e => { + expect(e.message).toEqual( + "Two segments cannot have the same outlet name: 'right:d' and 'right:e'."); + async.done(); + }); + })); + + it('should handle nested aux routes', + inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { + recognize(resolver, ComponentA, tree("b/paramB(/d(right:e))")) + .then(r => { + let c = r.children(r.root); + expect(stringifyUrl(c[0].urlSegments)).toEqual(["b", "paramB"]); + expect(c[0].outlet).toEqual(DEFAULT_OUTLET_NAME); + expect(c[0].type).toBe(ComponentB); + + expect(stringifyUrl(c[1].urlSegments)).toEqual(["d"]); + expect(c[1].outlet).toEqual("aux"); + expect(c[1].type).toBe(ComponentD); + + expect(stringifyUrl(c[2].urlSegments)).toEqual(["e"]); + expect(c[2].outlet).toEqual("right"); + expect(c[2].type).toBe(ComponentE); + + async.done(); + }); + })); + + it('should handle matrix parameters', + inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { + recognize(resolver, ComponentA, tree("b/paramB;b1=1;b2=2(/d;d1=1;d2=2)")) + .then(r => { + let c = r.children(r.root); + expect(c[0].parameters).toEqual({'b': 'paramB', 'b1': '1', 'b2': '2'}); + expect(c[1].parameters).toEqual({'d1': '1', 'd2': '2'}); + async.done(); }); })); it('should error when no matching routes', inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { - recognize(resolver, ComponentA, tree(["invalid"])) + recognize(resolver, ComponentA, tree("invalid")) + .catch(e => { + expect(e.message).toEqual("Cannot match any routes"); + async.done(); + }); + })); + + it('should handle no matching routes (too short)', + inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { + recognize(resolver, ComponentA, tree("b")) .catch(e => { expect(e.message).toEqual("Cannot match any routes"); async.done(); @@ -49,7 +132,7 @@ export function main() { it("should error when a component doesn't have @Routes", inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { - recognize(resolver, ComponentA, tree(["d", "invalid"])) + recognize(resolver, ComponentA, tree("d/invalid")) .catch(e => { expect(e.message) .toEqual("Component 'ComponentD' does not have route configuration"); @@ -59,22 +142,27 @@ export function main() { }); } -function tree(nodes: string[]) { - return new Tree(nodes.map(v => new UrlSegment(v, {}, ""))); +function tree(url: string): Tree { + return new DefaultRouterUrlParser().parse(url); } function stringifyUrl(segments: UrlSegment[]): string[] { return segments.map(s => s.segment); } -@Component({selector: 'c', template: 't'}) -class ComponentC { -} - @Component({selector: 'd', template: 't'}) class ComponentD { } +@Component({selector: 'e', template: 't'}) +class ComponentE { +} + +@Component({selector: 'c', template: 't'}) +@Routes([new Route({path: "d", component: ComponentD})]) +class ComponentC { +} + @Component({selector: 'b', template: 't'}) @Routes([new Route({path: "c/:c", component: ComponentC})]) class ComponentB { @@ -83,7 +171,8 @@ class ComponentB { @Component({selector: 'a', template: 't'}) @Routes([ new Route({path: "b/:b", component: ComponentB}), - new Route({path: "d", component: ComponentD}) + new Route({path: "d", component: ComponentD}), + new Route({path: "e", component: ComponentE}) ]) class ComponentA { } \ No newline at end of file