feat(router): add createUrlTree
This commit is contained in:
parent
a9e773b47b
commit
2e1bd46bb1
|
@ -0,0 +1,213 @@
|
|||
import { UrlTree, UrlSegment, equalUrlSegments } from './url_tree';
|
||||
import { TreeNode, rootNode } from './utils/tree';
|
||||
import { forEach, shallowEqual } from './utils/collection';
|
||||
import { RouterState, ActivatedRoute } from './router_state';
|
||||
import { Params, PRIMARY_OUTLET } from './shared';
|
||||
|
||||
export function createUrlTree(route: ActivatedRoute, urlTree: UrlTree, commands: any[],
|
||||
queryParameters: Params | undefined, fragment: string | undefined): UrlTree {
|
||||
if (commands.length === 0) {
|
||||
return tree(rootNode(urlTree), urlTree, queryParameters, fragment);
|
||||
}
|
||||
|
||||
const normalizedCommands = normalizeCommands(commands);
|
||||
if (navigateToRoot(normalizedCommands)) {
|
||||
return tree(new TreeNode<UrlSegment>(urlTree.root, []), urlTree, queryParameters, fragment);
|
||||
}
|
||||
|
||||
const startingNode = findStartingNode(normalizedCommands, urlTree, route);
|
||||
const updated = normalizedCommands.commands.length > 0 ?
|
||||
updateMany(startingNode.children.slice(0), normalizedCommands.commands) :
|
||||
[];
|
||||
const newRoot = constructNewTree(rootNode(urlTree), startingNode, updated);
|
||||
|
||||
return tree(newRoot, urlTree, queryParameters, fragment);
|
||||
}
|
||||
|
||||
function tree(root: TreeNode<UrlSegment>, urlTree: UrlTree, queryParameters: Params | undefined, fragment: string | undefined): UrlTree {
|
||||
const q = queryParameters ? stringify(queryParameters) : urlTree.queryParameters;
|
||||
const f = fragment ? fragment : urlTree.fragment;
|
||||
return new UrlTree(root, q, f);
|
||||
}
|
||||
|
||||
function navigateToRoot(normalizedChange: NormalizedNavigationCommands): boolean {
|
||||
return normalizedChange.isAbsolute && normalizedChange.commands.length === 1 &&
|
||||
normalizedChange.commands[0] == "/";
|
||||
}
|
||||
|
||||
class NormalizedNavigationCommands {
|
||||
constructor(public isAbsolute: boolean, public numberOfDoubleDots: number,
|
||||
public commands: any[]) {}
|
||||
}
|
||||
|
||||
function normalizeCommands(commands: any[]): NormalizedNavigationCommands {
|
||||
if ((typeof commands[0] === "string") && commands.length === 1 && commands[0] == "/") {
|
||||
return new NormalizedNavigationCommands(true, 0, commands);
|
||||
}
|
||||
|
||||
let numberOfDoubleDots = 0;
|
||||
let isAbsolute = false;
|
||||
const res = [];
|
||||
|
||||
for (let i = 0; i < commands.length; ++i) {
|
||||
const c = commands[i];
|
||||
|
||||
if (!(typeof c === "string")) {
|
||||
res.push(c);
|
||||
continue;
|
||||
}
|
||||
|
||||
const 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 findStartingNode(normalizedChange: NormalizedNavigationCommands, urlTree: UrlTree,
|
||||
route: ActivatedRoute): TreeNode<UrlSegment> {
|
||||
if (normalizedChange.isAbsolute) {
|
||||
return rootNode(urlTree);
|
||||
} else {
|
||||
const urlSegment =
|
||||
findUrlSegment(route, urlTree, normalizedChange.numberOfDoubleDots);
|
||||
return findMatchingNode(urlSegment, rootNode(urlTree));
|
||||
}
|
||||
}
|
||||
|
||||
function findUrlSegment(route: ActivatedRoute, urlTree: UrlTree, numberOfDoubleDots: number): UrlSegment {
|
||||
const segments = (<any>route.urlSegments).value;
|
||||
const urlSegment = segments[segments.length - 1];
|
||||
const path = urlTree.pathFromRoot(urlSegment);
|
||||
if (path.length <= numberOfDoubleDots) {
|
||||
throw new Error("Invalid number of '../'");
|
||||
}
|
||||
return path[path.length - 1 - numberOfDoubleDots];
|
||||
}
|
||||
|
||||
function findMatchingNode(segment: UrlSegment, node: TreeNode<UrlSegment>): TreeNode<UrlSegment> {
|
||||
if (node.value === segment) return node;
|
||||
for (let c of node.children) {
|
||||
const r = findMatchingNode(segment, c);
|
||||
if (r) return r;
|
||||
}
|
||||
throw new Error(`Cannot find url segment '${segment}'`);
|
||||
}
|
||||
|
||||
function constructNewTree(node: TreeNode<UrlSegment>, original: TreeNode<UrlSegment>,
|
||||
updated: TreeNode<UrlSegment>[]): TreeNode<UrlSegment> {
|
||||
if (node === original) {
|
||||
return new TreeNode<UrlSegment>(node.value, updated);
|
||||
} else {
|
||||
return new TreeNode<UrlSegment>(
|
||||
node.value, node.children.map(c => constructNewTree(c, original, updated)));
|
||||
}
|
||||
}
|
||||
|
||||
function updateMany(nodes: TreeNode<UrlSegment>[], commands: any[]): TreeNode<UrlSegment>[] {
|
||||
const outlet = getOutlet(commands);
|
||||
const nodesInRightOutlet = nodes.filter(c => c.value.outlet === outlet);
|
||||
if (nodesInRightOutlet.length > 0) {
|
||||
const nodeRightOutlet = nodesInRightOutlet[0]; // there can be only one
|
||||
nodes[nodes.indexOf(nodeRightOutlet)] = update(nodeRightOutlet, commands);
|
||||
} else {
|
||||
nodes.push(update(null, commands));
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function getPath(commands: any[]): any {
|
||||
if (!(typeof commands[0] === "string")) return commands[0];
|
||||
const parts = commands[0].toString().split(":");
|
||||
return parts.length > 1 ? parts[1] : commands[0];
|
||||
}
|
||||
|
||||
function getOutlet(commands: any[]): string {
|
||||
if (!(typeof commands[0] === "string")) return PRIMARY_OUTLET;
|
||||
const parts = commands[0].toString().split(":");
|
||||
return parts.length > 1 ? parts[0] : PRIMARY_OUTLET;
|
||||
}
|
||||
|
||||
function update(node: TreeNode<UrlSegment>|null, commands: any[]): TreeNode<UrlSegment> {
|
||||
const rest = commands.slice(1);
|
||||
const next = rest.length === 0 ? null : rest[0];
|
||||
const outlet = getOutlet(commands);
|
||||
const path = getPath(commands);
|
||||
|
||||
// reach the end of the tree => create new tree nodes.
|
||||
if (!node && !(typeof next === 'object')) {
|
||||
const urlSegment = new UrlSegment(path, {}, outlet);
|
||||
const children = rest.length === 0 ? [] : [update(null, rest)];
|
||||
return new TreeNode<UrlSegment>(urlSegment, children);
|
||||
|
||||
} else if (!node && typeof next === 'object') {
|
||||
const urlSegment = new UrlSegment(path, stringify(next), outlet);
|
||||
return recurse(urlSegment, node, rest.slice(1));
|
||||
|
||||
// different outlet => preserve the subtree
|
||||
} else if (node && outlet !== node.value.outlet) {
|
||||
return node;
|
||||
|
||||
// params command
|
||||
} else if (node && typeof path === 'object') {
|
||||
const newSegment = new UrlSegment(node.value.path, stringify(path), node.value.outlet);
|
||||
return recurse(newSegment, node, rest);
|
||||
|
||||
// next one is a params command && can reuse the node
|
||||
} else if (node && typeof next === 'object' && compare(path, stringify(next), node.value)) {
|
||||
return recurse(node.value, node, rest.slice(1));
|
||||
|
||||
// next one is a params command && cannot reuse the node
|
||||
} else if (node && typeof next === 'object') {
|
||||
const urlSegment = new UrlSegment(path, stringify(next), outlet);
|
||||
return recurse(urlSegment, node, rest.slice(1));
|
||||
|
||||
// next one is not a params command && can reuse the node
|
||||
} else if (node && compare(path, {}, node.value)) {
|
||||
return recurse(node.value, node, rest);
|
||||
|
||||
// next one is not a params command && cannot reuse the node
|
||||
} else {
|
||||
const urlSegment = new UrlSegment(path, {}, outlet);
|
||||
return recurse(urlSegment, node, rest);
|
||||
}
|
||||
}
|
||||
|
||||
function stringify(params: {[key: string]: any}): {[key: string]: string} {
|
||||
const res = {};
|
||||
forEach(params, (v, k) => res[k] = v.toString());
|
||||
return res;
|
||||
}
|
||||
|
||||
function compare(path: string, params: {[key: string]: any}, segment: UrlSegment): boolean {
|
||||
return path == segment.path && shallowEqual(params, segment.parameters);
|
||||
}
|
||||
|
||||
function recurse(urlSegment: UrlSegment, node: TreeNode<UrlSegment> | null,
|
||||
rest: any[]): TreeNode<UrlSegment> {
|
||||
if (rest.length === 0) {
|
||||
return new TreeNode<UrlSegment>(urlSegment, []);
|
||||
}
|
||||
const children = node ? node.children.slice(0) : [];
|
||||
return new TreeNode<UrlSegment>(urlSegment, updateMany(children, rest));
|
||||
}
|
|
@ -0,0 +1,180 @@
|
|||
import {DefaultUrlSerializer} from '../src/url_serializer';
|
||||
import {UrlTree, UrlSegment} from '../src/url_tree';
|
||||
import {ActivatedRoute} from '../src/router_state';
|
||||
import {PRIMARY_OUTLET, Params} from '../src/shared';
|
||||
import {createUrlTree} from '../src/create_url_tree';
|
||||
import {BehaviorSubject} from 'rxjs/BehaviorSubject';
|
||||
|
||||
describe('createUrlTree', () => {
|
||||
const serializer = new DefaultUrlSerializer();
|
||||
|
||||
it("should navigate to the root", () => {
|
||||
const p = serializer.parse("/");
|
||||
const t = create(p.root, p, ["/"]);
|
||||
expect(serializer.serialize(t)).toEqual("");
|
||||
});
|
||||
|
||||
it("should support nested segments", () => {
|
||||
const p = serializer.parse("/a/b");
|
||||
const t = create(p.root, p, ["/one", 11, "two", 22]);
|
||||
expect(serializer.serialize(t)).toEqual("/one/11/two/22");
|
||||
});
|
||||
|
||||
it("should preserve secondary segments", () => {
|
||||
const p = serializer.parse("/a/11/b(right:c)");
|
||||
const t = create(p.root, p, ["/a", 11, 'd']);
|
||||
expect(serializer.serialize(t)).toEqual("/a/11/d(right:c)");
|
||||
});
|
||||
|
||||
it('should update matrix parameters', () => {
|
||||
const p = serializer.parse("/a;aa=11");
|
||||
const t = create(p.root, p, ["/a", {aa: 22, bb: 33}]);
|
||||
expect(serializer.serialize(t)).toEqual("/a;aa=22;bb=33");
|
||||
});
|
||||
|
||||
it('should create matrix parameters', () => {
|
||||
const p = serializer.parse("/a");
|
||||
const t = create(p.root, p, ["/a", {aa: 22, bb: 33}]);
|
||||
expect(serializer.serialize(t)).toEqual("/a;aa=22;bb=33");
|
||||
});
|
||||
|
||||
it('should create matrix parameters together with other segments', () => {
|
||||
const p = serializer.parse("/a");
|
||||
const t = create(p.root, p, ["/a", "/b", {aa: 22, bb: 33}]);
|
||||
expect(serializer.serialize(t)).toEqual("/a/b;aa=22;bb=33");
|
||||
});
|
||||
|
||||
describe("node reuse", () => {
|
||||
it('should reuse nodes when path is the same', () => {
|
||||
const p = serializer.parse("/a/b");
|
||||
const t = create(p.root, p, ['/a/c']);
|
||||
|
||||
expect(t.root).toBe(p.root);
|
||||
expect(t.firstChild(t.root)).toBe(p.firstChild(p.root));
|
||||
expect(t.firstChild(<any>t.firstChild(t.root))).not.toBe(p.firstChild(<any>p.firstChild(p.root)));
|
||||
});
|
||||
|
||||
it("should create new node when params are the same", () => {
|
||||
const p = serializer.parse("/a;x=1");
|
||||
const t = create(p.root, p, ['/a', {'x': 1}]);
|
||||
|
||||
expect(t.firstChild(t.root)).toBe(p.firstChild(p.root));
|
||||
});
|
||||
|
||||
it("should create new node when params are different", () => {
|
||||
const p = serializer.parse("/a;x=1");
|
||||
const t = create(p.root, p, ['/a', {'x': 2}]);
|
||||
|
||||
expect(t.firstChild(t.root)).not.toBe(p.firstChild(p.root));
|
||||
});
|
||||
});
|
||||
|
||||
describe("relative navigation", () => {
|
||||
it("should work", () => {
|
||||
const p = serializer.parse("/a(left:ap)/c(left:cp)");
|
||||
const c = p.firstChild(p.root);
|
||||
const t = create(c, p, ["c2"]);
|
||||
expect(serializer.serialize(t)).toEqual("/a(left:ap)/c2(left:cp)");
|
||||
});
|
||||
|
||||
it("should work when the first command starts with a ./", () => {
|
||||
const p = serializer.parse("/a(left:ap)/c(left:cp)");
|
||||
const c = p.firstChild(p.root);
|
||||
const t = create(c, p, ["./c2"]);
|
||||
expect(serializer.serialize(t)).toEqual("/a(left:ap)/c2(left:cp)");
|
||||
});
|
||||
|
||||
it("should work when the first command is ./)", () => {
|
||||
const p = serializer.parse("/a(left:ap)/c(left:cp)");
|
||||
const c = p.firstChild(p.root);
|
||||
const t = create(c, p, ["./", "c2"]);
|
||||
expect(serializer.serialize(t)).toEqual("/a(left:ap)/c2(left:cp)");
|
||||
});
|
||||
|
||||
it("should work when given params", () => {
|
||||
const p = serializer.parse("/a(left:ap)/c(left:cp)");
|
||||
const c = p.firstChild(p.root);
|
||||
const t = create(c, p, [{'x': 99}]);
|
||||
expect(serializer.serialize(t)).toEqual("/a(left:ap)/c;x=99(left:cp)");
|
||||
});
|
||||
|
||||
it("should support going to a parent", () => {
|
||||
const p = serializer.parse("/a(left:ap)/c(left:cp)");
|
||||
const c = p.firstChild(p.root);
|
||||
const t = create(c, p, ["../a2"]);
|
||||
expect(serializer.serialize(t)).toEqual("/a2(left:ap)");
|
||||
});
|
||||
|
||||
it("should support going to a parent (nested case)", () => {
|
||||
const p = serializer.parse("/a/c");
|
||||
const c = p.firstChild(<any>p.firstChild(p.root));
|
||||
const t = create(c, p, ["../c2"]);
|
||||
expect(serializer.serialize(t)).toEqual("/a/c2");
|
||||
});
|
||||
|
||||
it("should work when given ../", () => {
|
||||
const p = serializer.parse("/a/c");
|
||||
const c = p.firstChild(<any>p.firstChild(p.root));
|
||||
const t = create(c, p, ["../"]);
|
||||
expect(serializer.serialize(t)).toEqual("/a");
|
||||
});
|
||||
|
||||
it("should navigate to the root", () => {
|
||||
const p = serializer.parse("/a/c");
|
||||
const c = p.firstChild(p.root);
|
||||
const t = create(c, p, ["../"]);
|
||||
expect(serializer.serialize(t)).toEqual("");
|
||||
});
|
||||
|
||||
it("should support setting matrix params", () => {
|
||||
const p = serializer.parse("/a(left:ap)/c(left:cp)");
|
||||
const c = p.firstChild(p.root);
|
||||
const t = create(c, p, ["../", {'x': 5}]);
|
||||
expect(serializer.serialize(t)).toEqual("/a;x=5(left:ap)");
|
||||
});
|
||||
|
||||
it("should throw when too many ..", () => {
|
||||
const p = serializer.parse("/a(left:ap)/c(left:cp)");
|
||||
const c = p.firstChild(p.root);
|
||||
expect(() => create(c, p, ["../../"])).toThrowError("Invalid number of '../'");
|
||||
});
|
||||
});
|
||||
|
||||
it("should set query params", () => {
|
||||
const p = serializer.parse("/");
|
||||
const t = create(p.root, p, [], {a: 'hey'});
|
||||
expect(t.queryParameters).toEqual({a: 'hey'});
|
||||
});
|
||||
|
||||
it("should stringify query params", () => {
|
||||
const p = serializer.parse("/");
|
||||
const t = create(p.root, p, [], <any>{a: 1});
|
||||
expect(t.queryParameters).toEqual({a: '1'});
|
||||
});
|
||||
|
||||
it("should reuse old query params when given undefined", () => {
|
||||
const p = serializer.parse("/?a=1");
|
||||
const t = create(p.root, p, [], undefined);
|
||||
expect(t.queryParameters).toEqual({a: '1'});
|
||||
});
|
||||
|
||||
it("should set fragment", () => {
|
||||
const p = serializer.parse("/");
|
||||
const t = create(p.root, p, [], {}, "fragment");
|
||||
expect(t.fragment).toEqual("fragment");
|
||||
});
|
||||
|
||||
it("should reused old fragment when given undefined", () => {
|
||||
const p = serializer.parse("/#fragment");
|
||||
const t = create(p.root, p, [], undefined, undefined);
|
||||
expect(t.fragment).toEqual("fragment");
|
||||
});
|
||||
});
|
||||
|
||||
function create(start: UrlSegment | null, tree: UrlTree, commands: any[], queryParameters?: Params, fragment?: string) {
|
||||
if (!start) {
|
||||
expect(start).toBeDefined();
|
||||
}
|
||||
const a = new ActivatedRoute(new BehaviorSubject([start]), <any>null, PRIMARY_OUTLET, "someComponent");
|
||||
return createUrlTree(a, tree, commands, queryParameters, fragment);
|
||||
}
|
Loading…
Reference in New Issue