refactor(router): update link to reuse url segments when possible

This commit is contained in:
vsavkin 2016-05-04 11:46:38 -07:00
parent 12637a761c
commit d00b26d941
4 changed files with 81 additions and 38 deletions

View File

@ -1,11 +1,10 @@
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';
import {ListWrapper, StringMapWrapper} from './facade/collection';
// TODO: vsavkin: should reuse segments
export function link(segment: RouteSegment, routeTree: RouteTree, urlTree: UrlTree,
commands: any[]): UrlTree {
export function link(segment: RouteSegment, routeTree: RouteTree, urlTree: UrlTree, commands: any[]): UrlTree {
if (commands.length === 0) return urlTree;
let normalizedCommands = _normalizeCommands(commands);
@ -14,22 +13,20 @@ export function link(segment: RouteSegment, routeTree: RouteTree, urlTree: UrlTr
}
let startingNode = _findStartingNode(normalizedCommands, urlTree, segment, routeTree);
let updated =
normalizedCommands.commands.length > 0 ?
_updateMany(ListWrapper.clone(startingNode.children), normalizedCommands.commands) :
[];
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 _navigateToRoot(normalizedChange: _NormalizedNavigationCommands): boolean {
return normalizedChange.isAbsolute && normalizedChange.commands.length === 1 &&
normalizedChange.commands[0] == "/";
function _navigateToRoot(normalizedChange:_NormalizedNavigationCommands):boolean {
return normalizedChange.isAbsolute && normalizedChange.commands.length === 1 && normalizedChange.commands[0] == "/";
}
class _NormalizedNavigationCommands {
constructor(public isAbsolute: boolean, public numberOfDoubleDots: number,
constructor(public isAbsolute: boolean,
public numberOfDoubleDots: number,
public commands: any[]) {}
}
@ -56,18 +53,18 @@ function _normalizeCommands(commands: any[]): _NormalizedNavigationCommands {
// first exp is treated in a special way
if (i == 0) {
if (j == 0 && cc == ".") { // './a'
if (j == 0 && cc == ".") { // './a'
// skip it
} else if (j == 0 && cc == "") { // '/a'
} else if (j == 0 && cc == "") { // '/a'
isAbsolute = true;
} else if (cc == "..") { // '../a'
} else if (cc == "..") { // '../a'
numberOfDoubleDots++;
} else if (cc != '') {
res.push(cc);
}
} else {
if (cc != '') {
if (cc != ''){
res.push(cc);
}
}
@ -77,8 +74,7 @@ function _normalizeCommands(commands: any[]): _NormalizedNavigationCommands {
return new _NormalizedNavigationCommands(isAbsolute, numberOfDoubleDots, res);
}
function _findUrlSegment(segment: RouteSegment, routeTree: RouteTree, urlTree: UrlTree,
numberOfDoubleDots: number): UrlSegment {
function _findUrlSegment(segment: RouteSegment, routeTree: RouteTree, urlTree: UrlTree, numberOfDoubleDots: number): UrlSegment {
let s = segment;
while (s.urlSegments.length === 0) {
s = routeTree.parent(s);
@ -91,13 +87,11 @@ function _findUrlSegment(segment: RouteSegment, routeTree: RouteTree, urlTree: U
return path[path.length - 1 - numberOfDoubleDots];
}
function _findStartingNode(normalizedChange: _NormalizedNavigationCommands, urlTree: UrlTree,
segment: RouteSegment, routeTree: RouteTree): TreeNode<UrlSegment> {
function _findStartingNode(normalizedChange:_NormalizedNavigationCommands, urlTree:UrlTree, segment:RouteSegment, routeTree:RouteTree):TreeNode<UrlSegment> {
if (normalizedChange.isAbsolute) {
return rootNode(urlTree);
} else {
let urlSegment =
_findUrlSegment(segment, routeTree, urlTree, normalizedChange.numberOfDoubleDots);
let urlSegment = _findUrlSegment(segment, routeTree, urlTree, normalizedChange.numberOfDoubleDots);
return _findMatchingNode(urlSegment, rootNode(urlTree));
}
}
@ -117,7 +111,7 @@ function _constructNewTree(node: TreeNode<UrlSegment>, original: TreeNode<UrlSeg
return new TreeNode<UrlSegment>(node.value, updated);
} else {
return new TreeNode<UrlSegment>(
node.value, node.children.map(c => _constructNewTree(c, original, updated)));
node.value, node.children.map(c => _constructNewTree(c, original, updated)));
}
}
@ -134,7 +128,7 @@ function _update(node: TreeNode<UrlSegment>, commands: any[]): TreeNode<UrlSegme
return new TreeNode<UrlSegment>(urlSegment, children);
} else if (isBlank(node) && isStringMap(next)) {
let urlSegment = new UrlSegment(segment, next, outlet);
let urlSegment = new UrlSegment(segment, _stringify(next), outlet);
return _recurse(urlSegment, node, rest.slice(1));
// different outlet => preserve the subtree
@ -143,23 +137,40 @@ function _update(node: TreeNode<UrlSegment>, commands: any[]): TreeNode<UrlSegme
// params command
} else if (isStringMap(segment)) {
let newSegment = new UrlSegment(node.value.segment, segment, node.value.outlet);
let newSegment = new UrlSegment(node.value.segment, _stringify(segment), node.value.outlet);
return _recurse(newSegment, node, rest);
// next one is a params command
// next one is a params command && can reuse the node
} else if (isStringMap(next) && _compare(segment, _stringify(next), node.value)){
return _recurse(node.value, node, rest.slice(1));
// next one is a params command && cannot reuse the node
} else if (isStringMap(next)) {
let urlSegment = new UrlSegment(segment, next, outlet);
let urlSegment = new UrlSegment(segment, _stringify(next), outlet);
return _recurse(urlSegment, node, rest.slice(1));
// next one is not a params command
// next one is not a params command && can reuse the node
} else if (_compare(segment, {}, node.value)) {
return _recurse(node.value, node, rest);
// next one is not a params command && cannot reuse the node
} else {
let urlSegment = new UrlSegment(segment, {}, outlet);
return _recurse(urlSegment, node, rest);
}
}
function _recurse(urlSegment: UrlSegment, node: TreeNode<UrlSegment>,
rest: any[]): TreeNode<UrlSegment> {
function _stringify(params: {[key: string]: any}):{[key: string]: string} {
let res = {};
StringMapWrapper.forEach(params, (v, k) => res[k] = v.toString());
return res;
}
function _compare(path: string, params: {[key: string]: any}, segment: UrlSegment): boolean {
return path == segment.segment && StringMapWrapper.equals(params, segment.parameters);
}
function _recurse(urlSegment: UrlSegment, node: TreeNode<UrlSegment>, rest: any[]): TreeNode<UrlSegment> {
if (rest.length === 0) {
return new TreeNode<UrlSegment>(urlSegment, []);
}

View File

@ -1,6 +1,6 @@
import {ComponentFactory, Type} from '@angular/core';
import {StringMapWrapper, ListWrapper} from './facade/collection';
import {isBlank, isPresent, stringify} from './facade/lang';
import {isBlank, isPresent, stringify, NumberWrapper} from './facade/lang';
export class Tree<T> {
/** @internal */
@ -80,8 +80,8 @@ function _contains<T>(tree: TreeNode<T>, subtree: TreeNode<T>): boolean {
}
function _equalValues(a: any, b: any): boolean {
if (a instanceof RouteSegment) return equalSegments(<any>a, <any>b);
if (a instanceof UrlSegment) return equalUrlSegments(<any>a, <any>b);
// if (a instanceof RouteSegment) return equalSegments(<any>a, <any>b);
// if (a instanceof UrlSegment) return equalUrlSegments(<any>a, <any>b);
return a === b;
}
@ -90,8 +90,8 @@ export class TreeNode<T> {
}
export class UrlSegment {
constructor(public segment: any, public parameters: {[key: string]: any}, public outlet: string) {
}
constructor(public segment: any, public parameters: {[key: string]: string},
public outlet: string) {}
toString(): string {
let outletPrefix = isBlank(this.outlet) ? "" : `${this.outlet}:`;
@ -112,7 +112,7 @@ export class RouteSegment {
/** @internal */
_componentFactory: ComponentFactory<any>;
constructor(public urlSegments: UrlSegment[], public parameters: {[key: string]: any},
constructor(public urlSegments: UrlSegment[], public parameters: {[key: string]: string},
public outlet: string, type: Type, componentFactory: ComponentFactory<any>) {
this._type = type;
this._componentFactory = componentFactory;
@ -122,6 +122,10 @@ export class RouteSegment {
return isPresent(this.parameters) ? this.parameters[param] : null;
}
getParamAsNumber(param: string): number {
return isPresent(this.parameters) ? NumberWrapper.parseFloat(this.parameters[param]) : null;
}
get type(): Type { return this._type; }
get stringifiedUrlSegments(): string { return this.urlSegments.map(s => s.toString()).join("/"); }

View File

@ -69,6 +69,34 @@ export function main() {
expect(parser.serialize(t)).toEqual("/a/b;aa=22;bb=33");
});
describe("node reuse", () => {
it('should reuse nodes when path is the same', () => {
let p = parser.parse("/a/b");
let tree = s(p.root);
let t = link(tree.root, tree, p, ['/a/c']);
expect(t.root).toBe(p.root);
expect(t.firstChild(t.root)).toBe(p.firstChild(p.root));
expect(t.firstChild(t.firstChild(t.root))).not.toBe(p.firstChild(p.firstChild(p.root)));
});
it("should create new node when params are the same", () => {
let p = parser.parse("/a;x=1");
let tree = s(p.root);
let t = link(tree.root, tree, p, ['/a', {'x': 1}]);
expect(t.firstChild(t.root)).toBe(p.firstChild(p.root));
});
it("should create new node when params are different", () => {
let p = parser.parse("/a;x=1");
let tree = s(p.root);
let t = link(tree.root, tree, p, ['/a', {'x': 2}]);
expect(t.firstChild(t.root)).not.toBe(p.firstChild(p.root));
});
});
describe("relative navigation", () => {
it("should work", () => {
let p = parser.parse("/a(ap)/c(cp)");
@ -155,9 +183,9 @@ export function main() {
let p = parser.parse("/a(ap)/c(cp)");
let c = p.firstChild(p.root);
let child = new RouteSegment([], {'one': 1}, null, null, null);
let child = new RouteSegment([], {'one':'1'}, null, null, null);
let root = new TreeNode<RouteSegment>(new RouteSegment([c], {}, null, null, null),
[new TreeNode<RouteSegment>(child, [])]);
[new TreeNode<RouteSegment>(child, [])]);
let tree = new RouteTree(root);
let t = link(child, tree, p, ["./c2"]);

View File

@ -20,7 +20,7 @@ import {DefaultRouterUrlSerializer} from '../src/router_url_serializer';
import {DEFAULT_OUTLET_NAME} from '../src/constants';
export function main() {
describe('recognize', () => {
ddescribe('recognize', () => {
it('should handle position args',
inject([AsyncTestCompleter, ComponentResolver], (async, resolver) => {
recognize(resolver, ComponentA, tree("b/paramB/c/paramC/d"))