diff --git a/modules/@angular/router/src/link.ts b/modules/@angular/router/src/link.ts index 4068ce82f7..a2d05ae0c9 100644 --- a/modules/@angular/router/src/link.ts +++ b/modules/@angular/router/src/link.ts @@ -1,116 +1,184 @@ import {Tree, TreeNode, UrlSegment, RouteSegment, rootNode, UrlTree, RouteTree} from './segments'; import {isBlank, isPresent, isString, isStringMap} from './facade/lang'; +import {BaseException} from './facade/exceptions'; import {ListWrapper} from './facade/collection'; -export function link(segment: RouteSegment, routeTree: RouteTree, urlTree: UrlTree, - change: any[]): UrlTree { - if (change.length === 0) return urlTree; +export function link(segment: RouteSegment, routeTree: RouteTree, urlTree: UrlTree, commands: any[]): UrlTree { + if (commands.length === 0) return urlTree; - let startingNode; - let normalizedChange; - - if (isString(change[0]) && change[0].startsWith("./")) { - normalizedChange = ["/", change[0].substring(2)].concat(change.slice(1)); - startingNode = _findStartingNode(_findUrlSegment(segment, routeTree), rootNode(urlTree)); - - } else if (isString(change[0]) && change.length === 1 && change[0] == "/") { - normalizedChange = change; - startingNode = rootNode(urlTree); - - } else if (isString(change[0]) && !change[0].startsWith("/")) { - normalizedChange = ["/"].concat(change); - startingNode = _findStartingNode(_findUrlSegment(segment, routeTree), rootNode(urlTree)); - - } else { - normalizedChange = ["/"].concat(change); - startingNode = rootNode(urlTree); + let normalizedCommands = _normalizeCommands(commands); + if (_navigateToRoot(normalizedCommands)) { + return new UrlTree(new TreeNode(urlTree.root, [])); } - let updated = _update(startingNode, normalizedChange); + let startingNode = _findStartingNode(normalizedCommands, urlTree, segment, routeTree); + let updated = normalizedCommands.commands.length > 0 ? + _updateMany(ListWrapper.clone(startingNode.children), normalizedCommands.commands) : []; let newRoot = _constructNewTree(rootNode(urlTree), startingNode, updated); return new UrlTree(newRoot); } -function _findUrlSegment(segment: RouteSegment, routeTree: RouteTree): UrlSegment { - let s = segment; - let res = null; - while (isBlank(res)) { - res = ListWrapper.last(s.urlSegments); - s = routeTree.parent(s); - } - return res; +function _navigateToRoot(normalizedChange:_NormalizedNavigationCommands):boolean { + return normalizedChange.isAbsolute && normalizedChange.commands.length === 1 && normalizedChange.commands[0] == "/"; } -function _findStartingNode(segment: UrlSegment, node: TreeNode): TreeNode { +class _NormalizedNavigationCommands { + constructor(public isAbsolute: boolean, + public numberOfDoubleDots: number, + public commands: any[]) {} +} + +function _normalizeCommands(commands: any[]): _NormalizedNavigationCommands {;'' + if (isString(commands[0]) && commands.length === 1 && commands[0] == "/") { + return new _NormalizedNavigationCommands(true, 0, commands); + } + + let numberOfDoubleDots = 0; + let isAbsolute = false; + let res = []; + + for (let i = 0; i < commands.length; ++i) { + let c = commands[i]; + + if (!isString(c)) { + res.push(c); + continue; + } + + let parts = c.split('/'); + for (let j = 0; j < parts.length; ++j) { + let cc = parts[j]; + + // first exp is treated in a special way + if (i == 0) { + if (j == 0 && cc == ".") { // './a' + // skip it + } else if (j == 0 && cc == "") { // '/a' + isAbsolute = true; + } else if (cc == "..") { // '../a' + numberOfDoubleDots++; + } else if (cc != '') { + res.push(cc); + } + + } else { + if (cc != ''){ + res.push(cc); + } + } + } + } + + return new _NormalizedNavigationCommands(isAbsolute, numberOfDoubleDots, res); +} + +function _findUrlSegment(segment: RouteSegment, routeTree: RouteTree, urlTree: UrlTree, numberOfDoubleDots: number): UrlSegment { + let s = segment; + while (s.urlSegments.length === 0) { + s = routeTree.parent(s); + } + let urlSegment = ListWrapper.last(s.urlSegments); + let path = urlTree.pathFromRoot(urlSegment); + if (path.length <= numberOfDoubleDots) { + throw new BaseException("Invalid number of '../'"); + } + return path[path.length - 1 - numberOfDoubleDots]; +} + +function _findStartingNode(normalizedChange:_NormalizedNavigationCommands, urlTree:UrlTree, segment:RouteSegment, routeTree:RouteTree):TreeNode { + if (normalizedChange.isAbsolute) { + return rootNode(urlTree); + } else { + let urlSegment = _findUrlSegment(segment, routeTree, urlTree, normalizedChange.numberOfDoubleDots); + return _findMatchingNode(urlSegment, rootNode(urlTree)); + } +} + +function _findMatchingNode(segment: UrlSegment, node: TreeNode): TreeNode { if (node.value === segment) return node; for (var c of node.children) { - let r = _findStartingNode(segment, c); + let r = _findMatchingNode(segment, c); if (isPresent(r)) return r; } return null; } function _constructNewTree(node: TreeNode, original: TreeNode, - updated: TreeNode): TreeNode { + updated: TreeNode[]): TreeNode { if (node === original) { - return new TreeNode(node.value, updated.children); + return new TreeNode(node.value, updated); } else { return new TreeNode( node.value, node.children.map(c => _constructNewTree(c, original, updated))); } } -function _update(node: TreeNode, changes: any[]): TreeNode { - let rest = changes.slice(1); - let outlet = _outlet(changes); - let segment = _segment(changes); - if (isString(segment) && segment[0] == "/") segment = segment.substring(1); +function _update(node: TreeNode, commands: any[]): TreeNode { + let rest = commands.slice(1); + let next = rest.length === 0 ? null : rest[0]; + let outlet = _outlet(commands); + let segment = _segment(commands); // reach the end of the tree => create new tree nodes. - if (isBlank(node)) { - let urlSegment = new UrlSegment(segment, null, outlet); + if (isBlank(node) && !isStringMap(next)) { + let urlSegment = new UrlSegment(segment, {}, outlet); let children = rest.length === 0 ? [] : [_update(null, rest)]; return new TreeNode(urlSegment, children); - // different outlet => preserve the subtree + } else if (isBlank(node) && isStringMap(next)) { + let urlSegment = new UrlSegment(segment, next, outlet); + return _recurse(urlSegment, node, rest.slice(1)); + + // different outlet => preserve the subtree } else if (outlet != node.value.outlet) { return node; - // same outlet => modify the subtree - } else { - let urlSegment = isStringMap(segment) ? new UrlSegment(null, segment, null) : - new UrlSegment(segment, null, outlet); - if (rest.length === 0) { - return new TreeNode(urlSegment, []); - } + // params command + } else if (isStringMap(segment)) { + let newSegment = new UrlSegment(node.value.segment, segment, node.value.outlet); + return _recurse(newSegment, node, rest); - return new TreeNode(urlSegment, - _updateMany(ListWrapper.clone(node.children), rest)); + // next one is a params command + } else if (isStringMap(next)) { + let urlSegment = new UrlSegment(segment, next, outlet); + return _recurse(urlSegment, node, rest.slice(1)); + + // next one is not a params command + } else { + let urlSegment = new UrlSegment(segment, {}, outlet); + return _recurse(urlSegment, node, rest); } } -function _updateMany(nodes: TreeNode[], changes: any[]): TreeNode[] { - let outlet = _outlet(changes); +function _recurse(urlSegment: UrlSegment, node: TreeNode, rest: any[]): TreeNode { + if (rest.length === 0) { + return new TreeNode(urlSegment, []); + } + return new TreeNode(urlSegment, _updateMany(ListWrapper.clone(node.children), rest)); +} + +function _updateMany(nodes: TreeNode[], commands: any[]): TreeNode[] { + let outlet = _outlet(commands); let nodesInRightOutlet = nodes.filter(c => c.value.outlet == outlet); if (nodesInRightOutlet.length > 0) { let nodeRightOutlet = nodesInRightOutlet[0]; // there can be only one - nodes[nodes.indexOf(nodeRightOutlet)] = _update(nodeRightOutlet, changes); + nodes[nodes.indexOf(nodeRightOutlet)] = _update(nodeRightOutlet, commands); } else { - nodes.push(_update(null, changes)); + nodes.push(_update(null, commands)); } return nodes; } -function _segment(changes: any[]): any { - if (!isString(changes[0])) return changes[0]; - let parts = changes[0].toString().split(":"); - return parts.length > 1 ? parts[1] : changes[0]; +function _segment(commands: any[]): any { + if (!isString(commands[0])) return commands[0]; + let parts = commands[0].toString().split(":"); + return parts.length > 1 ? parts[1] : commands[0]; } -function _outlet(changes: any[]): string { - if (!isString(changes[0])) return null; - let parts = changes[0].toString().split(":"); +function _outlet(commands: any[]): string { + if (!isString(commands[0])) return null; + let parts = commands[0].toString().split(":"); return parts.length > 1 ? parts[0] : null; } diff --git a/modules/@angular/router/src/recognize.ts b/modules/@angular/router/src/recognize.ts index 5fdcf9fcb0..972c034b77 100644 --- a/modules/@angular/router/src/recognize.ts +++ b/modules/@angular/router/src/recognize.ts @@ -11,7 +11,7 @@ import {reflector} from '@angular/core'; // TODO: vsavkin: recognize should take the old tree and merge it export function recognize(componentResolver: ComponentResolver, type: Type, url: UrlTree): Promise { - let matched = new _MatchResult(type, [url.root], null, rootNode(url).children, []); + let matched = new _MatchResult(type, [url.root], {}, rootNode(url).children, []); return _constructSegment(componentResolver, matched).then(roots => new RouteTree(roots[0])); } @@ -82,7 +82,7 @@ function _recognizeLeftOvers(componentResolver: ComponentResolver, return componentResolver.resolveComponent(r[0].component) .then(factory => { let segment = - new RouteSegment([], null, DEFAULT_OUTLET_NAME, r[0].component, factory); + new RouteSegment([], {}, DEFAULT_OUTLET_NAME, r[0].component, factory); return [new TreeNode(segment, children)]; }); }); @@ -142,14 +142,9 @@ function _matchWithParts(route: RouteMetadata, url: TreeNode): _Matc current = ListWrapper.first(current.children); } - if (isPresent(current) && isBlank(current.value.segment)) { - lastParent = lastSegment; - lastSegment = current; - } - let p = lastSegment.value.parameters; let parameters = - <{[key: string]: string}>StringMapWrapper.merge(isBlank(p) ? {} : p, positionalParams); + <{[key: string]: string}>StringMapWrapper.merge(p, positionalParams); let axuUrlSubtrees = isPresent(lastParent) ? lastParent.children.slice(1) : []; return new _MatchResult(route.component, consumedUrlSegments, parameters, lastSegment.children, diff --git a/modules/@angular/router/src/router_url_serializer.ts b/modules/@angular/router/src/router_url_serializer.ts index e68d3958f0..ada4462f3d 100644 --- a/modules/@angular/router/src/router_url_serializer.ts +++ b/modules/@angular/router/src/router_url_serializer.ts @@ -30,8 +30,7 @@ function _serializeUrlTreeNodes(nodes: TreeNode[]): string { function _serializeChildren(node: TreeNode): string { if (node.children.length > 0) { - let slash = isBlank(node.children[0].value.segment) ? "" : "/"; - return `${slash}${_serializeUrlTreeNodes(node.children)}`; + return `/${_serializeUrlTreeNodes(node.children)}`; } else { return ""; } @@ -63,7 +62,7 @@ class _UrlParser { parse(url: string): TreeNode { this._remaining = url; if (url == '' || url == '/') { - return new TreeNode(new UrlSegment('', null, null), []); + return new TreeNode(new UrlSegment('', {}, null), []); } else { return this.parseRoot(); } @@ -71,8 +70,7 @@ class _UrlParser { parseRoot(): TreeNode { let segments = this.parseSegments(); - let queryParams = this.peekStartsWith('?') ? this.parseQueryParams() : null; - return new TreeNode(new UrlSegment('', queryParams, null), segments); + return new TreeNode(new UrlSegment('', {}, null), segments); } parseSegments(outletName: string = null): TreeNode[] { @@ -92,7 +90,7 @@ class _UrlParser { path = parts[1]; } - var matrixParams: {[key: string]: any} = null; + var matrixParams: {[key: string]: any} = {}; if (this.peekStartsWith(';')) { matrixParams = this.parseMatrixParams(); } @@ -108,16 +106,9 @@ class _UrlParser { children = this.parseSegments(); } - if (isPresent(matrixParams)) { - let matrixParamsSegment = new UrlSegment(null, matrixParams, null); - let matrixParamsNode = new TreeNode(matrixParamsSegment, children); - let segment = new UrlSegment(path, null, outletName); - return [new TreeNode(segment, [matrixParamsNode].concat(aux))]; - } else { - let segment = new UrlSegment(path, null, outletName); - let node = new TreeNode(segment, children); - return [node].concat(aux); - } + let segment = new UrlSegment(path, matrixParams, outletName); + let node = new TreeNode(segment, children); + return [node].concat(aux); } parseQueryParams(): {[key: string]: any} { diff --git a/modules/@angular/router/src/segments.ts b/modules/@angular/router/src/segments.ts index 70d4ec8914..ea2228e71e 100644 --- a/modules/@angular/router/src/segments.ts +++ b/modules/@angular/router/src/segments.ts @@ -90,21 +90,18 @@ export class TreeNode { } export class UrlSegment { - constructor(public segment: any, public parameters: {[key: string]: string}, + constructor(public segment: any, public parameters: {[key: string]: any}, public outlet: string) {} toString(): string { let outletPrefix = isBlank(this.outlet) ? "" : `${this.outlet}:`; - let segmentPrefix = isBlank(this.segment) ? "" : this.segment; - return `${outletPrefix}${segmentPrefix}${_serializeParams(this.parameters)}`; + return `${outletPrefix}${this.segment}${_serializeParams(this.parameters)}`; } } function _serializeParams(params: {[key: string]: string}): string { let res = ""; - if (isPresent(params)) { - StringMapWrapper.forEach(params, (v, k) => res += `;${k}=${v}`); - } + StringMapWrapper.forEach(params, (v, k) => res += `;${k}=${v}`); return res; } @@ -115,7 +112,7 @@ export class RouteSegment { /** @internal */ _componentFactory: ComponentFactory; - constructor(public urlSegments: UrlSegment[], public parameters: {[key: string]: string}, + constructor(public urlSegments: UrlSegment[], public parameters: {[key: string]: any}, public outlet: string, type: Type, componentFactory: ComponentFactory) { this._type = type; this._componentFactory = componentFactory; @@ -145,9 +142,6 @@ export function equalSegments(a: RouteSegment, b: RouteSegment): boolean { if (!isBlank(a) && isBlank(b)) return false; if (a._type !== b._type) return false; if (a.outlet != b.outlet) return false; - if (isBlank(a.parameters) && !isBlank(b.parameters)) return false; - if (!isBlank(a.parameters) && isBlank(b.parameters)) return false; - if (isBlank(a.parameters) && isBlank(b.parameters)) return true; return StringMapWrapper.equals(a.parameters, b.parameters); } @@ -156,9 +150,12 @@ export function equalUrlSegments(a: UrlSegment, b: UrlSegment): boolean { 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) && !isBlank(b.parameters)) return false; - if (!isBlank(a.parameters) && isBlank(b.parameters)) return false; - if (isBlank(a.parameters) && isBlank(b.parameters)) return true; + if (isBlank(a.parameters)) { + console.log("a", a); + } + if (isBlank(b.parameters)) { + console.log("b", b); + } return StringMapWrapper.equals(a.parameters, b.parameters); } diff --git a/modules/@angular/router/test/link_spec.ts b/modules/@angular/router/test/link_spec.ts index e844126a4d..215e6bbbed 100644 --- a/modules/@angular/router/test/link_spec.ts +++ b/modules/@angular/router/test/link_spec.ts @@ -27,68 +27,142 @@ export function main() { expect(t).toBe(p); }); - it("should support going to root", () => { + it("should navigate to the root", () => { let p = parser.parse("/"); let tree = s(p.root); let t = link(tree.root, tree, p, ["/"]); expect(parser.serialize(t)).toEqual(""); }); - it("should support positional params", () => { + it("should support nested segments", () => { let p = parser.parse("/a/b"); let tree = s(p.firstChild(p.root)); let t = link(tree.root, tree, p, ["/one", 11, "two", 22]); expect(parser.serialize(t)).toEqual("/one/11/two/22"); }); - it("should preserve route siblings when changing the main route", () => { + it("should preserve siblings", () => { let p = parser.parse("/a/11/b(c)"); let tree = s(p.root); let t = link(tree.root, tree, p, ["/a", 11, 'd']); expect(parser.serialize(t)).toEqual("/a/11/d(aux:c)"); }); - it("should preserve route siblings when changing a aux route", () => { - let p = parser.parse("/a/11/b(c)"); - let tree = s(p.root); - let t = link(tree.root, tree, p, ["/a", 11, 'aux:d']); - expect(parser.serialize(t)).toEqual("/a/11/b(aux:d)"); - }); - - it('should update parameters', () => { + it('should update matrix parameters', () => { let p = parser.parse("/a;aa=11"); let tree = s(p.root); let t = link(tree.root, tree, p, ["/a", {aa: 22, bb: 33}]); expect(parser.serialize(t)).toEqual("/a;aa=22;bb=33"); }); - it("should update relative subtree (when starts with ./)", () => { - let p = parser.parse("/a(ap)/c(cp)"); - let c = p.firstChild(p.root); - let tree = s(c); - let t = link(tree.root, tree, p, ["./c2"]); - expect(parser.serialize(t)).toEqual("/a(aux:ap)/c2(aux:cp)"); + it('should create matrix parameters', () => { + let p = parser.parse("/a"); + let tree = s(p.root); + let t = link(tree.root, tree, p, ["/a", {aa: 22, bb: 33}]); + expect(parser.serialize(t)).toEqual("/a;aa=22;bb=33"); }); - it("should update relative subtree (when does not start with ./)", () => { - let p = parser.parse("/a(ap)/c(cp)"); - let c = p.firstChild(p.root); - let tree = s(c); - let t = link(tree.root, tree, p, ["c2"]); - expect(parser.serialize(t)).toEqual("/a(aux:ap)/c2(aux:cp)"); + it('should create matrix parameters together with other segments', () => { + let p = parser.parse("/a"); + let tree = s(p.root); + let t = link(tree.root, tree, p, ["/a", "/b", {aa: 22, bb: 33}]); + expect(parser.serialize(t)).toEqual("/a/b;aa=22;bb=33"); }); - it("should update relative subtree when the provided segment doesn't have url segments", () => { - let p = parser.parse("/a(ap)/c(cp)"); - let c = p.firstChild(p.root); + describe("relative navigation", () => { + it("should work", () => { + let p = parser.parse("/a(ap)/c(cp)"); + let c = p.firstChild(p.root); + let tree = s(c); + let t = link(tree.root, tree, p, ["c2"]); + expect(parser.serialize(t)).toEqual("/a(aux:ap)/c2(aux:cp)"); + }); - let child = new RouteSegment([], null, null, null, null); - let root = new TreeNode(new RouteSegment([c], {}, null, null, null), - [new TreeNode(child, [])]); - let tree = new RouteTree(root); + it("should work when the first command starts with a ./", () => { + let p = parser.parse("/a(ap)/c(cp)"); + let c = p.firstChild(p.root); + let tree = s(c); + let t = link(tree.root, tree, p, ["./c2"]); + expect(parser.serialize(t)).toEqual("/a(aux:ap)/c2(aux:cp)"); + }); - let t = link(child, tree, p, ["./c2"]); - expect(parser.serialize(t)).toEqual("/a(aux:ap)/c2(aux:cp)"); + it("should work when the first command is ./)", () => { + let p = parser.parse("/a(ap)/c(cp)"); + let c = p.firstChild(p.root); + let tree = s(c); + let t = link(tree.root, tree, p, ["./", "c2"]); + expect(parser.serialize(t)).toEqual("/a(aux:ap)/c2(aux:cp)"); + }); + + it("should work when given params", () => { + let p = parser.parse("/a(ap)/c(cp)"); + let c = p.firstChild(p.root); + let tree = s(c); + let t = link(tree.root, tree, p, [{'x': 99}]); + + expect(parser.serialize(t)).toEqual("/a(aux:ap)/c;x=99(aux:cp)"); + }); + + it("should support going to a parent", () => { + let p = parser.parse("/a(ap)/c(cp)"); + let a = p.firstChild(p.root); + let tree = s(a); + let t = link(tree.root, tree, p, ["../a2"]); + expect(parser.serialize(t)).toEqual("/a2(aux:ap)"); + }); + + it("should support going to a parent (nested case)", () => { + let p = parser.parse("/a/c"); + let c = p.firstChild(p.firstChild(p.root)); + let tree = s(c); + let t = link(tree.root, tree, p, ["../c2"]); + expect(parser.serialize(t)).toEqual("/a/c2"); + }); + + it("should work when given ../", () => { + let p = parser.parse("/a/c"); + let c = p.firstChild(p.firstChild(p.root)); + let tree = s(c); + let t = link(tree.root, tree, p, ["../"]); + expect(parser.serialize(t)).toEqual("/a"); + }); + + it("should navigate to the root", () => { + let p = parser.parse("/a/c"); + let c = p.firstChild(p.root); + let tree = s(c); + let t = link(tree.root, tree, p, ["../"]); + expect(parser.serialize(t)).toEqual(""); + }); + + it("should support setting matrix params", () => { + let p = parser.parse("/a(ap)/c(cp)"); + let c = p.firstChild(p.root); + let tree = s(c); + let t = link(tree.root, tree, p, ["../", {'x': 5}]); + expect(parser.serialize(t)).toEqual("/a;x=5(aux:ap)"); + }); + + it("should throw when too many ..", () => { + let p = parser.parse("/a(ap)/c(cp)"); + let c = p.firstChild(p.root); + let tree = s(c); + + expect(() => link(tree.root, tree, p, ["../../"])).toThrowError("Invalid number of '../'"); + }); + + it("should work when the provided segment doesn't have url segments", () => { + let p = parser.parse("/a(ap)/c(cp)"); + let c = p.firstChild(p.root); + + let child = new RouteSegment([], {'one':1}, null, null, null); + let root = new TreeNode(new RouteSegment([c], {}, null, null, null), + [new TreeNode(child, [])]); + let tree = new RouteTree(root); + + let t = link(child, tree, p, ["./c2"]); + expect(parser.serialize(t)).toEqual("/a(aux:ap)/c2(aux:cp)"); + }); }); }); } diff --git a/modules/@angular/router/test/router_url_serializer_spec.ts b/modules/@angular/router/test/router_url_serializer_spec.ts index 688dc32fde..4dd1c9badc 100644 --- a/modules/@angular/router/test/router_url_serializer_spec.ts +++ b/modules/@angular/router/test/router_url_serializer_spec.ts @@ -79,17 +79,11 @@ export function main() { it("should parse key-value matrix params", () => { let tree = url.parse("/one;a=11a;b=11b(/two;c=22//right:three;d=33)"); + let c = tree.children(tree.root); - let c = tree.firstChild(tree.root); - expectSegment(c, "one"); - - let c2 = tree.children(c); - expectSegment(c2[0], ";a=11a;b=11b"); - expectSegment(c2[1], "aux:two"); - expectSegment(c2[2], "right:three"); - - expectSegment(tree.firstChild(c2[1]), ";c=22"); - expectSegment(tree.firstChild(c2[2]), ";d=33"); + expectSegment(c[0], "one;a=11a;b=11b"); + expectSegment(c[1], "aux:two;c=22"); + expectSegment(c[2], "right:three;d=33"); expect(url.serialize(tree)).toEqual("/one;a=11a;b=11b(aux:two;c=22//right:three;d=33)"); }); @@ -98,8 +92,7 @@ export function main() { let tree = url.parse("/one;a"); let c = tree.firstChild(tree.root); - expectSegment(c, "one"); - expectSegment(tree.firstChild(c), ";a=true"); + expectSegment(c, "one;a=true"); expect(url.serialize(tree)).toEqual("/one;a=true"); });