diff --git a/modules/@angular/router/src/link.ts b/modules/@angular/router/src/link.ts index b03bc721d5..13d5fbc481 100644 --- a/modules/@angular/router/src/link.ts +++ b/modules/@angular/router/src/link.ts @@ -3,7 +3,6 @@ import {isBlank, isPresent, isString, isStringMap} from './facade/lang'; import {BaseException} from './facade/exceptions'; import {ListWrapper, StringMapWrapper} from './facade/collection'; -// TODO: vsavkin: should reuse segments export function link(segment: RouteSegment, routeTree: RouteTree, urlTree: UrlTree, commands: any[]): UrlTree { if (commands.length === 0) return urlTree; diff --git a/modules/@angular/router/src/recognize.ts b/modules/@angular/router/src/recognize.ts index 0054c57422..92dbf8d078 100644 --- a/modules/@angular/router/src/recognize.ts +++ b/modules/@angular/router/src/recognize.ts @@ -1,21 +1,21 @@ -import {RouteSegment, UrlSegment, Tree, TreeNode, rootNode, UrlTree, RouteTree} from './segments'; +import {RouteSegment, UrlSegment, Tree, TreeNode, rootNode, UrlTree, RouteTree, equalUrlSegments} from './segments'; import {RoutesMetadata, RouteMetadata} from './metadata/metadata'; import {Type, isBlank, isPresent, stringify} from './facade/lang'; import {ListWrapper, StringMapWrapper} from './facade/collection'; import {PromiseWrapper} from './facade/promise'; -import {BaseException} from '@angular/core'; +import {BaseException, ComponentFactory} from '@angular/core'; import {ComponentResolver} from '@angular/core'; import {DEFAULT_OUTLET_NAME} from './constants'; import {reflector} from '@angular/core'; export function recognize(componentResolver: ComponentResolver, rootComponent: Type, - url: UrlTree): Promise { + url: UrlTree, existingTree: RouteTree): Promise { let matched = new _MatchResult(rootComponent, [url.root], {}, rootNode(url).children, []); - return _constructSegment(componentResolver, matched).then(roots => new RouteTree(roots[0])); + return _constructSegment(componentResolver, matched, rootNode(existingTree)).then(roots => new RouteTree(roots[0])); } function _recognize(componentResolver: ComponentResolver, parentComponent: Type, - url: TreeNode): Promise[]> { + url: TreeNode, existingSegments: TreeNode[]): Promise[]> { let metadata = _readMetadata(parentComponent); // should read from the factory instead if (isBlank(metadata)) { throw new BaseException( @@ -29,42 +29,53 @@ function _recognize(componentResolver: ComponentResolver, parentComponent: Type, return PromiseWrapper.reject(e, null); } - let main = _constructSegment(componentResolver, match); + let segmentsWithRightOutlet = existingSegments.filter(r => r.value.outlet == match.outlet); + let segmentWithRightOutlet = segmentsWithRightOutlet.length > 0 ? segmentsWithRightOutlet[0] : null; + + let main = _constructSegment(componentResolver, match, segmentWithRightOutlet); let aux = - _recognizeMany(componentResolver, parentComponent, match.aux).then(_checkOutletNameUniqueness); + _recognizeMany(componentResolver, parentComponent, match.aux, existingSegments).then(_checkOutletNameUniqueness); return PromiseWrapper.all([main, aux]).then(ListWrapper.flatten); } function _recognizeMany(componentResolver: ComponentResolver, parentComponent: Type, - urls: TreeNode[]): Promise[]> { - let recognized = urls.map(u => _recognize(componentResolver, parentComponent, u)); + urls: TreeNode[], existingSegments: TreeNode[]): Promise[]> { + let recognized = urls.map(u => _recognize(componentResolver, parentComponent, u, existingSegments)); return PromiseWrapper.all(recognized).then(ListWrapper.flatten); } function _constructSegment(componentResolver: ComponentResolver, - matched: _MatchResult): Promise[]> { + matched: _MatchResult, existingSegment: TreeNode): Promise[]> { return componentResolver.resolveComponent(matched.component) .then(factory => { - let urlOutlet = matched.consumedUrlSegments.length === 0 || - isBlank(matched.consumedUrlSegments[0].outlet) ? - DEFAULT_OUTLET_NAME : - matched.consumedUrlSegments[0].outlet; - - let segment = new RouteSegment(matched.consumedUrlSegments, matched.parameters, urlOutlet, - factory.componentType, factory); + let segment = _createOrReuseSegment(matched, factory, existingSegment); + let existingChildren = isPresent(existingSegment) ? existingSegment.children : []; if (matched.leftOverUrl.length > 0) { - return _recognizeMany(componentResolver, factory.componentType, matched.leftOverUrl) + return _recognizeMany(componentResolver, factory.componentType, matched.leftOverUrl, existingChildren) .then(children => [new TreeNode(segment, children)]); } else { - return _recognizeLeftOvers(componentResolver, factory.componentType) + return _recognizeLeftOvers(componentResolver, factory.componentType, existingChildren) .then(children => [new TreeNode(segment, children)]); } }); } +function _createOrReuseSegment(matched: _MatchResult, factory: ComponentFactory, segmentNode: TreeNode): RouteSegment { + let segment = isPresent(segmentNode) ? segmentNode.value : null; + + if (isPresent(segment) && equalUrlSegments(segment.urlSegments, matched.consumedUrlSegments) + && StringMapWrapper.equals(segment.parameters, matched.parameters) && + segment.outlet == matched.outlet && factory.componentType == segment.type) { + return segment; + } else { + return new RouteSegment(matched.consumedUrlSegments, matched.parameters, matched.outlet, + factory.componentType, factory); + } +} + function _recognizeLeftOvers(componentResolver: ComponentResolver, - parentComponent: Type): Promise[]> { + parentComponent: Type, existingSegments: TreeNode[]): Promise[]> { return componentResolver.resolveComponent(parentComponent) .then(factory => { let metadata = _readMetadata(factory.componentType); @@ -76,12 +87,14 @@ function _recognizeLeftOvers(componentResolver: ComponentResolver, if (r.length === 0) { return PromiseWrapper.resolve([]); } else { - return _recognizeLeftOvers(componentResolver, r[0].component) - .then(children => { + let segmentsWithMatchingOutlet = existingSegments.filter(r => r.value.outlet == DEFAULT_OUTLET_NAME); + let segmentWithMatchingOutlet = segmentsWithMatchingOutlet.length > 0 ? segmentsWithMatchingOutlet[0] : null; + let existingChildren = isPresent(segmentWithMatchingOutlet) ? segmentWithMatchingOutlet.children : []; + + return _recognizeLeftOvers(componentResolver, r[0].component, existingChildren).then(children => { return componentResolver.resolveComponent(r[0].component) .then(factory => { - let segment = - new RouteSegment([], {}, DEFAULT_OUTLET_NAME, r[0].component, factory); + let segment = _createOrReuseSegment(new _MatchResult(r[0].component, [], {}, [], []), factory, segmentWithMatchingOutlet); return [new TreeNode(segment, children)]; }); }); @@ -167,6 +180,13 @@ class _MatchResult { constructor(public component: Type|string, public consumedUrlSegments: UrlSegment[], public parameters: {[key: string]: string}, public leftOverUrl: TreeNode[], public aux: TreeNode[]) {} + + get outlet():string { + return this.consumedUrlSegments.length === 0 || + isBlank(this.consumedUrlSegments[0].outlet) ? + DEFAULT_OUTLET_NAME : + this.consumedUrlSegments[0].outlet; + } } function _readMetadata(componentType: Type) { diff --git a/modules/@angular/router/src/router.ts b/modules/@angular/router/src/router.ts index da89028a97..e4ca90e9f3 100644 --- a/modules/@angular/router/src/router.ts +++ b/modules/@angular/router/src/router.ts @@ -12,7 +12,6 @@ import {Location} from '@angular/common'; import {link} from './link'; import { - equalSegments, routeSegmentComponentFactory, RouteSegment, UrlTree, @@ -20,7 +19,8 @@ import { rootNode, TreeNode, UrlSegment, - serializeRouteSegmentTree + serializeRouteSegmentTree, +createEmptyRouteTree } from './segments'; import {hasLifecycleHook} from './lifecycle_reflector'; import {DEFAULT_OUTLET_NAME} from './constants'; @@ -53,7 +53,7 @@ export class Router { private _componentResolver: ComponentResolver, private _urlSerializer: RouterUrlSerializer, private _routerOutletMap: RouterOutletMap, private _location: Location) { - this._routeTree = this._createInitialTree(); + this._routeTree = createEmptyRouteTree(this._rootComponentType); this._setUpLocationChangeListener(); this.navigateByUrl(this._location.path()); } @@ -146,12 +146,6 @@ export class Router { */ serializeUrl(url: UrlTree): string { return this._urlSerializer.serialize(url); } - private _createInitialTree(): RouteTree { - let root = new RouteSegment([new UrlSegment("", {}, null)], {}, DEFAULT_OUTLET_NAME, - this._rootComponentType, null); - return new RouteTree(new TreeNode(root, [])); - } - private _setUpLocationChangeListener(): void { this._locationSubscription = this._location.subscribe( (change) => { this._navigate(this._urlSerializer.parse(change['url'])); }); @@ -159,7 +153,7 @@ export class Router { private _navigate(url: UrlTree): Promise { this._urlTree = url; - return recognize(this._componentResolver, this._rootComponentType, url) + return recognize(this._componentResolver, this._rootComponentType, url, this._routeTree) .then(currTree => { return new _ActivateSegments(currTree, this._routeTree) .activate(this._routerOutletMap, this._rootComponent) @@ -244,7 +238,7 @@ class _ActivateSegments { let prev = isPresent(prevNode) ? prevNode.value : null; let outlet = this.getOutlet(parentOutletMap, currNode.value); - if (equalSegments(curr, prev)) { + if (curr === prev) { this.activateChildSegments(currNode, prevNode, outlet.outletMap, components.concat([outlet.component])); } else { diff --git a/modules/@angular/router/src/segments.ts b/modules/@angular/router/src/segments.ts index 40c2a1d595..9065d9b041 100644 --- a/modules/@angular/router/src/segments.ts +++ b/modules/@angular/router/src/segments.ts @@ -1,6 +1,7 @@ import {ComponentFactory, Type} from '@angular/core'; import {StringMapWrapper, ListWrapper} from './facade/collection'; import {isBlank, isPresent, stringify, NumberWrapper} from './facade/lang'; +import {DEFAULT_OUTLET_NAME} from './constants'; export class Tree { /** @internal */ @@ -43,8 +44,6 @@ export function rootNode(tree: Tree): TreeNode { } function _findNode(expected: T, c: TreeNode): TreeNode { - // TODO: vsavkin remove it once recognize is fixed - if (expected instanceof RouteSegment && equalSegments(expected, c.value)) return c; if (expected === c.value) return c; for (let cc of c.children) { let r = _findNode(expected, cc); @@ -55,9 +54,7 @@ function _findNode(expected: T, c: TreeNode): TreeNode { function _findPath(expected: T, c: TreeNode, collected: TreeNode[]): TreeNode[] { collected.push(c); - - // TODO: vsavkin remove it once recognize is fixed - if (_equalValues(expected, c.value)) return collected; + if (expected === c.value) return collected; for (let cc of c.children) { let r = _findPath(expected, cc, ListWrapper.clone(collected)); @@ -68,10 +65,10 @@ function _findPath(expected: T, c: TreeNode, collected: TreeNode[]): Tr } function _contains(tree: TreeNode, subtree: TreeNode): boolean { - if (!_equalValues(tree.value, subtree.value)) return false; + if (tree.value !== subtree.value) return false; for (let subtreeNode of subtree.children) { - let s = tree.children.filter(child => _equalValues(child.value, subtreeNode.value)); + let s = tree.children.filter(child => child.value === subtreeNode.value); if (s.length === 0) return false; if (!_contains(s[0], subtreeNode)) return false; } @@ -79,12 +76,6 @@ function _contains(tree: TreeNode, subtree: TreeNode): boolean { return true; } -function _equalValues(a: any, b: any): boolean { - // if (a instanceof RouteSegment) return equalSegments(a, b); - // if (a instanceof UrlSegment) return equalUrlSegments(a, b); - return a === b; -} - export class TreeNode { constructor(public value: T, public children: TreeNode[]) {} } @@ -131,6 +122,11 @@ export class RouteSegment { get stringifiedUrlSegments(): string { return this.urlSegments.map(s => s.toString()).join("/"); } } +export function createEmptyRouteTree(type:Type): RouteTree { + let root = new RouteSegment([new UrlSegment("", {}, null)], {}, DEFAULT_OUTLET_NAME, type, null); + return new RouteTree(new TreeNode(root, [])); +} + export function serializeRouteSegmentTree(tree: RouteTree): string { return _serializeRouteSegmentTree(tree._root); } @@ -141,26 +137,16 @@ function _serializeRouteSegmentTree(node: TreeNode): string { return `${v.outlet}:${v.stringifiedUrlSegments}(${stringify(v.type)}) [${children}]`; } -export function equalSegments(a: RouteSegment, b: RouteSegment): boolean { - if (isBlank(a) && !isBlank(b)) return false; - if (!isBlank(a) && isBlank(b)) return false; - if (a._type !== b._type) return false; - if (a.outlet != b.outlet) return false; - return StringMapWrapper.equals(a.parameters, b.parameters); -} +export function equalUrlSegments(a: UrlSegment[], b: UrlSegment[]): boolean { + if (a.length !== b.length) return false; -export function equalUrlSegments(a: UrlSegment, b: UrlSegment): boolean { - if (isBlank(a) && !isBlank(b)) return false; - if (!isBlank(a) && isBlank(b)) return false; - if (a.segment != b.segment) return false; - if (a.outlet != b.outlet) return false; - if (isBlank(a.parameters)) { - console.log("a", a); + for (let i = 0; i < a.length; ++i) { + if (a[i].segment != b[i].segment) return false; + if (a[i].outlet != b[i].outlet) return false; + if (!StringMapWrapper.equals(a[i].parameters, b[i].parameters)) return false; } - if (isBlank(b.parameters)) { - console.log("b", b); - } - return StringMapWrapper.equals(a.parameters, b.parameters); + + return true; } export function routeSegmentComponentFactory(a: RouteSegment): ComponentFactory { diff --git a/modules/@angular/router/test/recognize_spec.ts b/modules/@angular/router/test/recognize_spec.ts index 06d2cb59fa..f588a832c6 100644 --- a/modules/@angular/router/test/recognize_spec.ts +++ b/modules/@angular/router/test/recognize_spec.ts @@ -15,15 +15,17 @@ import { import {recognize} from '../src/recognize'; import {Routes, Route} from '@angular/router'; import {provide, Component, ComponentResolver} from '@angular/core'; -import {UrlSegment, RouteTree, UrlTree} from '../src/segments'; +import {UrlSegment, RouteTree, UrlTree, createEmptyRouteTree} from '../src/segments'; import {DefaultRouterUrlSerializer} from '../src/router_url_serializer'; import {DEFAULT_OUTLET_NAME} from '../src/constants'; export function main() { describe('recognize', () => { + let emptyRouteTree = createEmptyRouteTree(ComponentA); + it('should handle position args', inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { - recognize(resolver, ComponentA, tree("b/paramB/c/paramC/d")) + recognize(resolver, ComponentA, tree("b/paramB/c/paramC/d"), emptyRouteTree) .then(r => { let a = r.root; expect(stringifyUrl(a.urlSegments)).toEqual([""]); @@ -47,7 +49,7 @@ export function main() { it('should support empty routes', inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { - recognize(resolver, ComponentA, tree("f")) + recognize(resolver, ComponentA, tree("f"), emptyRouteTree) .then(r => { let a = r.root; expect(stringifyUrl(a.urlSegments)).toEqual([""]); @@ -67,7 +69,7 @@ export function main() { it('should handle aux routes', inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { - recognize(resolver, ComponentA, tree("b/paramB(/d//right:d)")) + recognize(resolver, ComponentA, tree("b/paramB(/d//right:d)"), emptyRouteTree) .then(r => { let c = r.children(r.root); expect(stringifyUrl(c[0].urlSegments)).toEqual(["b", "paramB"]); @@ -88,7 +90,7 @@ export function main() { 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)")) + recognize(resolver, ComponentA, tree("b/paramB(right:d//right:e)"), emptyRouteTree) .catch(e => { expect(e.message).toEqual( "Two segments cannot have the same outlet name: 'right:d' and 'right:e'."); @@ -98,7 +100,7 @@ export function main() { it('should handle nested aux routes', inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { - recognize(resolver, ComponentA, tree("b/paramB(/d(right:e))")) + recognize(resolver, ComponentA, tree("b/paramB(/d(right:e))"), emptyRouteTree) .then(r => { let c = r.children(r.root); expect(stringifyUrl(c[0].urlSegments)).toEqual(["b", "paramB"]); @@ -119,7 +121,7 @@ export function main() { it('should handle non top-level aux routes', inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { - recognize(resolver, ComponentA, tree('b/paramB/d(e)')) + recognize(resolver, ComponentA, tree('b/paramB/d(e)'), emptyRouteTree) .then(r => { let c = r.children(r.firstChild(r.root)); expect(stringifyUrl(c[0].urlSegments)).toEqual(["d"]); @@ -136,7 +138,7 @@ export function main() { 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)")) + recognize(resolver, ComponentA, tree("b/paramB;b1=1;b2=2(/d;d1=1;d2=2)"), emptyRouteTree) .then(r => { let c = r.children(r.root); expect(c[0].parameters).toEqual({'b': 'paramB', 'b1': '1', 'b2': '2'}); @@ -148,7 +150,7 @@ export function main() { it('should match a wildcard', inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { - recognize(resolver, ComponentG, tree("a;aa=1/b;bb=2")) + recognize(resolver, ComponentG, tree("a;aa=1/b;bb=2"), emptyRouteTree) .then(r => { let c = r.children(r.root); expect(c.length).toEqual(1); @@ -161,7 +163,7 @@ export function main() { it('should error when no matching routes', inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { - recognize(resolver, ComponentA, tree("invalid")) + recognize(resolver, ComponentA, tree("invalid"), emptyRouteTree) .catch(e => { expect(e.message).toContain("Cannot match any routes"); async.done(); @@ -170,7 +172,7 @@ export function main() { it('should handle no matching routes (too short)', inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { - recognize(resolver, ComponentA, tree("b")) + recognize(resolver, ComponentA, tree("b"), emptyRouteTree) .catch(e => { expect(e.message).toContain("Cannot match any routes"); async.done(); @@ -179,13 +181,27 @@ 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"), emptyRouteTree) .catch(e => { expect(e.message) .toEqual("Component 'ComponentD' does not have route configuration"); async.done(); }); })); + + it("should reuse existing segments", + inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => { + recognize(resolver, ComponentA, tree("/b/1/d"), emptyRouteTree).then(t1 => { + recognize(resolver, ComponentA, tree("/b/1/e"), t1).then(t2 => { + expect(t1.root).toBe(t2.root); + expect(t1.firstChild(t1.root)).toBe(t2.firstChild(t2.root)); + expect(t1.firstChild(t1.firstChild(t1.root))).not.toBe( + t2.firstChild(t2.firstChild(t2.root))); + + async.done(); + }); + }); + })); }); }