feat(router): implement terminal
This commit is contained in:
@ -3,158 +3,202 @@ import {of } from 'rxjs/observable/of';
|
|||||||
|
|
||||||
import {Route, RouterConfig} from './config';
|
import {Route, RouterConfig} from './config';
|
||||||
import {PRIMARY_OUTLET} from './shared';
|
import {PRIMARY_OUTLET} from './shared';
|
||||||
import {UrlSegment, UrlTree} from './url_tree';
|
import {UrlPathWithParams, UrlSegment, UrlTree, mapChildren} from './url_tree';
|
||||||
import {first} from './utils/collection';
|
|
||||||
import {TreeNode} from './utils/tree';
|
|
||||||
|
|
||||||
class NoMatch {}
|
class NoMatch {
|
||||||
|
constructor(public segment: UrlSegment = null) {}
|
||||||
|
}
|
||||||
class GlobalRedirect {
|
class GlobalRedirect {
|
||||||
constructor(public segments: UrlSegment[]) {}
|
constructor(public paths: UrlPathWithParams[]) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function applyRedirects(urlTree: UrlTree, config: RouterConfig): Observable<UrlTree> {
|
export function applyRedirects(urlTree: UrlTree, config: RouterConfig): Observable<UrlTree> {
|
||||||
try {
|
try {
|
||||||
const transformedChildren = urlTree._root.children.map(c => applyNode(config, c));
|
return createUrlTree(urlTree, expandSegment(config, urlTree.root, PRIMARY_OUTLET));
|
||||||
return createUrlTree(urlTree, transformedChildren);
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof GlobalRedirect) {
|
if (e instanceof GlobalRedirect) {
|
||||||
return createUrlTree(urlTree, [constructNodes(e.segments, [], [])]);
|
return createUrlTree(
|
||||||
|
urlTree, new UrlSegment([], {[PRIMARY_OUTLET]: new UrlSegment(e.paths, {})}));
|
||||||
} else if (e instanceof NoMatch) {
|
} else if (e instanceof NoMatch) {
|
||||||
return new Observable<UrlTree>(obs => obs.error(new Error('Cannot match any routes')));
|
return new Observable<UrlTree>(
|
||||||
|
obs => obs.error(new Error(`Cannot match any routes: '${e.segment}'`)));
|
||||||
} else {
|
} else {
|
||||||
return new Observable<UrlTree>(obs => obs.error(e));
|
return new Observable<UrlTree>(obs => obs.error(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createUrlTree(urlTree: UrlTree, children: TreeNode<UrlSegment>[]): Observable<UrlTree> {
|
function createUrlTree(urlTree: UrlTree, root: UrlSegment): Observable<UrlTree> {
|
||||||
const transformedRoot = new TreeNode<UrlSegment>(urlTree.root, children);
|
return of (new UrlTree(root, urlTree.queryParams, urlTree.fragment));
|
||||||
return of (new UrlTree(transformedRoot, urlTree.queryParams, urlTree.fragment));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyNode(config: Route[], url: TreeNode<UrlSegment>): TreeNode<UrlSegment> {
|
function expandSegment(routes: Route[], segment: UrlSegment, outlet: string): UrlSegment {
|
||||||
for (let r of config) {
|
if (segment.pathsWithParams.length === 0 && Object.keys(segment.children).length > 0) {
|
||||||
|
return new UrlSegment([], expandSegmentChildren(routes, segment));
|
||||||
|
} else {
|
||||||
|
return expandPathsWithParams(segment, routes, segment.pathsWithParams, outlet, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandSegmentChildren(routes: Route[], segment: UrlSegment): {[name: string]: UrlSegment} {
|
||||||
|
return mapChildren(segment, (child, childOutlet) => expandSegment(routes, child, childOutlet));
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandPathsWithParams(
|
||||||
|
segment: UrlSegment, routes: Route[], paths: UrlPathWithParams[], outlet: string,
|
||||||
|
allowRedirects: boolean): UrlSegment {
|
||||||
|
for (let r of routes) {
|
||||||
try {
|
try {
|
||||||
return matchNode(config, r, url);
|
return expandPathsWithParamsAgainstRoute(segment, routes, r, paths, outlet, allowRedirects);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!(e instanceof NoMatch)) throw e;
|
if (!(e instanceof NoMatch)) throw e;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new NoMatch();
|
throw new NoMatch(segment);
|
||||||
}
|
}
|
||||||
|
|
||||||
function matchNode(config: Route[], route: Route, url: TreeNode<UrlSegment>): TreeNode<UrlSegment> {
|
function expandPathsWithParamsAgainstRoute(
|
||||||
if (!route.path) throw new NoMatch();
|
segment: UrlSegment, routes: Route[], route: Route, paths: UrlPathWithParams[], outlet: string,
|
||||||
if ((route.outlet ? route.outlet : PRIMARY_OUTLET) !== url.value.outlet) {
|
allowRedirects: boolean): UrlSegment {
|
||||||
throw new NoMatch();
|
if ((route.outlet ? route.outlet : PRIMARY_OUTLET) !== outlet) throw new NoMatch();
|
||||||
}
|
if (route.redirectTo && !allowRedirects) throw new NoMatch();
|
||||||
|
|
||||||
|
if (route.redirectTo) {
|
||||||
|
return expandPathsWithParamsAgainstRouteUsingRedirect(segment, routes, route, paths, outlet);
|
||||||
|
} else {
|
||||||
|
return matchPathsWithParamsAgainstRoute(segment, route, paths);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function expandPathsWithParamsAgainstRouteUsingRedirect(
|
||||||
|
segment: UrlSegment, routes: Route[], route: Route, paths: UrlPathWithParams[],
|
||||||
|
outlet: string): UrlSegment {
|
||||||
if (route.path === '**') {
|
if (route.path === '**') {
|
||||||
const newSegments = applyRedirectCommands([], route.redirectTo, {});
|
return expandWildCardWithParamsAgainstRouteUsingRedirect(route);
|
||||||
return constructNodes(newSegments, [], []);
|
} else {
|
||||||
|
return expandRegularPathWithParamsAgainstRouteUsingRedirect(
|
||||||
|
segment, routes, route, paths, outlet);
|
||||||
}
|
}
|
||||||
|
|
||||||
const m = match(route, url);
|
|
||||||
if (!m) throw new NoMatch();
|
|
||||||
const {consumedUrlSegments, lastSegment, lastParent, positionalParamSegments} = m;
|
|
||||||
|
|
||||||
const newSegments =
|
|
||||||
applyRedirectCommands(consumedUrlSegments, route.redirectTo, positionalParamSegments);
|
|
||||||
|
|
||||||
const childConfig = route.children ? route.children : [];
|
|
||||||
const transformedChildren = lastSegment.children.map(c => applyNode(childConfig, c));
|
|
||||||
|
|
||||||
const secondarySubtrees = lastParent ? lastParent.children.slice(1) : [];
|
|
||||||
const transformedSecondarySubtrees = secondarySubtrees.map(c => applyNode(config, c));
|
|
||||||
|
|
||||||
return constructNodes(newSegments, transformedChildren, transformedSecondarySubtrees);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function match(route: Route, url: TreeNode<UrlSegment>) {
|
function expandWildCardWithParamsAgainstRouteUsingRedirect(route: Route): UrlSegment {
|
||||||
|
const newPaths = applyRedirectCommands([], route.redirectTo, {});
|
||||||
|
if (route.redirectTo.startsWith('/')) {
|
||||||
|
throw new GlobalRedirect(newPaths);
|
||||||
|
} else {
|
||||||
|
return new UrlSegment(newPaths, {});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function expandRegularPathWithParamsAgainstRouteUsingRedirect(
|
||||||
|
segment: UrlSegment, routes: Route[], route: Route, paths: UrlPathWithParams[],
|
||||||
|
outlet: string): UrlSegment {
|
||||||
|
const {consumedPaths, lastChild, positionalParamSegments} = match(segment, route, paths);
|
||||||
|
const newPaths = applyRedirectCommands(consumedPaths, route.redirectTo, positionalParamSegments);
|
||||||
|
if (route.redirectTo.startsWith('/')) {
|
||||||
|
throw new GlobalRedirect(newPaths);
|
||||||
|
} else {
|
||||||
|
return expandPathsWithParams(
|
||||||
|
segment, routes, newPaths.concat(paths.slice(lastChild)), outlet, false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function matchPathsWithParamsAgainstRoute(
|
||||||
|
segment: UrlSegment, route: Route, paths: UrlPathWithParams[]): UrlSegment {
|
||||||
|
if (route.path === '**') {
|
||||||
|
return new UrlSegment(paths, {});
|
||||||
|
} else {
|
||||||
|
const {consumedPaths, lastChild} = match(segment, route, paths);
|
||||||
|
const childConfig = route.children ? route.children : [];
|
||||||
|
const slicedPath = paths.slice(lastChild);
|
||||||
|
|
||||||
|
if (childConfig.length === 0 && slicedPath.length === 0) {
|
||||||
|
return new UrlSegment(consumedPaths, {});
|
||||||
|
|
||||||
|
// TODO: check that the right segment is present
|
||||||
|
} else if (slicedPath.length === 0 && Object.keys(segment.children).length > 0) {
|
||||||
|
const children = expandSegmentChildren(childConfig, segment);
|
||||||
|
return new UrlSegment(consumedPaths, children);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const cs = expandPathsWithParams(segment, childConfig, slicedPath, PRIMARY_OUTLET, true);
|
||||||
|
return new UrlSegment(consumedPaths.concat(cs.pathsWithParams), cs.children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]) {
|
||||||
|
if (route.index || route.path === '' || route.path === '/') {
|
||||||
|
if (route.terminal && (Object.keys(segment.children).length > 0 || paths.length > 0)) {
|
||||||
|
throw new NoMatch();
|
||||||
|
} else {
|
||||||
|
return {consumedPaths: [], lastChild: 0, positionalParamSegments: {}};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const path = route.path.startsWith('/') ? route.path.substring(1) : route.path;
|
const path = route.path.startsWith('/') ? route.path.substring(1) : route.path;
|
||||||
const parts = path.split('/');
|
const parts = path.split('/');
|
||||||
const positionalParamSegments = {};
|
const positionalParamSegments = {};
|
||||||
const consumedUrlSegments = [];
|
const consumedPaths = [];
|
||||||
|
|
||||||
let lastParent: TreeNode<UrlSegment>|null = null;
|
let currentIndex = 0;
|
||||||
let lastSegment: TreeNode<UrlSegment>|null = null;
|
|
||||||
|
|
||||||
let current: TreeNode<UrlSegment>|null = url;
|
|
||||||
for (let i = 0; i < parts.length; ++i) {
|
for (let i = 0; i < parts.length; ++i) {
|
||||||
if (!current) return null;
|
if (currentIndex >= paths.length) throw new NoMatch();
|
||||||
|
const current = paths[currentIndex];
|
||||||
|
|
||||||
const p = parts[i];
|
const p = parts[i];
|
||||||
const isLastSegment = i === parts.length - 1;
|
|
||||||
const isLastParent = i === parts.length - 2;
|
|
||||||
const isPosParam = p.startsWith(':');
|
const isPosParam = p.startsWith(':');
|
||||||
|
|
||||||
if (!isPosParam && p != current.value.path) return null;
|
if (!isPosParam && p !== current.path) throw new NoMatch();
|
||||||
if (isLastSegment) {
|
|
||||||
lastSegment = current;
|
|
||||||
}
|
|
||||||
if (isLastParent) {
|
|
||||||
lastParent = current;
|
|
||||||
}
|
|
||||||
if (isPosParam) {
|
if (isPosParam) {
|
||||||
positionalParamSegments[p.substring(1)] = current.value;
|
positionalParamSegments[p.substring(1)] = current;
|
||||||
}
|
}
|
||||||
consumedUrlSegments.push(current.value);
|
consumedPaths.push(current);
|
||||||
current = first(current.children);
|
currentIndex++;
|
||||||
}
|
}
|
||||||
if (!lastSegment) throw 'Cannot be reached';
|
|
||||||
return {consumedUrlSegments, lastSegment, lastParent, positionalParamSegments};
|
|
||||||
}
|
|
||||||
|
|
||||||
function constructNodes(
|
if (route.terminal && (Object.keys(segment.children).length > 0 || currentIndex < paths.length)) {
|
||||||
segments: UrlSegment[], children: TreeNode<UrlSegment>[],
|
throw new NoMatch();
|
||||||
secondary: TreeNode<UrlSegment>[]): TreeNode<UrlSegment> {
|
|
||||||
let prevChildren = children;
|
|
||||||
for (let i = segments.length - 1; i >= 0; --i) {
|
|
||||||
if (i === segments.length - 2) {
|
|
||||||
prevChildren = [new TreeNode<UrlSegment>(segments[i], prevChildren.concat(secondary))];
|
|
||||||
} else {
|
|
||||||
prevChildren = [new TreeNode<UrlSegment>(segments[i], prevChildren)];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return prevChildren[0];
|
|
||||||
|
return {consumedPaths, lastChild: currentIndex, positionalParamSegments};
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyRedirectCommands(
|
function applyRedirectCommands(
|
||||||
segments: UrlSegment[], redirectTo: string,
|
paths: UrlPathWithParams[], redirectTo: string,
|
||||||
posParams: {[k: string]: UrlSegment}): UrlSegment[] {
|
posParams: {[k: string]: UrlPathWithParams}): UrlPathWithParams[] {
|
||||||
if (!redirectTo) return segments;
|
|
||||||
|
|
||||||
if (redirectTo.startsWith('/')) {
|
if (redirectTo.startsWith('/')) {
|
||||||
const parts = redirectTo.substring(1).split('/');
|
const parts = redirectTo.substring(1).split('/');
|
||||||
throw new GlobalRedirect(createSegments(redirectTo, parts, segments, posParams));
|
throw new GlobalRedirect(createPaths(redirectTo, parts, paths, posParams));
|
||||||
} else {
|
} else {
|
||||||
return createSegments(redirectTo, redirectTo.split('/'), segments, posParams);
|
return createPaths(redirectTo, redirectTo.split('/'), paths, posParams);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function createSegments(
|
function createPaths(
|
||||||
redirectTo: string, parts: string[], segments: UrlSegment[],
|
redirectTo: string, parts: string[], segments: UrlPathWithParams[],
|
||||||
posParams: {[k: string]: UrlSegment}): UrlSegment[] {
|
posParams: {[k: string]: UrlPathWithParams}): UrlPathWithParams[] {
|
||||||
return parts.map(
|
return parts.map(
|
||||||
p => p.startsWith(':') ? findPosParamSegment(p, posParams, redirectTo) :
|
p => p.startsWith(':') ? findPosParam(p, posParams, redirectTo) :
|
||||||
findOrCreateSegment(p, segments));
|
findOrCreatePath(p, segments));
|
||||||
}
|
}
|
||||||
|
|
||||||
function findPosParamSegment(
|
function findPosParam(
|
||||||
part: string, posParams: {[k: string]: UrlSegment}, redirectTo: string): UrlSegment {
|
part: string, posParams: {[k: string]: UrlPathWithParams},
|
||||||
|
redirectTo: string): UrlPathWithParams {
|
||||||
const paramName = part.substring(1);
|
const paramName = part.substring(1);
|
||||||
const pos = posParams[paramName];
|
const pos = posParams[paramName];
|
||||||
if (!pos) throw new Error(`Cannot redirect to '${redirectTo}'. Cannot find '${part}'.`);
|
if (!pos) throw new Error(`Cannot redirect to '${redirectTo}'. Cannot find '${part}'.`);
|
||||||
return pos;
|
return pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
function findOrCreateSegment(part: string, segments: UrlSegment[]): UrlSegment {
|
function findOrCreatePath(part: string, paths: UrlPathWithParams[]): UrlPathWithParams {
|
||||||
const matchingIndex = segments.findIndex(s => s.path === part);
|
const matchingIndex = paths.findIndex(s => s.path === part);
|
||||||
if (matchingIndex > -1) {
|
if (matchingIndex > -1) {
|
||||||
const r = segments[matchingIndex];
|
const r = paths[matchingIndex];
|
||||||
segments.splice(matchingIndex);
|
paths.splice(matchingIndex);
|
||||||
return r;
|
return r;
|
||||||
} else {
|
} else {
|
||||||
return new UrlSegment(part, {}, PRIMARY_OUTLET);
|
return new UrlPathWithParams(part, {});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -72,9 +72,7 @@ export function provideRouter(config: RouterConfig, opts: ExtraOptions): any[] {
|
|||||||
setTimeout(_ => {
|
setTimeout(_ => {
|
||||||
const appRef = injector.get(ApplicationRef);
|
const appRef = injector.get(ApplicationRef);
|
||||||
if (appRef.componentTypes.length == 0) {
|
if (appRef.componentTypes.length == 0) {
|
||||||
appRef.registerBootstrapListener((_) => {
|
appRef.registerBootstrapListener((_) => { injector.get(Router).initialNavigation(); });
|
||||||
injector.get(Router).initialNavigation();
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
injector.get(Router).initialNavigation();
|
injector.get(Router).initialNavigation();
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,7 @@ export type RouterConfig = Route[];
|
|||||||
export interface Route {
|
export interface Route {
|
||||||
index?: boolean;
|
index?: boolean;
|
||||||
path?: string;
|
path?: string;
|
||||||
|
terminal?: boolean;
|
||||||
component?: Type|string;
|
component?: Type|string;
|
||||||
outlet?: string;
|
outlet?: string;
|
||||||
canActivate?: any[];
|
canActivate?: any[];
|
||||||
|
@ -41,7 +41,7 @@ function createOrReuseChildren(
|
|||||||
|
|
||||||
function createActivatedRoute(c: ActivatedRouteSnapshot) {
|
function createActivatedRoute(c: ActivatedRouteSnapshot) {
|
||||||
return new ActivatedRoute(
|
return new ActivatedRoute(
|
||||||
new BehaviorSubject(c.urlSegments), new BehaviorSubject(c.params), c.outlet, c.component, c);
|
new BehaviorSubject(c.url), new BehaviorSubject(c.params), c.outlet, c.component, c);
|
||||||
}
|
}
|
||||||
|
|
||||||
function equalRouteSnapshots(a: ActivatedRouteSnapshot, b: ActivatedRouteSnapshot): boolean {
|
function equalRouteSnapshots(a: ActivatedRouteSnapshot, b: ActivatedRouteSnapshot): boolean {
|
||||||
|
@ -1,36 +1,52 @@
|
|||||||
import {ActivatedRoute} from './router_state';
|
import {ActivatedRoute} from './router_state';
|
||||||
import {PRIMARY_OUTLET, Params} from './shared';
|
import {PRIMARY_OUTLET, Params} from './shared';
|
||||||
import {UrlSegment, UrlTree} from './url_tree';
|
import {UrlPathWithParams, UrlSegment, UrlTree} from './url_tree';
|
||||||
import {forEach, shallowEqual} from './utils/collection';
|
import {forEach, shallowEqual} from './utils/collection';
|
||||||
import {TreeNode} from './utils/tree';
|
|
||||||
|
|
||||||
export function createUrlTree(
|
export function createUrlTree(
|
||||||
route: ActivatedRoute, urlTree: UrlTree, commands: any[], queryParams: Params | undefined,
|
route: ActivatedRoute, urlTree: UrlTree, commands: any[], queryParams: Params | undefined,
|
||||||
fragment: string | undefined): UrlTree {
|
fragment: string | undefined): UrlTree {
|
||||||
if (commands.length === 0) {
|
if (commands.length === 0) {
|
||||||
return tree(urlTree._root, urlTree, queryParams, fragment);
|
return tree(urlTree.root, urlTree.root, urlTree, queryParams, fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
const normalizedCommands = normalizeCommands(commands);
|
const normalizedCommands = normalizeCommands(commands);
|
||||||
if (navigateToRoot(normalizedCommands)) {
|
if (navigateToRoot(normalizedCommands)) {
|
||||||
return tree(new TreeNode<UrlSegment>(urlTree.root, []), urlTree, queryParams, fragment);
|
return tree(urlTree.root, new UrlSegment([], {}), urlTree, queryParams, fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
const startingNode = findStartingNode(normalizedCommands, urlTree, route);
|
const startingPosition = findStartingPosition(normalizedCommands, urlTree, route);
|
||||||
const updated = normalizedCommands.commands.length > 0 ?
|
const segment = startingPosition.processChildren ?
|
||||||
updateMany(startingNode.children.slice(0), normalizedCommands.commands) :
|
updateSegmentChildren(
|
||||||
[];
|
startingPosition.segment, startingPosition.index, normalizedCommands.commands) :
|
||||||
const newRoot = constructNewTree(urlTree._root, startingNode, updated);
|
updateSegment(startingPosition.segment, startingPosition.index, normalizedCommands.commands);
|
||||||
|
return tree(startingPosition.segment, segment, urlTree, queryParams, fragment);
|
||||||
return tree(newRoot, urlTree, queryParams, fragment);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function tree(
|
function tree(
|
||||||
root: TreeNode<UrlSegment>, urlTree: UrlTree, queryParams: Params | undefined,
|
oldSegment: UrlSegment, newSegment: UrlSegment, urlTree: UrlTree,
|
||||||
fragment: string | undefined): UrlTree {
|
queryParams: Params | undefined, fragment: string | undefined): UrlTree {
|
||||||
const q = queryParams ? stringify(queryParams) : urlTree.queryParams;
|
const q = queryParams ? stringify(queryParams) : urlTree.queryParams;
|
||||||
const f = fragment ? fragment : urlTree.fragment;
|
const f = fragment ? fragment : urlTree.fragment;
|
||||||
return new UrlTree(root, q, f);
|
|
||||||
|
if (urlTree.root === oldSegment) {
|
||||||
|
return new UrlTree(newSegment, q, f);
|
||||||
|
} else {
|
||||||
|
return new UrlTree(replaceSegment(urlTree.root, oldSegment, newSegment), q, f);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function replaceSegment(
|
||||||
|
current: UrlSegment, oldSegment: UrlSegment, newSegment: UrlSegment): UrlSegment {
|
||||||
|
const children = {};
|
||||||
|
forEach(current.children, (c, k) => {
|
||||||
|
if (c === oldSegment) {
|
||||||
|
children[k] = newSegment;
|
||||||
|
} else {
|
||||||
|
children[k] = replaceSegment(c, oldSegment, newSegment);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return new UrlSegment(current.pathsWithParams, children);
|
||||||
}
|
}
|
||||||
|
|
||||||
function navigateToRoot(normalizedChange: NormalizedNavigationCommands): boolean {
|
function navigateToRoot(normalizedChange: NormalizedNavigationCommands): boolean {
|
||||||
@ -87,63 +103,30 @@ function normalizeCommands(commands: any[]): NormalizedNavigationCommands {
|
|||||||
return new NormalizedNavigationCommands(isAbsolute, numberOfDoubleDots, res);
|
return new NormalizedNavigationCommands(isAbsolute, numberOfDoubleDots, res);
|
||||||
}
|
}
|
||||||
|
|
||||||
function findStartingNode(
|
class Position {
|
||||||
normalizedChange: NormalizedNavigationCommands, urlTree: UrlTree,
|
constructor(public segment: UrlSegment, public processChildren: boolean, public index: number) {}
|
||||||
route: ActivatedRoute): TreeNode<UrlSegment> {
|
|
||||||
if (normalizedChange.isAbsolute) {
|
|
||||||
return urlTree._root;
|
|
||||||
} else {
|
|
||||||
const urlSegment = findUrlSegment(route, urlTree, normalizedChange.numberOfDoubleDots);
|
|
||||||
return findMatchingNode(urlSegment, urlTree._root);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function findUrlSegment(
|
function findStartingPosition(
|
||||||
route: ActivatedRoute, urlTree: UrlTree, numberOfDoubleDots: number): UrlSegment {
|
normalizedChange: NormalizedNavigationCommands, urlTree: UrlTree,
|
||||||
const urlSegment = route.snapshot._lastUrlSegment;
|
route: ActivatedRoute): Position {
|
||||||
const path = urlTree.pathFromRoot(urlSegment);
|
if (normalizedChange.isAbsolute) {
|
||||||
if (path.length <= numberOfDoubleDots) {
|
return new Position(urlTree.root, true, 0);
|
||||||
|
} else if (route.snapshot._lastPathIndex === -1) {
|
||||||
|
return new Position(route.snapshot._urlSegment, true, 0);
|
||||||
|
} else if (route.snapshot._lastPathIndex + 1 - normalizedChange.numberOfDoubleDots >= 0) {
|
||||||
|
return new Position(
|
||||||
|
route.snapshot._urlSegment, false,
|
||||||
|
route.snapshot._lastPathIndex + 1 - normalizedChange.numberOfDoubleDots);
|
||||||
|
} else {
|
||||||
throw new Error('Invalid number of \'../\'');
|
throw new Error('Invalid number of \'../\'');
|
||||||
}
|
}
|
||||||
return path[path.length - 1 - numberOfDoubleDots];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function findMatchingNode(segment: UrlSegment, node: TreeNode<UrlSegment>): TreeNode<UrlSegment> {
|
function getPath(command: any): any {
|
||||||
if (node.value === segment) return node;
|
if (!(typeof command === 'string')) return command;
|
||||||
for (let c of node.children) {
|
const parts = command.toString().split(':');
|
||||||
const r = findMatchingNode(segment, c);
|
return parts.length > 1 ? parts[1] : command;
|
||||||
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 {
|
function getOutlet(commands: any[]): string {
|
||||||
@ -152,49 +135,91 @@ function getOutlet(commands: any[]): string {
|
|||||||
return parts.length > 1 ? parts[0] : PRIMARY_OUTLET;
|
return parts.length > 1 ? parts[0] : PRIMARY_OUTLET;
|
||||||
}
|
}
|
||||||
|
|
||||||
function update(node: TreeNode<UrlSegment>| null, commands: any[]): TreeNode<UrlSegment> {
|
function updateSegment(segment: UrlSegment, startIndex: number, commands: any[]): UrlSegment {
|
||||||
const rest = commands.slice(1);
|
if (!segment) {
|
||||||
const next = rest.length === 0 ? null : rest[0];
|
segment = new UrlSegment([], {});
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
if (segment.pathsWithParams.length === 0 && Object.keys(segment.children).length > 0) {
|
||||||
|
return updateSegmentChildren(segment, startIndex, commands);
|
||||||
|
}
|
||||||
|
const m = prefixedWith(segment, startIndex, commands);
|
||||||
|
const slicedCommands = commands.slice(m.lastIndex);
|
||||||
|
|
||||||
|
if (m.match && slicedCommands.length === 0) {
|
||||||
|
return new UrlSegment(segment.pathsWithParams, {});
|
||||||
|
} else if (m.match && Object.keys(segment.children).length === 0) {
|
||||||
|
return createNewSegment(segment, startIndex, commands);
|
||||||
|
} else if (m.match) {
|
||||||
|
return updateSegmentChildren(segment, 0, slicedCommands);
|
||||||
|
} else {
|
||||||
|
return createNewSegment(segment, startIndex, commands);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSegmentChildren(
|
||||||
|
segment: UrlSegment, startIndex: number, commands: any[]): UrlSegment {
|
||||||
|
if (commands.length === 0) {
|
||||||
|
return new UrlSegment(segment.pathsWithParams, {});
|
||||||
|
} else {
|
||||||
|
const outlet = getOutlet(commands);
|
||||||
|
const children = {};
|
||||||
|
children[outlet] = updateSegment(segment.children[outlet], startIndex, commands);
|
||||||
|
forEach(segment.children, (child, childOutlet) => {
|
||||||
|
if (childOutlet !== outlet) {
|
||||||
|
children[childOutlet] = child;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return new UrlSegment(segment.pathsWithParams, children);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefixedWith(segment: UrlSegment, startIndex: number, commands: any[]) {
|
||||||
|
let currentCommandIndex = 0;
|
||||||
|
let currentPathIndex = startIndex;
|
||||||
|
|
||||||
|
const noMatch = {match: false, lastIndex: 0};
|
||||||
|
while (currentPathIndex < segment.pathsWithParams.length) {
|
||||||
|
if (currentCommandIndex >= commands.length) return noMatch;
|
||||||
|
const path = segment.pathsWithParams[currentPathIndex];
|
||||||
|
const curr = getPath(commands[currentCommandIndex]);
|
||||||
|
const next =
|
||||||
|
currentCommandIndex < commands.length - 1 ? commands[currentCommandIndex + 1] : null;
|
||||||
|
|
||||||
|
if (curr && next && (typeof next === 'object')) {
|
||||||
|
if (!compare(curr, next, path)) return noMatch;
|
||||||
|
currentCommandIndex += 2;
|
||||||
|
} else {
|
||||||
|
if (!compare(curr, {}, path)) return noMatch;
|
||||||
|
currentCommandIndex++;
|
||||||
|
}
|
||||||
|
currentPathIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return { match: true, lastIndex: currentCommandIndex };
|
||||||
|
}
|
||||||
|
|
||||||
|
function createNewSegment(segment: UrlSegment, startIndex: number, commands: any[]): UrlSegment {
|
||||||
|
const paths = segment.pathsWithParams.slice(0, startIndex);
|
||||||
|
let i = 0;
|
||||||
|
while (i < commands.length) {
|
||||||
|
if (i === 0 && (typeof commands[0] === 'object')) {
|
||||||
|
const p = segment.pathsWithParams[startIndex];
|
||||||
|
paths.push(new UrlPathWithParams(p.path, commands[0]));
|
||||||
|
i++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const curr = getPath(commands[i]);
|
||||||
|
const next = (i < commands.length - 1) ? commands[i + 1] : null;
|
||||||
|
if (curr && next && (typeof next === 'object')) {
|
||||||
|
paths.push(new UrlPathWithParams(curr, stringify(next)));
|
||||||
|
i += 2;
|
||||||
|
} else {
|
||||||
|
paths.push(new UrlPathWithParams(curr, {}));
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return new UrlSegment(paths, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function stringify(params: {[key: string]: any}): {[key: string]: string} {
|
function stringify(params: {[key: string]: any}): {[key: string]: string} {
|
||||||
@ -203,15 +228,7 @@ function stringify(params: {[key: string]: any}): {[key: string]: string} {
|
|||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
function compare(path: string, params: {[key: string]: any}, segment: UrlSegment): boolean {
|
function compare(
|
||||||
return path == segment.path && shallowEqual(params, segment.parameters);
|
path: string, params: {[key: string]: any}, pathWithParams: UrlPathWithParams): boolean {
|
||||||
}
|
return path == pathWithParams.path && shallowEqual(params, pathWithParams.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));
|
|
||||||
}
|
}
|
@ -10,6 +10,6 @@ export {provideRouter} from './router_providers';
|
|||||||
export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
|
export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './router_state';
|
||||||
export {PRIMARY_OUTLET, Params} from './shared';
|
export {PRIMARY_OUTLET, Params} from './shared';
|
||||||
export {DefaultUrlSerializer, UrlSerializer} from './url_serializer';
|
export {DefaultUrlSerializer, UrlSerializer} from './url_serializer';
|
||||||
export {UrlSegment, UrlTree} from './url_tree';
|
export {UrlPathWithParams, UrlTree} from './url_tree';
|
||||||
|
|
||||||
export const ROUTER_DIRECTIVES = [RouterOutlet, RouterLink];
|
export const ROUTER_DIRECTIVES = [RouterOutlet, RouterLink];
|
@ -2,171 +2,159 @@ import {Type} from '@angular/core';
|
|||||||
import {Observable} from 'rxjs/Observable';
|
import {Observable} from 'rxjs/Observable';
|
||||||
import {of } from 'rxjs/observable/of';
|
import {of } from 'rxjs/observable/of';
|
||||||
|
|
||||||
import {match} from './apply_redirects';
|
|
||||||
import {Route, RouterConfig} from './config';
|
import {Route, RouterConfig} from './config';
|
||||||
import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
|
import {ActivatedRouteSnapshot, RouterStateSnapshot} from './router_state';
|
||||||
import {PRIMARY_OUTLET} from './shared';
|
import {PRIMARY_OUTLET} from './shared';
|
||||||
import {UrlSegment, UrlTree} from './url_tree';
|
import {UrlPathWithParams, UrlSegment, UrlTree, mapChildrenIntoArray} from './url_tree';
|
||||||
import {first, flatten, forEach, merge} from './utils/collection';
|
import {last, merge} from './utils/collection';
|
||||||
import {TreeNode} from './utils/tree';
|
import {TreeNode} from './utils/tree';
|
||||||
|
|
||||||
class CannotRecognize {}
|
class NoMatch {
|
||||||
|
constructor(public segment: UrlSegment = null) {}
|
||||||
|
}
|
||||||
|
|
||||||
export function recognize(
|
export function recognize(
|
||||||
rootComponentType: Type, config: RouterConfig, url: UrlTree): Observable<RouterStateSnapshot> {
|
rootComponentType: Type, config: RouterConfig, urlTree: UrlTree,
|
||||||
|
url: string): Observable<RouterStateSnapshot> {
|
||||||
try {
|
try {
|
||||||
const match = new MatchResult(
|
const children = processSegment(config, urlTree.root, PRIMARY_OUTLET);
|
||||||
rootComponentType, config, [url.root], {}, url._root.children, [], PRIMARY_OUTLET, null,
|
const root = new ActivatedRouteSnapshot(
|
||||||
url.root);
|
[], {}, PRIMARY_OUTLET, rootComponentType, null, urlTree.root, -1);
|
||||||
const roots = constructActivatedRoute(match);
|
const rootNode = new TreeNode<ActivatedRouteSnapshot>(root, children);
|
||||||
return of (new RouterStateSnapshot(roots[0], url.queryParams, url.fragment));
|
return of (new RouterStateSnapshot(url, rootNode, urlTree.queryParams, urlTree.fragment));
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e instanceof CannotRecognize) {
|
if (e instanceof NoMatch) {
|
||||||
return new Observable<RouterStateSnapshot>(
|
return new Observable<RouterStateSnapshot>(
|
||||||
obs => obs.error(new Error('Cannot match any routes')));
|
obs => obs.error(new Error(`Cannot match any routes: '${e.segment}'`)));
|
||||||
} else {
|
} else {
|
||||||
return new Observable<RouterStateSnapshot>(obs => obs.error(e));
|
return new Observable<RouterStateSnapshot>(obs => obs.error(e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function constructActivatedRoute(match: MatchResult): TreeNode<ActivatedRouteSnapshot>[] {
|
function processSegment(
|
||||||
const activatedRoute = createActivatedRouteSnapshot(match);
|
config: Route[], segment: UrlSegment, outlet: string): TreeNode<ActivatedRouteSnapshot>[] {
|
||||||
const children = match.leftOverUrl.length > 0 ?
|
if (segment.pathsWithParams.length === 0 && Object.keys(segment.children).length > 0) {
|
||||||
recognizeMany(match.children, match.leftOverUrl) :
|
return processSegmentChildren(config, segment);
|
||||||
recognizeLeftOvers(match.children, match.lastUrlSegment);
|
} else {
|
||||||
|
return [processPathsWithParams(config, segment, 0, segment.pathsWithParams, outlet)];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function processSegmentChildren(
|
||||||
|
config: Route[], segment: UrlSegment): TreeNode<ActivatedRouteSnapshot>[] {
|
||||||
|
const children = mapChildrenIntoArray(
|
||||||
|
segment, (child, childOutlet) => processSegment(config, child, childOutlet));
|
||||||
checkOutletNameUniqueness(children);
|
checkOutletNameUniqueness(children);
|
||||||
children.sort((a, b) => {
|
sortActivatedRouteSnapshots(children);
|
||||||
|
return children;
|
||||||
|
}
|
||||||
|
|
||||||
|
function sortActivatedRouteSnapshots(nodes: TreeNode<ActivatedRouteSnapshot>[]): void {
|
||||||
|
nodes.sort((a, b) => {
|
||||||
if (a.value.outlet === PRIMARY_OUTLET) return -1;
|
if (a.value.outlet === PRIMARY_OUTLET) return -1;
|
||||||
if (b.value.outlet === PRIMARY_OUTLET) return 1;
|
if (b.value.outlet === PRIMARY_OUTLET) return 1;
|
||||||
return a.value.outlet.localeCompare(b.value.outlet);
|
return a.value.outlet.localeCompare(b.value.outlet);
|
||||||
});
|
});
|
||||||
return [new TreeNode<ActivatedRouteSnapshot>(activatedRoute, children)];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function recognizeLeftOvers(
|
function processPathsWithParams(
|
||||||
config: Route[], lastUrlSegment: UrlSegment): TreeNode<ActivatedRouteSnapshot>[] {
|
config: Route[], segment: UrlSegment, pathIndex: number, paths: UrlPathWithParams[],
|
||||||
if (!config) return [];
|
outlet: string): TreeNode<ActivatedRouteSnapshot> {
|
||||||
const mIndex = matchIndex(config, [], lastUrlSegment);
|
for (let r of config) {
|
||||||
return mIndex ? constructActivatedRoute(mIndex) : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
function recognizeMany(
|
|
||||||
config: Route[], urls: TreeNode<UrlSegment>[]): TreeNode<ActivatedRouteSnapshot>[] {
|
|
||||||
return flatten(urls.map(url => recognizeOne(config, url)));
|
|
||||||
}
|
|
||||||
|
|
||||||
function createActivatedRouteSnapshot(match: MatchResult): ActivatedRouteSnapshot {
|
|
||||||
return new ActivatedRouteSnapshot(
|
|
||||||
match.consumedUrlSegments, match.parameters, match.outlet, match.component, match.route,
|
|
||||||
match.lastUrlSegment);
|
|
||||||
}
|
|
||||||
|
|
||||||
function recognizeOne(
|
|
||||||
config: Route[], url: TreeNode<UrlSegment>): TreeNode<ActivatedRouteSnapshot>[] {
|
|
||||||
const matches = matchNode(config, url);
|
|
||||||
for (let match of matches) {
|
|
||||||
try {
|
try {
|
||||||
const primary = constructActivatedRoute(match);
|
return processPathsWithParamsAgainstRoute(r, segment, pathIndex, paths, outlet);
|
||||||
const secondary = recognizeMany(config, match.secondary);
|
|
||||||
const res = primary.concat(secondary);
|
|
||||||
checkOutletNameUniqueness(res);
|
|
||||||
return res;
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (!(e instanceof CannotRecognize)) {
|
if (!(e instanceof NoMatch)) throw e;
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
throw new CannotRecognize();
|
throw new NoMatch(segment);
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkOutletNameUniqueness(nodes: TreeNode<ActivatedRouteSnapshot>[]):
|
function processPathsWithParamsAgainstRoute(
|
||||||
TreeNode<ActivatedRouteSnapshot>[] {
|
route: Route, segment: UrlSegment, pathIndex: number, paths: UrlPathWithParams[],
|
||||||
let names = {};
|
outlet: string): TreeNode<ActivatedRouteSnapshot> {
|
||||||
|
if (route.redirectTo) throw new NoMatch();
|
||||||
|
if ((route.outlet ? route.outlet : PRIMARY_OUTLET) !== outlet) throw new NoMatch();
|
||||||
|
|
||||||
|
if (route.path === '**') {
|
||||||
|
const params = paths.length > 0 ? last(paths).parameters : {};
|
||||||
|
const snapshot =
|
||||||
|
new ActivatedRouteSnapshot(paths, params, outlet, route.component, route, segment, -1);
|
||||||
|
return new TreeNode<ActivatedRouteSnapshot>(snapshot, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
const {consumedPaths, parameters, lastChild} = match(segment, route, paths);
|
||||||
|
|
||||||
|
const snapshot = new ActivatedRouteSnapshot(
|
||||||
|
consumedPaths, parameters, outlet, route.component, route, segment,
|
||||||
|
pathIndex + lastChild - 1);
|
||||||
|
const slicedPath = paths.slice(lastChild);
|
||||||
|
const childConfig = route.children ? route.children : [];
|
||||||
|
|
||||||
|
if (childConfig.length === 0 && slicedPath.length === 0) {
|
||||||
|
return new TreeNode<ActivatedRouteSnapshot>(snapshot, []);
|
||||||
|
|
||||||
|
// TODO: check that the right segment is present
|
||||||
|
} else if (slicedPath.length === 0 && Object.keys(segment.children).length > 0) {
|
||||||
|
const children = processSegmentChildren(childConfig, segment);
|
||||||
|
return new TreeNode<ActivatedRouteSnapshot>(snapshot, children);
|
||||||
|
|
||||||
|
} else {
|
||||||
|
const child = processPathsWithParams(
|
||||||
|
childConfig, segment, pathIndex + lastChild, slicedPath, PRIMARY_OUTLET);
|
||||||
|
return new TreeNode<ActivatedRouteSnapshot>(snapshot, [child]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]) {
|
||||||
|
if (route.index || route.path === '' || route.path === '/') {
|
||||||
|
if (route.terminal && (Object.keys(segment.children).length > 0 || paths.length > 0)) {
|
||||||
|
throw new NoMatch();
|
||||||
|
} else {
|
||||||
|
return {consumedPaths: [], lastChild: 0, parameters: {}};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const path = route.path.startsWith('/') ? route.path.substring(1) : route.path;
|
||||||
|
const parts = path.split('/');
|
||||||
|
const posParameters = {};
|
||||||
|
const consumedPaths = [];
|
||||||
|
|
||||||
|
let currentIndex = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; ++i) {
|
||||||
|
if (currentIndex >= paths.length) throw new NoMatch();
|
||||||
|
const current = paths[currentIndex];
|
||||||
|
|
||||||
|
const p = parts[i];
|
||||||
|
const isPosParam = p.startsWith(':');
|
||||||
|
|
||||||
|
if (!isPosParam && p !== current.path) throw new NoMatch();
|
||||||
|
if (isPosParam) {
|
||||||
|
posParameters[p.substring(1)] = current.path;
|
||||||
|
}
|
||||||
|
consumedPaths.push(current);
|
||||||
|
currentIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (route.terminal && (Object.keys(segment.children).length > 0 || currentIndex < paths.length)) {
|
||||||
|
throw new NoMatch();
|
||||||
|
}
|
||||||
|
|
||||||
|
const parameters = <any>merge(posParameters, consumedPaths[consumedPaths.length - 1].parameters);
|
||||||
|
return {consumedPaths, lastChild: currentIndex, parameters};
|
||||||
|
}
|
||||||
|
|
||||||
|
function checkOutletNameUniqueness(nodes: TreeNode<ActivatedRouteSnapshot>[]): void {
|
||||||
|
const names = {};
|
||||||
nodes.forEach(n => {
|
nodes.forEach(n => {
|
||||||
let routeWithSameOutletName = names[n.value.outlet];
|
let routeWithSameOutletName = names[n.value.outlet];
|
||||||
if (routeWithSameOutletName) {
|
if (routeWithSameOutletName) {
|
||||||
const p = routeWithSameOutletName.urlSegments.map(s => s.toString()).join('/');
|
const p = routeWithSameOutletName.urlSegments.map(s => s.toString()).join('/');
|
||||||
const c = n.value.urlSegments.map(s => s.toString()).join('/');
|
const c = n.value.url.map(s => s.toString()).join('/');
|
||||||
throw new Error(`Two segments cannot have the same outlet name: '${p}' and '${c}'.`);
|
throw new Error(`Two segments cannot have the same outlet name: '${p}' and '${c}'.`);
|
||||||
}
|
}
|
||||||
names[n.value.outlet] = n.value;
|
names[n.value.outlet] = n.value;
|
||||||
});
|
});
|
||||||
return nodes;
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchNode(config: Route[], url: TreeNode<UrlSegment>): MatchResult[] {
|
|
||||||
const res = [];
|
|
||||||
for (let r of config) {
|
|
||||||
const m = matchWithParts(r, url);
|
|
||||||
if (m) {
|
|
||||||
res.push(m);
|
|
||||||
} else if (r.index) {
|
|
||||||
res.push(createIndexMatch(r, [url], url.value));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return res;
|
|
||||||
}
|
|
||||||
|
|
||||||
function createIndexMatch(
|
|
||||||
r: Route, leftOverUrls: TreeNode<UrlSegment>[], lastUrlSegment: UrlSegment): MatchResult {
|
|
||||||
const outlet = r.outlet ? r.outlet : PRIMARY_OUTLET;
|
|
||||||
const children = r.children ? r.children : [];
|
|
||||||
return new MatchResult(
|
|
||||||
r.component, children, [], lastUrlSegment.parameters, leftOverUrls, [], outlet, r,
|
|
||||||
lastUrlSegment);
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchIndex(
|
|
||||||
config: Route[], leftOverUrls: TreeNode<UrlSegment>[], lastUrlSegment: UrlSegment): MatchResult|
|
|
||||||
null {
|
|
||||||
for (let r of config) {
|
|
||||||
if (r.index) {
|
|
||||||
return createIndexMatch(r, leftOverUrls, lastUrlSegment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
function matchWithParts(route: Route, url: TreeNode<UrlSegment>): MatchResult|null {
|
|
||||||
if (!route.path) return null;
|
|
||||||
if ((route.outlet ? route.outlet : PRIMARY_OUTLET) !== url.value.outlet) return null;
|
|
||||||
|
|
||||||
const path = route.path.startsWith('/') ? route.path.substring(1) : route.path;
|
|
||||||
if (path === '**') {
|
|
||||||
const consumedUrl = [];
|
|
||||||
let u: TreeNode<UrlSegment>|null = url;
|
|
||||||
while (u) {
|
|
||||||
consumedUrl.push(u.value);
|
|
||||||
u = first(u.children);
|
|
||||||
}
|
|
||||||
const last = consumedUrl[consumedUrl.length - 1];
|
|
||||||
return new MatchResult(
|
|
||||||
route.component, [], consumedUrl, last.parameters, [], [], PRIMARY_OUTLET, route, last);
|
|
||||||
}
|
|
||||||
|
|
||||||
const m = match(route, url);
|
|
||||||
if (!m) return null;
|
|
||||||
const {consumedUrlSegments, lastSegment, lastParent, positionalParamSegments} = m;
|
|
||||||
|
|
||||||
const p = lastSegment.value.parameters;
|
|
||||||
const posParams = {};
|
|
||||||
forEach(positionalParamSegments, (v, k) => { posParams[k] = v.path; });
|
|
||||||
const parameters = <{[key: string]: string}>merge(p, posParams);
|
|
||||||
const secondarySubtrees = lastParent ? lastParent.children.slice(1) : [];
|
|
||||||
const children = route.children ? route.children : [];
|
|
||||||
const outlet = route.outlet ? route.outlet : PRIMARY_OUTLET;
|
|
||||||
|
|
||||||
return new MatchResult(
|
|
||||||
route.component, children, consumedUrlSegments, parameters, lastSegment.children,
|
|
||||||
secondarySubtrees, outlet, route, lastSegment.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
class MatchResult {
|
|
||||||
constructor(
|
|
||||||
public component: Type|string, public children: Route[],
|
|
||||||
public consumedUrlSegments: UrlSegment[], public parameters: {[key: string]: string},
|
|
||||||
public leftOverUrl: TreeNode<UrlSegment>[], public secondary: TreeNode<UrlSegment>[],
|
|
||||||
public outlet: string, public route: Route|null, public lastUrlSegment: UrlSegment) {}
|
|
||||||
}
|
}
|
@ -39,7 +39,7 @@ export interface NavigationExtras {
|
|||||||
* An event triggered when a navigation starts
|
* An event triggered when a navigation starts
|
||||||
*/
|
*/
|
||||||
export class NavigationStart {
|
export class NavigationStart {
|
||||||
constructor(public id: number, public url: UrlTree) {}
|
constructor(public id: number, public url: string) {}
|
||||||
|
|
||||||
toString(): string { return `NavigationStart(id: ${this.id}, url: '${this.url}')`; }
|
toString(): string { return `NavigationStart(id: ${this.id}, url: '${this.url}')`; }
|
||||||
}
|
}
|
||||||
@ -48,16 +48,18 @@ export class NavigationStart {
|
|||||||
* An event triggered when a navigation ends successfully
|
* An event triggered when a navigation ends successfully
|
||||||
*/
|
*/
|
||||||
export class NavigationEnd {
|
export class NavigationEnd {
|
||||||
constructor(public id: number, public url: UrlTree) {}
|
constructor(public id: number, public url: string, public urlAfterRedirects: string) {}
|
||||||
|
|
||||||
toString(): string { return `NavigationEnd(id: ${this.id}, url: '${this.url}')`; }
|
toString(): string {
|
||||||
|
return `NavigationEnd(id: ${this.id}, url: '${this.url}', urlAfterRedirects: '${this.urlAfterRedirects}')`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* An event triggered when a navigation is canceled
|
* An event triggered when a navigation is canceled
|
||||||
*/
|
*/
|
||||||
export class NavigationCancel {
|
export class NavigationCancel {
|
||||||
constructor(public id: number, public url: UrlTree) {}
|
constructor(public id: number, public url: string) {}
|
||||||
|
|
||||||
toString(): string { return `NavigationCancel(id: ${this.id}, url: '${this.url}')`; }
|
toString(): string { return `NavigationCancel(id: ${this.id}, url: '${this.url}')`; }
|
||||||
}
|
}
|
||||||
@ -66,7 +68,7 @@ export class NavigationCancel {
|
|||||||
* An event triggered when a navigation fails due to unexpected error
|
* An event triggered when a navigation fails due to unexpected error
|
||||||
*/
|
*/
|
||||||
export class NavigationError {
|
export class NavigationError {
|
||||||
constructor(public id: number, public url: UrlTree, public error: any) {}
|
constructor(public id: number, public url: string, public error: any) {}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
return `NavigationError(id: ${this.id}, url: '${this.url}', error: ${this.error})`;
|
return `NavigationError(id: ${this.id}, url: '${this.url}', error: ${this.error})`;
|
||||||
@ -78,7 +80,7 @@ export class NavigationError {
|
|||||||
*/
|
*/
|
||||||
export class RoutesRecognized {
|
export class RoutesRecognized {
|
||||||
constructor(
|
constructor(
|
||||||
public id: number, public url: UrlTree, public urlAfterRedirects: UrlTree,
|
public id: number, public url: string, public urlAfterRedirects: string,
|
||||||
public state: RouterStateSnapshot) {}
|
public state: RouterStateSnapshot) {}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
@ -107,7 +109,7 @@ export class Router {
|
|||||||
private location: Location, private injector: Injector, private config: RouterConfig) {
|
private location: Location, private injector: Injector, private config: RouterConfig) {
|
||||||
this.routerEvents = new Subject<Event>();
|
this.routerEvents = new Subject<Event>();
|
||||||
this.currentUrlTree = createEmptyUrlTree();
|
this.currentUrlTree = createEmptyUrlTree();
|
||||||
this.currentRouterState = createEmptyState(this.rootComponentType);
|
this.currentRouterState = createEmptyState(this.currentUrlTree, this.rootComponentType);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -124,9 +126,9 @@ export class Router {
|
|||||||
get routerState(): RouterState { return this.currentRouterState; }
|
get routerState(): RouterState { return this.currentRouterState; }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the current url tree.
|
* Returns the current url.
|
||||||
*/
|
*/
|
||||||
get urlTree(): UrlTree { return this.currentUrlTree; }
|
get url(): string { return this.serializeUrl(this.currentUrlTree); }
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns an observable of route events
|
* Returns an observable of route events
|
||||||
@ -210,7 +212,6 @@ export class Router {
|
|||||||
return createUrlTree(a, this.currentUrlTree, commands, queryParams, fragment);
|
return createUrlTree(a, this.currentUrlTree, commands, queryParams, fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Navigate based on the provided array of commands and a starting point.
|
* Navigate based on the provided array of commands and a starting point.
|
||||||
* If no starting route is provided, the navigation is absolute.
|
* If no starting route is provided, the navigation is absolute.
|
||||||
@ -242,7 +243,7 @@ export class Router {
|
|||||||
|
|
||||||
private scheduleNavigation(url: UrlTree, pop: boolean): Promise<boolean> {
|
private scheduleNavigation(url: UrlTree, pop: boolean): Promise<boolean> {
|
||||||
const id = ++this.navigationId;
|
const id = ++this.navigationId;
|
||||||
this.routerEvents.next(new NavigationStart(id, url));
|
this.routerEvents.next(new NavigationStart(id, this.serializeUrl(url)));
|
||||||
return Promise.resolve().then((_) => this.runNavigate(url, false, id));
|
return Promise.resolve().then((_) => this.runNavigate(url, false, id));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -255,7 +256,7 @@ export class Router {
|
|||||||
private runNavigate(url: UrlTree, pop: boolean, id: number): Promise<boolean> {
|
private runNavigate(url: UrlTree, pop: boolean, id: number): Promise<boolean> {
|
||||||
if (id !== this.navigationId) {
|
if (id !== this.navigationId) {
|
||||||
this.location.go(this.urlSerializer.serialize(this.currentUrlTree));
|
this.location.go(this.urlSerializer.serialize(this.currentUrlTree));
|
||||||
this.routerEvents.next(new NavigationCancel(id, url));
|
this.routerEvents.next(new NavigationCancel(id, this.serializeUrl(url)));
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -265,12 +266,13 @@ export class Router {
|
|||||||
applyRedirects(url, this.config)
|
applyRedirects(url, this.config)
|
||||||
.mergeMap(u => {
|
.mergeMap(u => {
|
||||||
updatedUrl = u;
|
updatedUrl = u;
|
||||||
return recognize(this.rootComponentType, this.config, updatedUrl);
|
return recognize(
|
||||||
|
this.rootComponentType, this.config, updatedUrl, this.serializeUrl(updatedUrl));
|
||||||
})
|
})
|
||||||
|
|
||||||
.mergeMap((newRouterStateSnapshot) => {
|
.mergeMap((newRouterStateSnapshot) => {
|
||||||
this.routerEvents.next(
|
this.routerEvents.next(new RoutesRecognized(
|
||||||
new RoutesRecognized(id, url, updatedUrl, newRouterStateSnapshot));
|
id, this.serializeUrl(url), this.serializeUrl(updatedUrl), newRouterStateSnapshot));
|
||||||
return resolve(this.resolver, newRouterStateSnapshot);
|
return resolve(this.resolver, newRouterStateSnapshot);
|
||||||
|
|
||||||
})
|
})
|
||||||
@ -290,13 +292,13 @@ export class Router {
|
|||||||
.forEach((shouldActivate) => {
|
.forEach((shouldActivate) => {
|
||||||
if (!shouldActivate || id !== this.navigationId) {
|
if (!shouldActivate || id !== this.navigationId) {
|
||||||
this.location.go(this.urlSerializer.serialize(this.currentUrlTree));
|
this.location.go(this.urlSerializer.serialize(this.currentUrlTree));
|
||||||
this.routerEvents.next(new NavigationCancel(id, url));
|
this.routerEvents.next(new NavigationCancel(id, this.serializeUrl(url)));
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
new ActivateRoutes(state, this.currentRouterState).activate(this.outletMap);
|
new ActivateRoutes(state, this.currentRouterState).activate(this.outletMap);
|
||||||
|
|
||||||
this.currentUrlTree = url;
|
this.currentUrlTree = updatedUrl;
|
||||||
this.currentRouterState = state;
|
this.currentRouterState = state;
|
||||||
if (!pop) {
|
if (!pop) {
|
||||||
this.location.go(this.urlSerializer.serialize(updatedUrl));
|
this.location.go(this.urlSerializer.serialize(updatedUrl));
|
||||||
@ -304,12 +306,13 @@ export class Router {
|
|||||||
})
|
})
|
||||||
.then(
|
.then(
|
||||||
() => {
|
() => {
|
||||||
this.routerEvents.next(new NavigationEnd(id, url));
|
this.routerEvents.next(
|
||||||
|
new NavigationEnd(id, this.serializeUrl(url), this.serializeUrl(updatedUrl)));
|
||||||
resolvePromise(true);
|
resolvePromise(true);
|
||||||
|
|
||||||
},
|
},
|
||||||
e => {
|
e => {
|
||||||
this.routerEvents.next(new NavigationError(id, url, e));
|
this.routerEvents.next(new NavigationError(id, this.serializeUrl(url), e));
|
||||||
rejectPromise(e);
|
rejectPromise(e);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -380,9 +383,11 @@ class GuardChecks {
|
|||||||
|
|
||||||
private deactivateOutletAndItChildren(route: ActivatedRouteSnapshot, outlet: RouterOutlet): void {
|
private deactivateOutletAndItChildren(route: ActivatedRouteSnapshot, outlet: RouterOutlet): void {
|
||||||
if (outlet && outlet.isActivated) {
|
if (outlet && outlet.isActivated) {
|
||||||
forEach(
|
forEach(outlet.outletMap._outlets, (v, k) => {
|
||||||
outlet.outletMap._outlets,
|
if (v.isActivated) {
|
||||||
(v, k) => this.deactivateOutletAndItChildren(v.activatedRoute.snapshot, v));
|
this.deactivateOutletAndItChildren(v.activatedRoute.snapshot, v);
|
||||||
|
}
|
||||||
|
});
|
||||||
this.checks.push(new CanDeactivate(outlet.component, route));
|
this.checks.push(new CanDeactivate(outlet.component, route));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -455,6 +460,7 @@ class ActivateRoutes {
|
|||||||
parentOutletMap: RouterOutletMap): void {
|
parentOutletMap: RouterOutletMap): void {
|
||||||
const future = futureNode.value;
|
const future = futureNode.value;
|
||||||
const curr = currNode ? currNode.value : null;
|
const curr = currNode ? currNode.value : null;
|
||||||
|
|
||||||
const outlet = getOutlet(parentOutletMap, futureNode.value);
|
const outlet = getOutlet(parentOutletMap, futureNode.value);
|
||||||
|
|
||||||
if (future === curr) {
|
if (future === curr) {
|
||||||
@ -506,10 +512,11 @@ function nodeChildrenAsMap(node: TreeNode<any>| null) {
|
|||||||
function getOutlet(outletMap: RouterOutletMap, route: ActivatedRoute): RouterOutlet {
|
function getOutlet(outletMap: RouterOutletMap, route: ActivatedRoute): RouterOutlet {
|
||||||
let outlet = outletMap._outlets[route.outlet];
|
let outlet = outletMap._outlets[route.outlet];
|
||||||
if (!outlet) {
|
if (!outlet) {
|
||||||
|
const componentName = (<any>route.component).name;
|
||||||
if (route.outlet === PRIMARY_OUTLET) {
|
if (route.outlet === PRIMARY_OUTLET) {
|
||||||
throw new Error(`Cannot find primary outlet`);
|
throw new Error(`Cannot find primary outlet to load '${componentName}'`);
|
||||||
} else {
|
} else {
|
||||||
throw new Error(`Cannot find the outlet ${route.outlet}`);
|
throw new Error(`Cannot find the outlet ${route.outlet} to load '${componentName}'`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return outlet;
|
return outlet;
|
||||||
|
@ -4,7 +4,7 @@ import {Observable} from 'rxjs/Observable';
|
|||||||
|
|
||||||
import {Route} from './config';
|
import {Route} from './config';
|
||||||
import {PRIMARY_OUTLET, Params} from './shared';
|
import {PRIMARY_OUTLET, Params} from './shared';
|
||||||
import {UrlSegment} from './url_tree';
|
import {UrlPathWithParams, UrlSegment, UrlTree} from './url_tree';
|
||||||
import {shallowEqual} from './utils/collection';
|
import {shallowEqual} from './utils/collection';
|
||||||
import {Tree, TreeNode} from './utils/tree';
|
import {Tree, TreeNode} from './utils/tree';
|
||||||
|
|
||||||
@ -37,9 +37,9 @@ export class RouterState extends Tree<ActivatedRoute> {
|
|||||||
toString(): string { return this.snapshot.toString(); }
|
toString(): string { return this.snapshot.toString(); }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createEmptyState(rootComponent: Type): RouterState {
|
export function createEmptyState(urlTree: UrlTree, rootComponent: Type): RouterState {
|
||||||
const snapshot = createEmptyStateSnapshot(rootComponent);
|
const snapshot = createEmptyStateSnapshot(urlTree, rootComponent);
|
||||||
const emptyUrl = new BehaviorSubject([new UrlSegment('', {}, PRIMARY_OUTLET)]);
|
const emptyUrl = new BehaviorSubject([new UrlPathWithParams('', {})]);
|
||||||
const emptyParams = new BehaviorSubject({});
|
const emptyParams = new BehaviorSubject({});
|
||||||
const emptyQueryParams = new BehaviorSubject({});
|
const emptyQueryParams = new BehaviorSubject({});
|
||||||
const fragment = new BehaviorSubject('');
|
const fragment = new BehaviorSubject('');
|
||||||
@ -50,16 +50,14 @@ export function createEmptyState(rootComponent: Type): RouterState {
|
|||||||
new TreeNode<ActivatedRoute>(activated, []), emptyQueryParams, fragment, snapshot);
|
new TreeNode<ActivatedRoute>(activated, []), emptyQueryParams, fragment, snapshot);
|
||||||
}
|
}
|
||||||
|
|
||||||
function createEmptyStateSnapshot(rootComponent: Type): RouterStateSnapshot {
|
function createEmptyStateSnapshot(urlTree: UrlTree, rootComponent: Type): RouterStateSnapshot {
|
||||||
const rootUrlSegment = new UrlSegment('', {}, PRIMARY_OUTLET);
|
|
||||||
const emptyUrl = [rootUrlSegment];
|
|
||||||
const emptyParams = {};
|
const emptyParams = {};
|
||||||
const emptyQueryParams = {};
|
const emptyQueryParams = {};
|
||||||
const fragment = '';
|
const fragment = '';
|
||||||
const activated = new ActivatedRouteSnapshot(
|
const activated = new ActivatedRouteSnapshot(
|
||||||
emptyUrl, emptyParams, PRIMARY_OUTLET, rootComponent, null, rootUrlSegment);
|
[], emptyParams, PRIMARY_OUTLET, rootComponent, null, urlTree.root, -1);
|
||||||
return new RouterStateSnapshot(
|
return new RouterStateSnapshot(
|
||||||
new TreeNode<ActivatedRouteSnapshot>(activated, []), emptyQueryParams, fragment);
|
'', new TreeNode<ActivatedRouteSnapshot>(activated, []), emptyQueryParams, fragment);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -86,7 +84,7 @@ export class ActivatedRoute {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
public urlSegments: Observable<UrlSegment[]>, public params: Observable<Params>,
|
public url: Observable<UrlPathWithParams[]>, public params: Observable<Params>,
|
||||||
public outlet: string, public component: Type|string,
|
public outlet: string, public component: Type|string,
|
||||||
futureSnapshot: ActivatedRouteSnapshot) {
|
futureSnapshot: ActivatedRouteSnapshot) {
|
||||||
this._futureSnapshot = futureSnapshot;
|
this._futureSnapshot = futureSnapshot;
|
||||||
@ -120,20 +118,24 @@ export class ActivatedRouteSnapshot {
|
|||||||
_routeConfig: Route|null;
|
_routeConfig: Route|null;
|
||||||
|
|
||||||
/** @internal **/
|
/** @internal **/
|
||||||
_lastUrlSegment: UrlSegment;
|
_urlSegment: UrlSegment;
|
||||||
|
|
||||||
|
_lastPathIndex: number;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
public urlSegments: UrlSegment[], public params: Params, public outlet: string,
|
public url: UrlPathWithParams[], public params: Params, public outlet: string,
|
||||||
public component: Type|string, routeConfig: Route|null, lastUrlSegment: UrlSegment) {
|
public component: Type|string, routeConfig: Route|null, urlSegment: UrlSegment,
|
||||||
|
lastPathIndex: number) {
|
||||||
this._routeConfig = routeConfig;
|
this._routeConfig = routeConfig;
|
||||||
this._lastUrlSegment = lastUrlSegment;
|
this._urlSegment = urlSegment;
|
||||||
|
this._lastPathIndex = lastPathIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
toString(): string {
|
toString(): string {
|
||||||
const url = this.urlSegments.map(s => s.toString()).join('/');
|
const url = this.url.map(s => s.toString()).join('/');
|
||||||
const matched = this._routeConfig ? this._routeConfig.path : '';
|
const matched = this._routeConfig ? this._routeConfig.path : '';
|
||||||
return `Route(url:'${url}', path:'${matched}')`;
|
return `Route(url:'${url}', path:'${matched}')`;
|
||||||
}
|
}
|
||||||
@ -157,7 +159,7 @@ export class RouterStateSnapshot extends Tree<ActivatedRouteSnapshot> {
|
|||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
root: TreeNode<ActivatedRouteSnapshot>, public queryParams: Params,
|
public url: string, root: TreeNode<ActivatedRouteSnapshot>, public queryParams: Params,
|
||||||
public fragment: string|null) {
|
public fragment: string|null) {
|
||||||
super(root);
|
super(root);
|
||||||
}
|
}
|
||||||
@ -179,7 +181,7 @@ function serializeNode(node: TreeNode<ActivatedRouteSnapshot>): string {
|
|||||||
export function advanceActivatedRoute(route: ActivatedRoute): void {
|
export function advanceActivatedRoute(route: ActivatedRoute): void {
|
||||||
if (route.snapshot && !shallowEqual(route.snapshot.params, route._futureSnapshot.params)) {
|
if (route.snapshot && !shallowEqual(route.snapshot.params, route._futureSnapshot.params)) {
|
||||||
route.snapshot = route._futureSnapshot;
|
route.snapshot = route._futureSnapshot;
|
||||||
(<any>route.urlSegments).next(route.snapshot.urlSegments);
|
(<any>route.url).next(route.snapshot.url);
|
||||||
(<any>route.params).next(route.snapshot.params);
|
(<any>route.params).next(route.snapshot.params);
|
||||||
} else {
|
} else {
|
||||||
route.snapshot = route._futureSnapshot;
|
route.snapshot = route._futureSnapshot;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import {PRIMARY_OUTLET} from './shared';
|
import {PRIMARY_OUTLET} from './shared';
|
||||||
import {UrlSegment, UrlTree} from './url_tree';
|
import {UrlPathWithParams, UrlSegment, UrlTree} from './url_tree';
|
||||||
import {TreeNode} from './utils/tree';
|
import {forEach} from './utils/collection';
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,37 +29,65 @@ export class DefaultUrlSerializer implements UrlSerializer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
serialize(tree: UrlTree): string {
|
serialize(tree: UrlTree): string {
|
||||||
const node = serializeUrlTreeNode(tree._root);
|
const segment = `/${serializeSegment(tree.root, true)}`;
|
||||||
const query = serializeQueryParams(tree.queryParams);
|
const query = serializeQueryParams(tree.queryParams);
|
||||||
const fragment = tree.fragment !== null ? `#${tree.fragment}` : '';
|
const fragment = tree.fragment !== null ? `#${tree.fragment}` : '';
|
||||||
return `${node}${query}${fragment}`;
|
return `${segment}${query}${fragment}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeUrlTreeNode(node: TreeNode<UrlSegment>): string {
|
export function serializePaths(segment: UrlSegment): string {
|
||||||
return `${serializeSegment(node.value)}${serializeChildren(node)}`;
|
return segment.pathsWithParams.map(p => serializePath(p)).join('/');
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeUrlTreeNodes(nodes: TreeNode<UrlSegment>[]): string {
|
function serializeSegment(segment: UrlSegment, root: boolean): string {
|
||||||
const primary = serializeSegment(nodes[0].value);
|
if (segment.children[PRIMARY_OUTLET] && root) {
|
||||||
const secondaryNodes = nodes.slice(1);
|
const primary = serializeSegment(segment.children[PRIMARY_OUTLET], false);
|
||||||
const secondary =
|
const children = [];
|
||||||
secondaryNodes.length > 0 ? `(${secondaryNodes.map(serializeUrlTreeNode).join("//")})` : '';
|
forEach(segment.children, (v, k) => {
|
||||||
const children = serializeChildren(nodes[0]);
|
if (k !== PRIMARY_OUTLET) {
|
||||||
return `${primary}${secondary}${children}`;
|
children.push(`${k}:${serializeSegment(v, false)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (children.length > 0) {
|
||||||
|
return `${primary}(${children.join('//')})`;
|
||||||
|
} else {
|
||||||
|
return `${primary}`;
|
||||||
|
}
|
||||||
|
} else if (segment.children[PRIMARY_OUTLET] && !root) {
|
||||||
|
const children = [serializeSegment(segment.children[PRIMARY_OUTLET], false)];
|
||||||
|
forEach(segment.children, (v, k) => {
|
||||||
|
if (k !== PRIMARY_OUTLET) {
|
||||||
|
children.push(`${k}:${serializeSegment(v, false)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return `${serializePaths(segment)}/(${children.join('//')})`;
|
||||||
|
} else {
|
||||||
|
return serializePaths(segment);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeChildren(node: TreeNode<UrlSegment>): string {
|
function serializeChildren(segment: UrlSegment) {
|
||||||
if (node.children.length > 0) {
|
if (segment.children[PRIMARY_OUTLET]) {
|
||||||
return `/${serializeUrlTreeNodes(node.children)}`;
|
const primary = serializePaths(segment.children[PRIMARY_OUTLET]);
|
||||||
|
|
||||||
|
const secondary = [];
|
||||||
|
forEach(segment.children, (v, k) => {
|
||||||
|
if (k !== PRIMARY_OUTLET) {
|
||||||
|
secondary.push(`${k}:${serializePaths(v)}${serializeChildren(v)}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const secondaryStr = secondary.length > 0 ? `(${secondary.join('//')})` : '';
|
||||||
|
const primaryChildren = serializeChildren(segment.children[PRIMARY_OUTLET]);
|
||||||
|
const primaryChildrenStr = primaryChildren ? `/${primaryChildren}` : '';
|
||||||
|
return `${primary}${secondaryStr}${primaryChildrenStr}`;
|
||||||
} else {
|
} else {
|
||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeSegment(segment: UrlSegment): string {
|
export function serializePath(path: UrlPathWithParams): string {
|
||||||
const outlet = segment.outlet === PRIMARY_OUTLET ? '' : `${segment.outlet}:`;
|
return `${path.path}${serializeParams(path.parameters)}`;
|
||||||
return `${outlet}${segment.path}${serializeParams(segment.parameters)}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeParams(params: {[key: string]: string}): string {
|
function serializeParams(params: {[key: string]: string}): string {
|
||||||
@ -84,7 +113,7 @@ function pairs<T>(obj: {[key: string]: T}): Pair<string, T>[] {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const SEGMENT_RE = /^[^\/\(\)\?;=&#]+/;
|
const SEGMENT_RE = /^[^\/\(\)\?;=&#]+/;
|
||||||
function matchUrlSegment(str: string): string {
|
function matchPathWithParams(str: string): string {
|
||||||
SEGMENT_RE.lastIndex = 0;
|
SEGMENT_RE.lastIndex = 0;
|
||||||
var match = SEGMENT_RE.exec(str);
|
var match = SEGMENT_RE.exec(str);
|
||||||
return match ? match[0] : '';
|
return match ? match[0] : '';
|
||||||
@ -109,61 +138,53 @@ class UrlParser {
|
|||||||
this.remaining = this.remaining.substring(str.length);
|
this.remaining = this.remaining.substring(str.length);
|
||||||
}
|
}
|
||||||
|
|
||||||
parseRootSegment(): TreeNode<UrlSegment> {
|
parseRootSegment(): UrlSegment {
|
||||||
if (this.remaining == '' || this.remaining == '/') {
|
if (this.remaining === '' || this.remaining === '/') {
|
||||||
return new TreeNode<UrlSegment>(new UrlSegment('', {}, PRIMARY_OUTLET), []);
|
return new UrlSegment([], {});
|
||||||
} else {
|
} else {
|
||||||
const segments = this.parseSegments(false);
|
return new UrlSegment([], this.parseSegmentChildren());
|
||||||
return new TreeNode<UrlSegment>(new UrlSegment('', {}, PRIMARY_OUTLET), segments);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
parseSegments(hasOutletName: boolean): TreeNode<UrlSegment>[] {
|
parseSegmentChildren(): {[key: string]: UrlSegment} {
|
||||||
if (this.remaining.length == 0) {
|
if (this.remaining.length == 0) {
|
||||||
return [];
|
return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.peekStartsWith('/')) {
|
if (this.peekStartsWith('/')) {
|
||||||
this.capture('/');
|
this.capture('/');
|
||||||
}
|
}
|
||||||
let path = matchUrlSegment(this.remaining);
|
|
||||||
this.capture(path);
|
|
||||||
|
|
||||||
let outletName;
|
const paths = [this.parsePathWithParams()];
|
||||||
if (hasOutletName) {
|
|
||||||
if (path.indexOf(':') === -1) {
|
while (this.peekStartsWith('/') && !this.peekStartsWith('//') && !this.peekStartsWith('/(')) {
|
||||||
throw new Error('Not outlet name is provided');
|
this.capture('/');
|
||||||
}
|
paths.push(this.parsePathWithParams());
|
||||||
if (path.indexOf(':') > -1 && hasOutletName) {
|
|
||||||
let parts = path.split(':');
|
|
||||||
outletName = parts[0];
|
|
||||||
path = parts[1];
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (path.indexOf(':') > -1) {
|
|
||||||
throw new Error('Not outlet name is allowed');
|
|
||||||
}
|
|
||||||
outletName = PRIMARY_OUTLET;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let children = {};
|
||||||
|
if (this.peekStartsWith('/(')) {
|
||||||
|
this.capture('/');
|
||||||
|
children = this.parseParens(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let res: {[key: string]: UrlSegment} = {};
|
||||||
|
if (this.peekStartsWith('(')) {
|
||||||
|
res = this.parseParens(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
res[PRIMARY_OUTLET] = new UrlSegment(paths, children);
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
|
||||||
|
parsePathWithParams(): UrlPathWithParams {
|
||||||
|
let path = matchPathWithParams(this.remaining);
|
||||||
|
this.capture(path);
|
||||||
let matrixParams: {[key: string]: any} = {};
|
let matrixParams: {[key: string]: any} = {};
|
||||||
if (this.peekStartsWith(';')) {
|
if (this.peekStartsWith(';')) {
|
||||||
matrixParams = this.parseMatrixParams();
|
matrixParams = this.parseMatrixParams();
|
||||||
}
|
}
|
||||||
|
return new UrlPathWithParams(path, matrixParams);
|
||||||
let secondary = [];
|
|
||||||
if (this.peekStartsWith('(')) {
|
|
||||||
secondary = this.parseSecondarySegments();
|
|
||||||
}
|
|
||||||
|
|
||||||
let children: TreeNode<UrlSegment>[] = [];
|
|
||||||
if (this.peekStartsWith('/') && !this.peekStartsWith('//')) {
|
|
||||||
this.capture('/');
|
|
||||||
children = this.parseSegments(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const segment = new UrlSegment(path, matrixParams, outletName);
|
|
||||||
const node = new TreeNode<UrlSegment>(segment, children);
|
|
||||||
return [node].concat(secondary);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
parseQueryParams(): {[key: string]: any} {
|
parseQueryParams(): {[key: string]: any} {
|
||||||
@ -197,7 +218,7 @@ class UrlParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parseParam(params: {[key: string]: any}): void {
|
parseParam(params: {[key: string]: any}): void {
|
||||||
var key = matchUrlSegment(this.remaining);
|
var key = matchPathWithParams(this.remaining);
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -205,7 +226,7 @@ class UrlParser {
|
|||||||
var value: any = 'true';
|
var value: any = 'true';
|
||||||
if (this.peekStartsWith('=')) {
|
if (this.peekStartsWith('=')) {
|
||||||
this.capture('=');
|
this.capture('=');
|
||||||
var valueMatch = matchUrlSegment(this.remaining);
|
var valueMatch = matchPathWithParams(this.remaining);
|
||||||
if (valueMatch) {
|
if (valueMatch) {
|
||||||
value = valueMatch;
|
value = valueMatch;
|
||||||
this.capture(value);
|
this.capture(value);
|
||||||
@ -216,7 +237,7 @@ class UrlParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
parseQueryParam(params: {[key: string]: any}): void {
|
parseQueryParam(params: {[key: string]: any}): void {
|
||||||
var key = matchUrlSegment(this.remaining);
|
var key = matchPathWithParams(this.remaining);
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -233,12 +254,25 @@ class UrlParser {
|
|||||||
params[key] = value;
|
params[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
parseSecondarySegments(): TreeNode<UrlSegment>[] {
|
parseParens(allowPrimary: boolean): {[key: string]: UrlSegment} {
|
||||||
var segments = [];
|
const segments = {};
|
||||||
this.capture('(');
|
this.capture('(');
|
||||||
|
|
||||||
while (!this.peekStartsWith(')') && this.remaining.length > 0) {
|
while (!this.peekStartsWith(')') && this.remaining.length > 0) {
|
||||||
segments = segments.concat(this.parseSegments(true));
|
let path = matchPathWithParams(this.remaining);
|
||||||
|
let outletName;
|
||||||
|
if (path.indexOf(':') > -1) {
|
||||||
|
outletName = path.substr(0, path.indexOf(':'));
|
||||||
|
this.capture(outletName);
|
||||||
|
this.capture(':');
|
||||||
|
} else if (allowPrimary) {
|
||||||
|
outletName = PRIMARY_OUTLET;
|
||||||
|
}
|
||||||
|
|
||||||
|
const children = this.parseSegmentChildren();
|
||||||
|
segments[outletName] = Object.keys(children).length === 1 ? children[PRIMARY_OUTLET] :
|
||||||
|
new UrlSegment([], children);
|
||||||
|
|
||||||
if (this.peekStartsWith('//')) {
|
if (this.peekStartsWith('//')) {
|
||||||
this.capture('//');
|
this.capture('//');
|
||||||
}
|
}
|
||||||
|
@ -1,40 +1,41 @@
|
|||||||
import {PRIMARY_OUTLET} from './shared';
|
import {PRIMARY_OUTLET} from './shared';
|
||||||
import {DefaultUrlSerializer, serializeSegment} from './url_serializer';
|
import {DefaultUrlSerializer, serializePath, serializePaths} from './url_serializer';
|
||||||
import {shallowEqual} from './utils/collection';
|
import {forEach, shallowEqual} from './utils/collection';
|
||||||
import {Tree, TreeNode} from './utils/tree';
|
|
||||||
|
|
||||||
export function createEmptyUrlTree() {
|
export function createEmptyUrlTree() {
|
||||||
return new UrlTree(
|
return new UrlTree(new UrlSegment([], {}), {}, null);
|
||||||
new TreeNode<UrlSegment>(new UrlSegment('', {}, PRIMARY_OUTLET), []), {}, null);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A URL in the tree form.
|
* A URL in the tree form.
|
||||||
*/
|
*/
|
||||||
export class UrlTree extends Tree<UrlSegment> {
|
export class UrlTree {
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
root: TreeNode<UrlSegment>, public queryParams: {[key: string]: string},
|
public root: UrlSegment, public queryParams: {[key: string]: string},
|
||||||
public fragment: string|null) {
|
public fragment: string|null) {}
|
||||||
super(root);
|
|
||||||
}
|
|
||||||
|
|
||||||
toString(): string { return new DefaultUrlSerializer().serialize(this); }
|
toString(): string { return new DefaultUrlSerializer().serialize(this); }
|
||||||
}
|
}
|
||||||
|
|
||||||
export class UrlSegment {
|
export class UrlSegment {
|
||||||
/**
|
public parent: UrlSegment|null = null;
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
constructor(
|
constructor(
|
||||||
public path: string, public parameters: {[key: string]: string}, public outlet: string) {}
|
public pathsWithParams: UrlPathWithParams[], public children: {[key: string]: UrlSegment}) {
|
||||||
|
forEach(children, (v, k) => v.parent = this);
|
||||||
|
}
|
||||||
|
|
||||||
toString(): string { return serializeSegment(this); }
|
toString(): string { return serializePaths(this); }
|
||||||
}
|
}
|
||||||
|
|
||||||
export function equalUrlSegments(a: UrlSegment[], b: UrlSegment[]): boolean {
|
export class UrlPathWithParams {
|
||||||
|
constructor(public path: string, public parameters: {[key: string]: string}) {}
|
||||||
|
toString(): string { return serializePath(this); }
|
||||||
|
}
|
||||||
|
|
||||||
|
export function equalPathsWithParams(a: UrlPathWithParams[], b: UrlPathWithParams[]): boolean {
|
||||||
if (a.length !== b.length) return false;
|
if (a.length !== b.length) return false;
|
||||||
for (let i = 0; i < a.length; ++i) {
|
for (let i = 0; i < a.length; ++i) {
|
||||||
if (a[i].path !== b[i].path) return false;
|
if (a[i].path !== b[i].path) return false;
|
||||||
@ -42,3 +43,35 @@ export function equalUrlSegments(a: UrlSegment[], b: UrlSegment[]): boolean {
|
|||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapChildren(segment: UrlSegment, fn: (v: UrlSegment, k: string) => UrlSegment):
|
||||||
|
{[name: string]: UrlSegment} {
|
||||||
|
const newChildren = {};
|
||||||
|
forEach(segment.children, (child, childOutlet) => {
|
||||||
|
if (childOutlet === PRIMARY_OUTLET) {
|
||||||
|
newChildren[childOutlet] = fn(child, childOutlet);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
forEach(segment.children, (child, childOutlet) => {
|
||||||
|
if (childOutlet !== PRIMARY_OUTLET) {
|
||||||
|
newChildren[childOutlet] = fn(child, childOutlet);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return newChildren;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapChildrenIntoArray<T>(
|
||||||
|
segment: UrlSegment, fn: (v: UrlSegment, k: string) => T[]): T[] {
|
||||||
|
let res = [];
|
||||||
|
forEach(segment.children, (child, childOutlet) => {
|
||||||
|
if (childOutlet === PRIMARY_OUTLET) {
|
||||||
|
res = res.concat(fn(child, childOutlet));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
forEach(segment.children, (child, childOutlet) => {
|
||||||
|
if (childOutlet !== PRIMARY_OUTLET) {
|
||||||
|
res = res.concat(fn(child, childOutlet));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return res;
|
||||||
|
}
|
||||||
|
@ -1,30 +1,32 @@
|
|||||||
import {DefaultUrlSerializer} from '../src/url_serializer';
|
import {DefaultUrlSerializer} from '../src/url_serializer';
|
||||||
import {TreeNode} from '../src/utils/tree';
|
import {TreeNode} from '../src/utils/tree';
|
||||||
import {UrlTree, UrlSegment, equalUrlSegments} from '../src/url_tree';
|
import {UrlTree, UrlSegment, equalPathsWithParams} from '../src/url_tree';
|
||||||
import {Params, PRIMARY_OUTLET} from '../src/shared';
|
import {RouterConfig} from '../src/config';
|
||||||
import {applyRedirects} from '../src/apply_redirects';
|
import {applyRedirects} from '../src/apply_redirects';
|
||||||
|
|
||||||
describe('applyRedirects', () => {
|
describe('applyRedirects', () => {
|
||||||
it("should return the same url tree when no redirects", () => {
|
it("should return the same url tree when no redirects", () => {
|
||||||
applyRedirects(tree("/a/b"), [
|
checkRedirect([
|
||||||
{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}
|
{path: 'a', component: ComponentA, children: [{path: 'b', component: ComponentB}]}
|
||||||
]).forEach(t => {
|
], "/a/b", t => {
|
||||||
compareTrees(t, tree('/a/b'));
|
compareTrees(t, tree('/a/b'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should add new segments when needed", () => {
|
it("should add new segments when needed", () => {
|
||||||
applyRedirects(tree("/a/b"), [
|
checkRedirect([
|
||||||
{path: 'a/b', redirectTo: 'a/b/c'}
|
{path: 'a/b', redirectTo: 'a/b/c'},
|
||||||
]).forEach(t => {
|
{path: '**', component: ComponentC}
|
||||||
|
], "/a/b", t => {
|
||||||
compareTrees(t, tree('/a/b/c'));
|
compareTrees(t, tree('/a/b/c'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle positional parameters", () => {
|
it("should handle positional parameters", () => {
|
||||||
applyRedirects(tree("/a/1/b/2"), [
|
checkRedirect([
|
||||||
{path: 'a/:aid/b/:bid', redirectTo: 'newa/:aid/newb/:bid'}
|
{path: 'a/:aid/b/:bid', redirectTo: 'newa/:aid/newb/:bid'},
|
||||||
]).forEach(t => {
|
{path: '**', component: ComponentC}
|
||||||
|
], "/a/1/b/2", t => {
|
||||||
compareTrees(t, tree('/newa/1/newb/2'));
|
compareTrees(t, tree('/newa/1/newb/2'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -38,50 +40,122 @@ describe('applyRedirects', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it("should pass matrix parameters", () => {
|
it("should pass matrix parameters", () => {
|
||||||
applyRedirects(tree("/a;p1=1/1;p2=2"), [
|
checkRedirect([
|
||||||
{path: 'a/:id', redirectTo: 'd/a/:id/e'}
|
{path: 'a/:id', redirectTo: 'd/a/:id/e'},
|
||||||
]).forEach(t => {
|
{path: '**', component: ComponentC}
|
||||||
|
], "/a;p1=1/1;p2=2", t => {
|
||||||
compareTrees(t, tree('/d/a;p1=1/1;p2=2/e'));
|
compareTrees(t, tree('/d/a;p1=1/1;p2=2/e'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should handle preserve secondary routes", () => {
|
it("should handle preserve secondary routes", () => {
|
||||||
applyRedirects(tree("/a/1(aux:c/d)"), [
|
checkRedirect([
|
||||||
{path: 'a/:id', redirectTo: 'd/a/:id/e'},
|
{path: 'a/:id', redirectTo: 'd/a/:id/e'},
|
||||||
{path: 'c/d', component: ComponentA, outlet: 'aux'}
|
{path: 'c/d', component: ComponentA, outlet: 'aux'},
|
||||||
]).forEach(t => {
|
{path: '**', component: ComponentC}
|
||||||
|
], "/a/1(aux:c/d)", t => {
|
||||||
compareTrees(t, tree('/d/a/1/e(aux:c/d)'));
|
compareTrees(t, tree('/d/a/1/e(aux:c/d)'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should redirect secondary routes", () => {
|
it("should redirect secondary routes", () => {
|
||||||
applyRedirects(tree("/a/1(aux:c/d)"), [
|
checkRedirect([
|
||||||
{path: 'a/:id', component: ComponentA},
|
{path: 'a/:id', component: ComponentA},
|
||||||
{path: 'c/d', redirectTo: 'f/c/d/e', outlet: 'aux'}
|
{path: 'c/d', redirectTo: 'f/c/d/e', outlet: 'aux'},
|
||||||
]).forEach(t => {
|
{path: '**', component: ComponentC, outlet: 'aux'}
|
||||||
|
], "/a/1(aux:c/d)", t => {
|
||||||
compareTrees(t, tree('/a/1(aux:f/c/d/e)'));
|
compareTrees(t, tree('/a/1(aux:f/c/d/e)'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("should use the configuration of the route redirected to", () => {
|
||||||
|
checkRedirect([
|
||||||
|
{path: 'a', component: ComponentA, children: [
|
||||||
|
{path: 'b', component: ComponentB},
|
||||||
|
]},
|
||||||
|
{path: 'c', redirectTo: 'a'}
|
||||||
|
], "c/b", t => {
|
||||||
|
compareTrees(t, tree('a/b'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redirect empty path", () => {
|
||||||
|
checkRedirect([
|
||||||
|
{path: 'a', component: ComponentA, children: [
|
||||||
|
{path: 'b', component: ComponentB},
|
||||||
|
]},
|
||||||
|
{path: '', redirectTo: 'a'}
|
||||||
|
], "b", t => {
|
||||||
|
compareTrees(t, tree('a/b'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
xit("should support nested redirects", () => {
|
||||||
|
checkRedirect([
|
||||||
|
{path: 'a', component: ComponentA, children: [
|
||||||
|
{path: 'b', component: ComponentB},
|
||||||
|
{path: '', redirectTo: 'b'}
|
||||||
|
]},
|
||||||
|
{path: '', redirectTo: 'a'}
|
||||||
|
], "", t => {
|
||||||
|
compareTrees(t, tree('a/b'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
xit("should support nested redirects (when redirected to an empty path)", () => {
|
||||||
|
checkRedirect([
|
||||||
|
{path: '', component: ComponentA, children: [
|
||||||
|
{path: 'b', component: ComponentB},
|
||||||
|
{path: '', redirectTo: 'b'}
|
||||||
|
]},
|
||||||
|
{path: 'a', redirectTo: ''}
|
||||||
|
], "a", t => {
|
||||||
|
compareTrees(t, tree('b'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should redirect empty path route only when terminal", () => {
|
||||||
|
const config = [
|
||||||
|
{path: 'a', component: ComponentA, children: [
|
||||||
|
{path: 'b', component: ComponentB},
|
||||||
|
]},
|
||||||
|
{path: '', redirectTo: 'a', terminal: true}
|
||||||
|
];
|
||||||
|
|
||||||
|
applyRedirects(tree("b"), config).subscribe((_) => {
|
||||||
|
throw "Should not be reached";
|
||||||
|
}, e => {
|
||||||
|
expect(e.message).toEqual("Cannot match any routes: 'b'");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("should redirect wild cards", () => {
|
it("should redirect wild cards", () => {
|
||||||
applyRedirects(tree("/a/1(aux:c/d)"), [
|
checkRedirect([
|
||||||
|
{path: '404', component: ComponentA},
|
||||||
{path: '**', redirectTo: '/404'},
|
{path: '**', redirectTo: '/404'},
|
||||||
]).forEach(t => {
|
], "/a/1(aux:c/d)", t => {
|
||||||
compareTrees(t, tree('/404'));
|
compareTrees(t, tree('/404'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support global redirects", () => {
|
it("should support global redirects", () => {
|
||||||
applyRedirects(tree("/a/b/1"), [
|
checkRedirect([
|
||||||
{path: 'a', component: ComponentA, children: [
|
{path: 'a', component: ComponentA, children: [
|
||||||
{path: 'b/:id', redirectTo: '/global/:id'}
|
{path: 'b/:id', redirectTo: '/global/:id'}
|
||||||
]},
|
]},
|
||||||
]).forEach(t => {
|
{path: '**', component: ComponentC}
|
||||||
|
], "/a/b/1", t => {
|
||||||
compareTrees(t, tree('/global/1'));
|
compareTrees(t, tree('/global/1'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function checkRedirect(config: RouterConfig, url: string, callback: any): void {
|
||||||
|
applyRedirects(tree(url), config).subscribe(callback, e => {
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function tree(url: string): UrlTree {
|
function tree(url: string): UrlTree {
|
||||||
return new DefaultUrlSerializer().parse(url);
|
return new DefaultUrlSerializer().parse(url);
|
||||||
}
|
}
|
||||||
@ -89,19 +163,18 @@ function tree(url: string): UrlTree {
|
|||||||
function compareTrees(actual: UrlTree, expected: UrlTree): void{
|
function compareTrees(actual: UrlTree, expected: UrlTree): void{
|
||||||
const serializer = new DefaultUrlSerializer();
|
const serializer = new DefaultUrlSerializer();
|
||||||
const error = `"${serializer.serialize(actual)}" is not equal to "${serializer.serialize(expected)}"`;
|
const error = `"${serializer.serialize(actual)}" is not equal to "${serializer.serialize(expected)}"`;
|
||||||
compareNode(actual._root, expected._root, error);
|
compareSegments(actual.root, expected.root, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
function compareNode(actual: TreeNode<UrlSegment>, expected: TreeNode<UrlSegment>, error: string): void{
|
function compareSegments(actual: UrlSegment, expected: UrlSegment, error: string): void{
|
||||||
expect(equalUrlSegments([actual.value], [expected.value])).toEqual(true, error);
|
expect(actual).toBeDefined(error);
|
||||||
|
expect(equalPathsWithParams(actual.pathsWithParams, expected.pathsWithParams)).toEqual(true, error);
|
||||||
|
|
||||||
expect(actual.children.length).toEqual(expected.children.length, error);
|
expect(Object.keys(actual.children).length).toEqual(Object.keys(expected.children).length, error);
|
||||||
|
|
||||||
if (actual.children.length === expected.children.length) {
|
Object.keys(expected.children).forEach(key => {
|
||||||
for (let i = 0; i < actual.children.length; ++i) {
|
compareSegments(actual.children[key], expected.children[key], error);
|
||||||
compareNode(actual.children[i], expected.children[i], error);
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
class ComponentA {}
|
class ComponentA {}
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {DefaultUrlSerializer} from '../src/url_serializer';
|
import {DefaultUrlSerializer} from '../src/url_serializer';
|
||||||
import {UrlTree} from '../src/url_tree';
|
import {UrlTree, UrlSegment} from '../src/url_tree';
|
||||||
import {TreeNode} from '../src/utils/tree';
|
import {TreeNode} from '../src/utils/tree';
|
||||||
import {Params, PRIMARY_OUTLET} from '../src/shared';
|
import {Params, PRIMARY_OUTLET} from '../src/shared';
|
||||||
import {ActivatedRoute, RouterState, RouterStateSnapshot, createEmptyState, advanceActivatedRoute} from '../src/router_state';
|
import {ActivatedRoute, RouterState, RouterStateSnapshot, createEmptyState, advanceActivatedRoute} from '../src/router_state';
|
||||||
@ -8,7 +8,7 @@ import {recognize} from '../src/recognize';
|
|||||||
import {RouterConfig} from '../src/config';
|
import {RouterConfig} from '../src/config';
|
||||||
|
|
||||||
describe('create router state', () => {
|
describe('create router state', () => {
|
||||||
const emptyState = () => createEmptyState(RootComponent);
|
const emptyState = () => createEmptyState(new UrlTree(new UrlSegment([], {}), {}, null), RootComponent);
|
||||||
|
|
||||||
it('should work create new state', () => {
|
it('should work create new state', () => {
|
||||||
const state = createRouterState(createState([
|
const state = createRouterState(createState([
|
||||||
@ -57,7 +57,7 @@ function advanceNode(node: TreeNode<ActivatedRoute>): void {
|
|||||||
|
|
||||||
function createState(config: RouterConfig, url: string): RouterStateSnapshot {
|
function createState(config: RouterConfig, url: string): RouterStateSnapshot {
|
||||||
let res;
|
let res;
|
||||||
recognize(RootComponent, config, tree(url)).forEach(s => res = s);
|
recognize(RootComponent, config, tree(url), url).forEach(s => res = s);
|
||||||
return res;
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
import {DefaultUrlSerializer} from '../src/url_serializer';
|
import {DefaultUrlSerializer} from '../src/url_serializer';
|
||||||
import {UrlTree, UrlSegment} from '../src/url_tree';
|
import {UrlTree, UrlPathWithParams, UrlSegment} from '../src/url_tree';
|
||||||
import {ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute} from '../src/router_state';
|
import {ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute} from '../src/router_state';
|
||||||
import {PRIMARY_OUTLET, Params} from '../src/shared';
|
import {PRIMARY_OUTLET, Params} from '../src/shared';
|
||||||
import {createUrlTree} from '../src/create_url_tree';
|
import {createUrlTree} from '../src/create_url_tree';
|
||||||
@ -10,185 +10,163 @@ describe('createUrlTree', () => {
|
|||||||
|
|
||||||
it("should navigate to the root", () => {
|
it("should navigate to the root", () => {
|
||||||
const p = serializer.parse("/");
|
const p = serializer.parse("/");
|
||||||
const t = create(p.root, p, ["/"]);
|
const t = createRoot(p, ["/"]);
|
||||||
expect(serializer.serialize(t)).toEqual("");
|
expect(serializer.serialize(t)).toEqual("/");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support nested segments", () => {
|
it("should support nested segments", () => {
|
||||||
const p = serializer.parse("/a/b");
|
const p = serializer.parse("/a/b");
|
||||||
const t = create(p.root, p, ["/one", 11, "two", 22]);
|
const t = createRoot(p, ["/one", 11, "two", 22]);
|
||||||
expect(serializer.serialize(t)).toEqual("/one/11/two/22");
|
expect(serializer.serialize(t)).toEqual("/one/11/two/22");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should preserve secondary segments", () => {
|
it("should preserve secondary segments", () => {
|
||||||
const p = serializer.parse("/a/11/b(right:c)");
|
const p = serializer.parse("/a/11/b(right:c)");
|
||||||
const t = create(p.root, p, ["/a", 11, 'd']);
|
const t = createRoot(p, ["/a", 11, 'd']);
|
||||||
expect(serializer.serialize(t)).toEqual("/a/11/d(right:c)");
|
expect(serializer.serialize(t)).toEqual("/a/11/d(right:c)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support updating secondary segments", () => {
|
it("should support updating secondary segments", () => {
|
||||||
const p = serializer.parse("/a(right:b)");
|
const p = serializer.parse("/a(right:b)");
|
||||||
const t = create(p.root, p, ["right:c", 11, 'd']);
|
const t = createRoot(p, ["right:c", 11, 'd']);
|
||||||
expect(t.children(t.root)[1].outlet).toEqual("right");
|
|
||||||
expect(serializer.serialize(t)).toEqual("/a(right:c/11/d)");
|
expect(serializer.serialize(t)).toEqual("/a(right:c/11/d)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support updating secondary segments (nested case)", () => {
|
it("should support updating secondary segments (nested case)", () => {
|
||||||
const p = serializer.parse("/a/b(right:c)");
|
const p = serializer.parse("/a/(b//right:c)");
|
||||||
const t = create(p.root, p, ["a", "right:d", 11, 'e']);
|
const t = createRoot(p, ["a", "right:d", 11, 'e']);
|
||||||
expect(serializer.serialize(t)).toEqual("/a/b(right:d/11/e)");
|
expect(serializer.serialize(t)).toEqual("/a/(b//right:d/11/e)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update matrix parameters', () => {
|
it('should update matrix parameters', () => {
|
||||||
const p = serializer.parse("/a;aa=11");
|
const p = serializer.parse("/a;pp=11");
|
||||||
const t = create(p.root, p, ["/a", {aa: 22, bb: 33}]);
|
const t = createRoot(p, ["/a", {pp: 22, dd: 33}]);
|
||||||
expect(serializer.serialize(t)).toEqual("/a;aa=22;bb=33");
|
expect(serializer.serialize(t)).toEqual("/a;pp=22;dd=33");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create matrix parameters', () => {
|
it('should create matrix parameters', () => {
|
||||||
const p = serializer.parse("/a");
|
const p = serializer.parse("/a");
|
||||||
const t = create(p.root, p, ["/a", {aa: 22, bb: 33}]);
|
const t = createRoot(p, ["/a", {pp: 22, dd: 33}]);
|
||||||
expect(serializer.serialize(t)).toEqual("/a;aa=22;bb=33");
|
expect(serializer.serialize(t)).toEqual("/a;pp=22;dd=33");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create matrix parameters together with other segments', () => {
|
it('should create matrix parameters together with other segments', () => {
|
||||||
const p = serializer.parse("/a");
|
const p = serializer.parse("/a");
|
||||||
const t = create(p.root, p, ["/a", "/b", {aa: 22, bb: 33}]);
|
const t = createRoot(p, ["/a", "/b", {aa: 22, bb: 33}]);
|
||||||
expect(serializer.serialize(t)).toEqual("/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", () => {
|
describe("relative navigation", () => {
|
||||||
it("should work", () => {
|
it("should work", () => {
|
||||||
const p = serializer.parse("/a(left:ap)/c(left:cp)");
|
const p = serializer.parse("/a/(c//left:cp)(left:ap)");
|
||||||
const c = p.firstChild(p.root);
|
const t = create(p.root.children[PRIMARY_OUTLET], 0, p, ["c2"]);
|
||||||
const t = create(c, p, ["c2"]);
|
expect(serializer.serialize(t)).toEqual("/a/(c2//left:cp)(left:ap)");
|
||||||
expect(serializer.serialize(t)).toEqual("/a(left:ap)/c2(left:cp)");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should work when the first command starts with a ./", () => {
|
it("should work when the first command starts with a ./", () => {
|
||||||
const p = serializer.parse("/a(left:ap)/c(left:cp)");
|
const p = serializer.parse("/a/(c//left:cp)(left:ap)");
|
||||||
const c = p.firstChild(p.root);
|
const t = create(p.root.children[PRIMARY_OUTLET], 0, p, ["./c2"]);
|
||||||
const t = create(c, p, ["./c2"]);
|
expect(serializer.serialize(t)).toEqual("/a/(c2//left:cp)(left:ap)");
|
||||||
expect(serializer.serialize(t)).toEqual("/a(left:ap)/c2(left:cp)");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should work when the first command is ./)", () => {
|
it("should work when the first command is ./)", () => {
|
||||||
const p = serializer.parse("/a(left:ap)/c(left:cp)");
|
const p = serializer.parse("/a/(c//left:cp)(left:ap)");
|
||||||
const c = p.firstChild(p.root);
|
const t = create(p.root.children[PRIMARY_OUTLET], 0, p, ["./", "c2"]);
|
||||||
const t = create(c, p, ["./", "c2"]);
|
expect(serializer.serialize(t)).toEqual("/a/(c2//left:cp)(left:ap)");
|
||||||
expect(serializer.serialize(t)).toEqual("/a(left:ap)/c2(left:cp)");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should work when given params", () => {
|
it("should work when given params", () => {
|
||||||
const p = serializer.parse("/a(left:ap)/c(left:cp)");
|
const p = serializer.parse("/a/(c//left:cp)(left:ap)");
|
||||||
const c = p.firstChild(p.root);
|
const t = create(p.root.children[PRIMARY_OUTLET], 0, p, [{'x': 99}]);
|
||||||
const t = create(c, p, [{'x': 99}]);
|
expect(serializer.serialize(t)).toEqual("/a/(c;x=99//left:cp)(left:ap)");
|
||||||
expect(serializer.serialize(t)).toEqual("/a(left:ap)/c;x=99(left:cp)");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support going to a parent", () => {
|
it("should work when index > 0", () => {
|
||||||
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 p = serializer.parse("/a/c");
|
||||||
const c = p.firstChild(<any>p.firstChild(p.root));
|
const t = create(p.root.children[PRIMARY_OUTLET], 1, p, ["c2"]);
|
||||||
const t = create(c, p, ["../c2"]);
|
expect(serializer.serialize(t)).toEqual("/a/c/c2");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support going to a parent (within a segment)", () => {
|
||||||
|
const p = serializer.parse("/a/c");
|
||||||
|
const t = create(p.root.children[PRIMARY_OUTLET], 1, p, ["../c2"]);
|
||||||
expect(serializer.serialize(t)).toEqual("/a/c2");
|
expect(serializer.serialize(t)).toEqual("/a/c2");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should work when given ../", () => {
|
it("should work when given ../", () => {
|
||||||
const p = serializer.parse("/a/c");
|
const p = serializer.parse("/a/c");
|
||||||
const c = p.firstChild(<any>p.firstChild(p.root));
|
const t = create(p.root.children[PRIMARY_OUTLET], 1, p, ["../", "c2"]);
|
||||||
const t = create(c, p, ["../"]);
|
expect(serializer.serialize(t)).toEqual("/a/c2");
|
||||||
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", () => {
|
it("should support setting matrix params", () => {
|
||||||
const p = serializer.parse("/a(left:ap)/c(left:cp)");
|
const p = serializer.parse("/a/(c//left:cp)(left:ap)");
|
||||||
const c = p.firstChild(p.root);
|
const t = create(p.root.children[PRIMARY_OUTLET], 0, p, ['../', {x: 5}]);
|
||||||
const t = create(c, p, ["../", {'x': 5}]);
|
|
||||||
expect(serializer.serialize(t)).toEqual("/a;x=5(left:ap)");
|
expect(serializer.serialize(t)).toEqual("/a;x=5(left:ap)");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
xit("should support going to a parent (across segments)", () => {
|
||||||
|
const p = serializer.parse("/q/(a/(c//left:cp)//left:qp)(left:ap)");
|
||||||
|
|
||||||
|
const t = create(p.root.children[PRIMARY_OUTLET].children[PRIMARY_OUTLET], 0, p, ['../../q2']);
|
||||||
|
expect(serializer.serialize(t)).toEqual("/q2(left:ap)");
|
||||||
|
});
|
||||||
|
|
||||||
|
xit("should navigate to the root", () => {
|
||||||
|
const p = serializer.parse("/a/c");
|
||||||
|
const t = create(p.root.children[PRIMARY_OUTLET], 0, p, ['../']);
|
||||||
|
expect(serializer.serialize(t)).toEqual("");
|
||||||
|
});
|
||||||
|
|
||||||
it("should throw when too many ..", () => {
|
it("should throw when too many ..", () => {
|
||||||
const p = serializer.parse("/a(left:ap)/c(left:cp)");
|
const p = serializer.parse("/a/(c//left:cp)(left:ap)");
|
||||||
const c = p.firstChild(p.root);
|
expect(() => create(p.root.children[PRIMARY_OUTLET], 0, p, ['../../'])).toThrowError("Invalid number of '../'");
|
||||||
expect(() => create(c, p, ["../../"])).toThrowError("Invalid number of '../'");
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set query params", () => {
|
it("should set query params", () => {
|
||||||
const p = serializer.parse("/");
|
const p = serializer.parse("/");
|
||||||
const t = create(p.root, p, [], {a: 'hey'});
|
const t = createRoot(p, [], {a: 'hey'});
|
||||||
expect(t.queryParams).toEqual({a: 'hey'});
|
expect(t.queryParams).toEqual({a: 'hey'});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should stringify query params", () => {
|
it("should stringify query params", () => {
|
||||||
const p = serializer.parse("/");
|
const p = serializer.parse("/");
|
||||||
const t = create(p.root, p, [], <any>{a: 1});
|
const t = createRoot(p, [], <any>{a: 1});
|
||||||
expect(t.queryParams).toEqual({a: '1'});
|
expect(t.queryParams).toEqual({a: '1'});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reuse old query params when given undefined", () => {
|
it("should reuse old query params when given undefined", () => {
|
||||||
const p = serializer.parse("/?a=1");
|
const p = serializer.parse("/?a=1");
|
||||||
const t = create(p.root, p, [], undefined);
|
const t = createRoot(p, [], undefined);
|
||||||
expect(t.queryParams).toEqual({a: '1'});
|
expect(t.queryParams).toEqual({a: '1'});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should set fragment", () => {
|
it("should set fragment", () => {
|
||||||
const p = serializer.parse("/");
|
const p = serializer.parse("/");
|
||||||
const t = create(p.root, p, [], {}, "fragment");
|
const t = createRoot(p, [], {}, "fragment");
|
||||||
expect(t.fragment).toEqual("fragment");
|
expect(t.fragment).toEqual("fragment");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should reused old fragment when given undefined", () => {
|
it("should reused old fragment when given undefined", () => {
|
||||||
const p = serializer.parse("/#fragment");
|
const p = serializer.parse("/#fragment");
|
||||||
const t = create(p.root, p, [], undefined, undefined);
|
const t = createRoot(p, [], undefined, undefined);
|
||||||
expect(t.fragment).toEqual("fragment");
|
expect(t.fragment).toEqual("fragment");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function create(start: UrlSegment | null, tree: UrlTree, commands: any[], queryParams?: Params, fragment?: string) {
|
function createRoot(tree: UrlTree, commands: any[], queryParams?: Params, fragment?: string) {
|
||||||
if (!start) {
|
const s = new ActivatedRouteSnapshot([], <any>{}, PRIMARY_OUTLET, "someComponent", null, tree.root, -1);
|
||||||
expect(start).toBeDefined();
|
const a = new ActivatedRoute(new BehaviorSubject(null), new BehaviorSubject(null), PRIMARY_OUTLET, "someComponent", s);
|
||||||
|
advanceActivatedRoute(a);
|
||||||
|
return createUrlTree(a, tree, commands, queryParams, fragment);
|
||||||
|
}
|
||||||
|
|
||||||
|
function create(segment: UrlSegment, startIndex: number, tree: UrlTree, commands: any[], queryParams?: Params, fragment?: string) {
|
||||||
|
if (!segment) {
|
||||||
|
expect(segment).toBeDefined();
|
||||||
}
|
}
|
||||||
const s = new ActivatedRouteSnapshot([], <any>{}, PRIMARY_OUTLET, "someComponent", null, <any>start);
|
const s = new ActivatedRouteSnapshot([], <any>{}, PRIMARY_OUTLET, "someComponent", null, <any>segment, startIndex);
|
||||||
const a = new ActivatedRoute(new BehaviorSubject(null), new BehaviorSubject(null), PRIMARY_OUTLET, "someComponent", s);
|
const a = new ActivatedRoute(new BehaviorSubject(null), new BehaviorSubject(null), PRIMARY_OUTLET, "someComponent", s);
|
||||||
advanceActivatedRoute(a);
|
advanceActivatedRoute(a);
|
||||||
return createUrlTree(a, tree, commands, queryParams, fragment);
|
return createUrlTree(a, tree, commands, queryParams, fragment);
|
||||||
|
@ -2,28 +2,27 @@ import {DefaultUrlSerializer} from '../src/url_serializer';
|
|||||||
import {UrlTree} from '../src/url_tree';
|
import {UrlTree} from '../src/url_tree';
|
||||||
import {Params, PRIMARY_OUTLET} from '../src/shared';
|
import {Params, PRIMARY_OUTLET} from '../src/shared';
|
||||||
import {ActivatedRouteSnapshot} from '../src/router_state';
|
import {ActivatedRouteSnapshot} from '../src/router_state';
|
||||||
|
import {RouterConfig} from '../src/config';
|
||||||
import {recognize} from '../src/recognize';
|
import {recognize} from '../src/recognize';
|
||||||
|
|
||||||
describe('recognize', () => {
|
describe('recognize', () => {
|
||||||
it('should work', (done) => {
|
it('should work', () => {
|
||||||
recognize(RootComponent, [
|
checkRecognize([
|
||||||
{
|
{
|
||||||
path: 'a', component: ComponentA
|
path: 'a', component: ComponentA
|
||||||
}
|
}
|
||||||
], tree("a")).forEach(s => {
|
], "a", s => {
|
||||||
checkActivatedRoute(s.root, "", {}, RootComponent);
|
checkActivatedRoute(s.root, "", {}, RootComponent);
|
||||||
checkActivatedRoute(s.firstChild(s.root), "a", {}, ComponentA);
|
checkActivatedRoute(s.firstChild(s.root), "a", {}, ComponentA);
|
||||||
done();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should support secondary routes', () => {
|
it('should support secondary routes', () => {
|
||||||
recognize(RootComponent, [
|
checkRecognize([
|
||||||
{ path: 'a', component: ComponentA },
|
{ path: 'a', component: ComponentA },
|
||||||
{ path: 'b', component: ComponentB, outlet: 'left' },
|
{ path: 'b', component: ComponentB, outlet: 'left' },
|
||||||
{ path: 'c', component: ComponentC, outlet: 'right' }
|
{ path: 'c', component: ComponentC, outlet: 'right' }
|
||||||
], tree("a(left:b//right:c)")).forEach(s => {
|
], "a(left:b//right:c)", s => {
|
||||||
const c = s.children(s.root);
|
const c = s.children(s.root);
|
||||||
checkActivatedRoute(c[0], "a", {}, ComponentA);
|
checkActivatedRoute(c[0], "a", {}, ComponentA);
|
||||||
checkActivatedRoute(c[1], "b", {}, ComponentB, 'left');
|
checkActivatedRoute(c[1], "b", {}, ComponentB, 'left');
|
||||||
@ -31,43 +30,85 @@ describe('recognize', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should match routes in the depth first order', () => {
|
it('should set url segment and index properly', () => {
|
||||||
|
const url = tree("a(left:b//right:c)");
|
||||||
recognize(RootComponent, [
|
recognize(RootComponent, [
|
||||||
|
{ path: 'a', component: ComponentA },
|
||||||
|
{ path: 'b', component: ComponentB, outlet: 'left' },
|
||||||
|
{ path: 'c', component: ComponentC, outlet: 'right' }
|
||||||
|
], url, "a(left:b//right:c)").subscribe((s) => {
|
||||||
|
expect(s.root._urlSegment).toBe(url.root);
|
||||||
|
expect(s.root._lastPathIndex).toBe(-1);
|
||||||
|
|
||||||
|
const c = s.children(s.root);
|
||||||
|
expect(c[0]._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
|
||||||
|
expect(c[0]._lastPathIndex).toBe(0);
|
||||||
|
|
||||||
|
expect(c[1]._urlSegment).toBe(url.root.children["left"]);
|
||||||
|
expect(c[1]._lastPathIndex).toBe(0);
|
||||||
|
|
||||||
|
expect(c[2]._urlSegment).toBe(url.root.children["right"]);
|
||||||
|
expect(c[2]._lastPathIndex).toBe(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set url segment and index properly (nested case)', () => {
|
||||||
|
const url = tree("a/b/c");
|
||||||
|
recognize(RootComponent, [
|
||||||
|
{ path: '/a/b', component: ComponentA, children: [
|
||||||
|
{path: 'c', component: ComponentC}
|
||||||
|
] },
|
||||||
|
], url, "a/b/c").subscribe(s => {
|
||||||
|
expect(s.root._urlSegment).toBe(url.root);
|
||||||
|
expect(s.root._lastPathIndex).toBe(-1);
|
||||||
|
|
||||||
|
const compA = s.firstChild(s.root);
|
||||||
|
expect(compA._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
|
||||||
|
expect(compA._lastPathIndex).toBe(1);
|
||||||
|
|
||||||
|
const compC = s.firstChild(<any>compA);
|
||||||
|
expect(compC._urlSegment).toBe(url.root.children[PRIMARY_OUTLET]);
|
||||||
|
expect(compC._lastPathIndex).toBe(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should match routes in the depth first order', () => {
|
||||||
|
checkRecognize([
|
||||||
{path: 'a', component: ComponentA, children: [{path: ':id', component: ComponentB}]},
|
{path: 'a', component: ComponentA, children: [{path: ':id', component: ComponentB}]},
|
||||||
{path: 'a/:id', component: ComponentC}
|
{path: 'a/:id', component: ComponentC}
|
||||||
], tree("a/paramA")).forEach(s => {
|
], "a/paramA", s => {
|
||||||
checkActivatedRoute(s.root, "", {}, RootComponent);
|
checkActivatedRoute(s.root, "", {}, RootComponent);
|
||||||
checkActivatedRoute(s.firstChild(s.root), "a", {}, ComponentA);
|
checkActivatedRoute(s.firstChild(s.root), "a", {}, ComponentA);
|
||||||
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "paramA", {id: 'paramA'}, ComponentB);
|
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "paramA", {id: 'paramA'}, ComponentB);
|
||||||
});
|
});
|
||||||
|
|
||||||
recognize(RootComponent, [
|
checkRecognize([
|
||||||
{path: 'a', component: ComponentA},
|
{path: 'a', component: ComponentA},
|
||||||
{path: 'a/:id', component: ComponentC}
|
{path: 'a/:id', component: ComponentC}
|
||||||
], tree("a/paramA")).forEach(s => {
|
], "a/paramA", s => {
|
||||||
checkActivatedRoute(s.root, "", {}, RootComponent);
|
checkActivatedRoute(s.root, "", {}, RootComponent);
|
||||||
checkActivatedRoute(s.firstChild(s.root), "a/paramA", {id: 'paramA'}, ComponentC);
|
checkActivatedRoute(s.firstChild(s.root), "a/paramA", {id: 'paramA'}, ComponentC);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use outlet name when matching secondary routes', () => {
|
it('should use outlet name when matching secondary routes', () => {
|
||||||
recognize(RootComponent, [
|
checkRecognize([
|
||||||
{ path: 'a', component: ComponentA },
|
{ path: 'a', component: ComponentA },
|
||||||
{ path: 'b', component: ComponentB, outlet: 'left' },
|
{ path: 'b', component: ComponentB, outlet: 'left' },
|
||||||
{ path: 'b', component: ComponentC, outlet: 'right' }
|
{ path: 'b', component: ComponentC, outlet: 'right' }
|
||||||
], tree("a(right:b)")).forEach(s => {
|
], "a(right:b)", s => {
|
||||||
const c = s.children(s.root);
|
const c = s.children(s.root);
|
||||||
checkActivatedRoute(c[0], "a", {}, ComponentA);
|
checkActivatedRoute(c[0], "a", {}, ComponentA);
|
||||||
checkActivatedRoute(c[1], "b", {}, ComponentC, 'right');
|
checkActivatedRoute(c[1], "b", {}, ComponentC, 'right');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should handle nested secondary routes', () => {
|
xit('should handle nested secondary routes', () => {
|
||||||
recognize(RootComponent, [
|
checkRecognize([
|
||||||
{ path: 'a', component: ComponentA },
|
{ path: 'a', component: ComponentA },
|
||||||
{ path: 'b', component: ComponentB, outlet: 'left' },
|
{ path: 'b', component: ComponentB, outlet: 'left' },
|
||||||
{ path: 'c', component: ComponentC, outlet: 'right' }
|
{ path: 'c', component: ComponentC, outlet: 'right' }
|
||||||
], tree("a(left:b(right:c))")).forEach(s => {
|
], "a(left:b(right:c))", s => {
|
||||||
const c = s.children(s.root);
|
const c = s.children(s.root);
|
||||||
checkActivatedRoute(c[0], "a", {}, ComponentA);
|
checkActivatedRoute(c[0], "a", {}, ComponentA);
|
||||||
checkActivatedRoute(c[1], "b", {}, ComponentB, 'left');
|
checkActivatedRoute(c[1], "b", {}, ComponentB, 'left');
|
||||||
@ -76,12 +117,12 @@ describe('recognize', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should handle non top-level secondary routes', () => {
|
it('should handle non top-level secondary routes', () => {
|
||||||
recognize(RootComponent, [
|
checkRecognize([
|
||||||
{ path: 'a', component: ComponentA, children: [
|
{ path: 'a', component: ComponentA, children: [
|
||||||
{ path: 'b', component: ComponentB },
|
{ path: 'b', component: ComponentB },
|
||||||
{ path: 'c', component: ComponentC, outlet: 'left' }
|
{ path: 'c', component: ComponentC, outlet: 'left' }
|
||||||
] },
|
] },
|
||||||
], tree("a/b(left:c))")).forEach(s => {
|
], "a/(b//left:c)", s => {
|
||||||
const c = s.children(<any>s.firstChild(s.root));
|
const c = s.children(<any>s.firstChild(s.root));
|
||||||
checkActivatedRoute(c[0], "b", {}, ComponentB, PRIMARY_OUTLET);
|
checkActivatedRoute(c[0], "b", {}, ComponentB, PRIMARY_OUTLET);
|
||||||
checkActivatedRoute(c[1], "c", {}, ComponentC, 'left');
|
checkActivatedRoute(c[1], "c", {}, ComponentC, 'left');
|
||||||
@ -89,11 +130,11 @@ describe('recognize', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should sort routes by outlet name', () => {
|
it('should sort routes by outlet name', () => {
|
||||||
recognize(RootComponent, [
|
checkRecognize([
|
||||||
{ path: 'a', component: ComponentA },
|
{ path: 'a', component: ComponentA },
|
||||||
{ path: 'c', component: ComponentC, outlet: 'c' },
|
{ path: 'c', component: ComponentC, outlet: 'c' },
|
||||||
{ path: 'b', component: ComponentB, outlet: 'b' }
|
{ path: 'b', component: ComponentB, outlet: 'b' }
|
||||||
], tree("a(c:c//b:b)")).forEach(s => {
|
], "a(c:c//b:b)", s => {
|
||||||
const c = s.children(s.root);
|
const c = s.children(s.root);
|
||||||
checkActivatedRoute(c[0], "a", {}, ComponentA);
|
checkActivatedRoute(c[0], "a", {}, ComponentA);
|
||||||
checkActivatedRoute(c[1], "b", {}, ComponentB, 'b');
|
checkActivatedRoute(c[1], "b", {}, ComponentB, 'b');
|
||||||
@ -102,52 +143,52 @@ describe('recognize', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should support matrix parameters', () => {
|
it('should support matrix parameters', () => {
|
||||||
recognize(RootComponent, [
|
checkRecognize([
|
||||||
{
|
{
|
||||||
path: 'a', component: ComponentA, children: [
|
path: 'a', component: ComponentA, children: [
|
||||||
{ path: 'b', component: ComponentB },
|
{ path: 'b', component: ComponentB }
|
||||||
{ path: 'c', component: ComponentC, outlet: 'left' }
|
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
], tree("a;a1=11;a2=22/b;b1=111;b2=222(left:c;c1=1111;c2=2222)")).forEach(s => {
|
{ path: 'c', component: ComponentC, outlet: 'left' }
|
||||||
checkActivatedRoute(s.firstChild(s.root), "a", {a1: '11', a2: '22'}, ComponentA);
|
], "a;a1=11;a2=22/b;b1=111;b2=222(left:c;c1=1111;c2=2222)", s => {
|
||||||
const c = s.children(<any>s.firstChild(s.root));
|
const c = s.children(s.root);
|
||||||
checkActivatedRoute(c[0], "b", {b1: '111', b2: '222'}, ComponentB);
|
checkActivatedRoute(c[0], "a", {a1: '11', a2: '22'}, ComponentA);
|
||||||
|
checkActivatedRoute(s.firstChild(<any>c[0]), "b", {b1: '111', b2: '222'}, ComponentB);
|
||||||
checkActivatedRoute(c[1], "c", {c1: '1111', c2: '2222'}, ComponentC, 'left');
|
checkActivatedRoute(c[1], "c", {c1: '1111', c2: '2222'}, ComponentC, 'left');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("index", () => {
|
describe("index", () => {
|
||||||
it("should support root index routes", () => {
|
it("should support root index routes", () => {
|
||||||
recognize(RootComponent, [
|
checkRecognize([
|
||||||
{index: true, component: ComponentA}
|
{index: true, component: ComponentA}
|
||||||
], tree("")).forEach(s => {
|
], "", s => {
|
||||||
checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA);
|
checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support nested root index routes", () => {
|
it("should support nested root index routes", () => {
|
||||||
recognize(RootComponent, [
|
checkRecognize([
|
||||||
{index: true, component: ComponentA, children: [{index: true, component: ComponentB}]}
|
{index: true, component: ComponentA, children: [{index: true, component: ComponentB}]}
|
||||||
], tree("")).forEach(s => {
|
], "", s => {
|
||||||
checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA);
|
checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA);
|
||||||
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "", {}, ComponentB);
|
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "", {}, ComponentB);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support index routes", () => {
|
it("should support index routes", () => {
|
||||||
recognize(RootComponent, [
|
checkRecognize([
|
||||||
{path: 'a', component: ComponentA, children: [
|
{path: 'a', component: ComponentA, children: [
|
||||||
{index: true, component: ComponentB}
|
{index: true, component: ComponentB}
|
||||||
]}
|
]}
|
||||||
], tree("a")).forEach(s => {
|
], "a", s => {
|
||||||
checkActivatedRoute(s.firstChild(s.root), "a", {}, ComponentA);
|
checkActivatedRoute(s.firstChild(s.root), "a", {}, ComponentA);
|
||||||
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "", {}, ComponentB);
|
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "", {}, ComponentB);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should support index routes with children", () => {
|
it("should support index routes with children", () => {
|
||||||
recognize(RootComponent, [
|
checkRecognize([
|
||||||
{
|
{
|
||||||
index: true, component: ComponentA, children: [
|
index: true, component: ComponentA, children: [
|
||||||
{ index: true, component: ComponentB, children: [
|
{ index: true, component: ComponentB, children: [
|
||||||
@ -156,7 +197,7 @@ describe('recognize', () => {
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
], tree("c/10")).forEach(s => {
|
], "c/10", s => {
|
||||||
checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA);
|
checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA);
|
||||||
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "", {}, ComponentB);
|
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "", {}, ComponentB);
|
||||||
checkActivatedRoute(
|
checkActivatedRoute(
|
||||||
@ -164,21 +205,96 @@ describe('recognize', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should pass parameters to every nested index route (case with non-index route)", () => {
|
xit("should pass parameters to every nested index route (case with non-index route)", () => {
|
||||||
recognize(RootComponent, [
|
checkRecognize([
|
||||||
{path: 'a', component: ComponentA, children: [{index: true, component: ComponentB}]}
|
{path: 'a', component: ComponentA, children: [{index: true, component: ComponentB}]}
|
||||||
], tree("/a;a=1")).forEach(s => {
|
], "/a;a=1", s => {
|
||||||
checkActivatedRoute(s.firstChild(s.root), "a", {a: '1'}, ComponentA);
|
checkActivatedRoute(s.firstChild(s.root), "a", {a: '1'}, ComponentA);
|
||||||
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "", {a: '1'}, ComponentB);
|
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "", {a: '1'}, ComponentB);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("matching empty url", () => {
|
||||||
|
it("should support root index routes", () => {
|
||||||
|
recognize(RootComponent, [
|
||||||
|
{path: '', component: ComponentA}
|
||||||
|
], tree(""), "").forEach(s => {
|
||||||
|
checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support nested root index routes", () => {
|
||||||
|
recognize(RootComponent, [
|
||||||
|
{path: '', component: ComponentA, children: [{path: '', component: ComponentB}]}
|
||||||
|
], tree(""), "").forEach(s => {
|
||||||
|
checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA);
|
||||||
|
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "", {}, ComponentB);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set url segment and index properly', () => {
|
||||||
|
const url = tree("");
|
||||||
|
recognize(RootComponent, [
|
||||||
|
{path: '', component: ComponentA, children: [{path: '', component: ComponentB}]}
|
||||||
|
], url, "").forEach(s => {
|
||||||
|
expect(s.root._urlSegment).toBe(url.root);
|
||||||
|
expect(s.root._lastPathIndex).toBe(-1);
|
||||||
|
|
||||||
|
const c = s.firstChild(s.root);
|
||||||
|
expect(c._urlSegment).toBe(url.root);
|
||||||
|
expect(c._lastPathIndex).toBe(-1);
|
||||||
|
|
||||||
|
const c2 = s.firstChild(<any>s.firstChild(s.root));
|
||||||
|
expect(c2._urlSegment).toBe(url.root);
|
||||||
|
expect(c2._lastPathIndex).toBe(-1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support index routes", () => {
|
||||||
|
recognize(RootComponent, [
|
||||||
|
{path: 'a', component: ComponentA, children: [
|
||||||
|
{path: '', component: ComponentB}
|
||||||
|
]}
|
||||||
|
], tree("a"), "a").forEach(s => {
|
||||||
|
checkActivatedRoute(s.firstChild(s.root), "a", {}, ComponentA);
|
||||||
|
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "", {}, ComponentB);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should support index routes with children", () => {
|
||||||
|
recognize(RootComponent, [
|
||||||
|
{
|
||||||
|
path: '', component: ComponentA, children: [
|
||||||
|
{ path: '', component: ComponentB, children: [
|
||||||
|
{path: 'c/:id', component: ComponentC}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
], tree("c/10"), "c/10").forEach(s => {
|
||||||
|
checkActivatedRoute(s.firstChild(s.root), "", {}, ComponentA);
|
||||||
|
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "", {}, ComponentB);
|
||||||
|
checkActivatedRoute(
|
||||||
|
s.firstChild(<any>s.firstChild(<any>s.firstChild(s.root))), "c/10", {id: '10'}, ComponentC);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
xit("should pass parameters to every nested index route (case with non-index route)", () => {
|
||||||
|
recognize(RootComponent, [
|
||||||
|
{path: 'a', component: ComponentA, children: [{path: '', component: ComponentB}]}
|
||||||
|
], tree("/a;a=1"), "/a;a=1").forEach(s => {
|
||||||
|
checkActivatedRoute(s.firstChild(s.root), "a", {a: '1'}, ComponentA);
|
||||||
|
checkActivatedRoute(s.firstChild(<any>s.firstChild(s.root)), "", {a: '1'}, ComponentB);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("wildcards", () => {
|
describe("wildcards", () => {
|
||||||
it("should support simple wildcards", () => {
|
it("should support simple wildcards", () => {
|
||||||
recognize(RootComponent, [
|
checkRecognize([
|
||||||
{path: '**', component: ComponentA}
|
{path: '**', component: ComponentA}
|
||||||
], tree("a/b/c/d;a1=11")).forEach(s => {
|
], "a/b/c/d;a1=11", s => {
|
||||||
checkActivatedRoute(s.firstChild(s.root), "a/b/c/d", {a1:'11'}, ComponentA);
|
checkActivatedRoute(s.firstChild(s.root), "a/b/c/d", {a1:'11'}, ComponentA);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -187,7 +303,7 @@ describe('recognize', () => {
|
|||||||
describe("query parameters", () => {
|
describe("query parameters", () => {
|
||||||
it("should support query params", () => {
|
it("should support query params", () => {
|
||||||
const config = [{path: 'a', component: ComponentA}];
|
const config = [{path: 'a', component: ComponentA}];
|
||||||
recognize(RootComponent, config, tree("a?q=11")).forEach(s => {
|
checkRecognize(config, "a?q=11", s => {
|
||||||
expect(s.queryParams).toEqual({q: '11'});
|
expect(s.queryParams).toEqual({q: '11'});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -196,7 +312,7 @@ describe('recognize', () => {
|
|||||||
describe("fragment", () => {
|
describe("fragment", () => {
|
||||||
it("should support fragment", () => {
|
it("should support fragment", () => {
|
||||||
const config = [{path: 'a', component: ComponentA}];
|
const config = [{path: 'a', component: ComponentA}];
|
||||||
recognize(RootComponent, config, tree("a#f1")).forEach(s => {
|
checkRecognize(config, "a#f1", s => {
|
||||||
expect(s.fragment).toEqual("f1");
|
expect(s.fragment).toEqual("f1");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -208,7 +324,7 @@ describe('recognize', () => {
|
|||||||
{ path: 'a', component: ComponentA },
|
{ path: 'a', component: ComponentA },
|
||||||
{ path: 'b', component: ComponentB, outlet: 'aux' },
|
{ path: 'b', component: ComponentB, outlet: 'aux' },
|
||||||
{ path: 'c', component: ComponentC, outlet: 'aux' }
|
{ path: 'c', component: ComponentC, outlet: 'aux' }
|
||||||
], tree("a(aux:b//aux:c)")).subscribe((_) => {}, s => {
|
], tree("a(aux:b//aux:c)"), "a(aux:b//aux:c)").subscribe((_) => {}, s => {
|
||||||
expect(s.toString()).toContain("Two segments cannot have the same outlet name: 'aux:b' and 'aux:c'.");
|
expect(s.toString()).toContain("Two segments cannot have the same outlet name: 'aux:b' and 'aux:c'.");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -216,7 +332,7 @@ describe('recognize', () => {
|
|||||||
it("should error when no matching routes", () => {
|
it("should error when no matching routes", () => {
|
||||||
recognize(RootComponent, [
|
recognize(RootComponent, [
|
||||||
{ path: 'a', component: ComponentA }
|
{ path: 'a', component: ComponentA }
|
||||||
], tree("invalid")).subscribe((_) => {}, s => {
|
], tree("invalid"), "invalid").subscribe((_) => {}, s => {
|
||||||
expect(s.toString()).toContain("Cannot match any routes");
|
expect(s.toString()).toContain("Cannot match any routes");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -224,18 +340,24 @@ describe('recognize', () => {
|
|||||||
it("should error when no matching routes (too short)", () => {
|
it("should error when no matching routes (too short)", () => {
|
||||||
recognize(RootComponent, [
|
recognize(RootComponent, [
|
||||||
{ path: 'a/:id', component: ComponentA }
|
{ path: 'a/:id', component: ComponentA }
|
||||||
], tree("a")).subscribe((_) => {}, s => {
|
], tree("a"), "a").subscribe((_) => {}, s => {
|
||||||
expect(s.toString()).toContain("Cannot match any routes");
|
expect(s.toString()).toContain("Cannot match any routes");
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function checkRecognize(config: RouterConfig, url: string, callback: any): void {
|
||||||
|
recognize(RootComponent, config, tree(url), url).subscribe(callback, e => {
|
||||||
|
throw e;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function checkActivatedRoute(actual: ActivatedRouteSnapshot | null, url: string, params: Params, cmp: Function, outlet: string = PRIMARY_OUTLET):void {
|
function checkActivatedRoute(actual: ActivatedRouteSnapshot | null, url: string, params: Params, cmp: Function, outlet: string = PRIMARY_OUTLET):void {
|
||||||
if (actual === null) {
|
if (actual === null) {
|
||||||
expect(actual).not.toBeNull();
|
expect(actual).not.toBeNull();
|
||||||
} else {
|
} else {
|
||||||
expect(actual.urlSegments.map(s => s.path).join("/")).toEqual(url);
|
expect(actual.url.map(s => s.path).join("/")).toEqual(url);
|
||||||
expect(actual.params).toEqual(params);
|
expect(actual.params).toEqual(params);
|
||||||
expect(actual.component).toBe(cmp);
|
expect(actual.component).toBe(cmp);
|
||||||
expect(actual.outlet).toEqual(outlet);
|
expect(actual.outlet).toEqual(outlet);
|
||||||
|
@ -27,6 +27,7 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
beforeEachProviders(() => {
|
beforeEachProviders(() => {
|
||||||
let config: RouterConfig = [
|
let config: RouterConfig = [
|
||||||
|
{ path: '', component: BlankCmp },
|
||||||
{ path: 'simple', component: SimpleCmp }
|
{ path: 'simple', component: SimpleCmp }
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -54,19 +55,20 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
router.navigateByUrl('/simple');
|
router.navigateByUrl('/simple');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
|
||||||
expect(location.path()).toEqual('/simple');
|
expect(location.path()).toEqual('/simple');
|
||||||
})));
|
})));
|
||||||
|
|
||||||
|
|
||||||
it('should update location when navigating',
|
it('should update location when navigating',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp }
|
{ path: 'team/:id', component: TeamCmp }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
advance(fixture);
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/22');
|
router.navigateByUrl('/team/22');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(location.path()).toEqual('/team/22');
|
expect(location.path()).toEqual('/team/22');
|
||||||
@ -79,6 +81,9 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
xit('should navigate back and forward',
|
xit('should navigate back and forward',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, children: [
|
{ path: 'team/:id', component: TeamCmp, children: [
|
||||||
{ path: 'simple', component: SimpleCmp },
|
{ path: 'simple', component: SimpleCmp },
|
||||||
@ -86,7 +91,6 @@ describe("Integration", () => {
|
|||||||
] }
|
] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/33/simple');
|
router.navigateByUrl('/team/33/simple');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
@ -106,14 +110,15 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it('should navigate when locations changes',
|
it('should navigate when locations changes',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, children: [
|
{ path: 'team/:id', component: TeamCmp, children: [
|
||||||
{ path: 'user/:name', component: UserCmp }
|
{ path: 'user/:name', component: UserCmp }
|
||||||
] }
|
] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/22/user/victor');
|
router.navigateByUrl('/team/22/user/victor');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
|
||||||
@ -125,6 +130,9 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it('should support secondary routes',
|
it('should support secondary routes',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, children: [
|
{ path: 'team/:id', component: TeamCmp, children: [
|
||||||
{ path: 'user/:name', component: UserCmp },
|
{ path: 'user/:name', component: UserCmp },
|
||||||
@ -132,9 +140,7 @@ describe("Integration", () => {
|
|||||||
] }
|
] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
router.navigateByUrl('/team/22/(user/victor//right:simple)');
|
||||||
|
|
||||||
router.navigateByUrl('/team/22/user/victor(right:simple)');
|
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
|
||||||
expect(fixture.debugElement.nativeElement)
|
expect(fixture.debugElement.nativeElement)
|
||||||
@ -143,6 +149,9 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it('should deactivate outlets',
|
it('should deactivate outlets',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, children: [
|
{ path: 'team/:id', component: TeamCmp, children: [
|
||||||
{ path: 'user/:name', component: UserCmp },
|
{ path: 'user/:name', component: UserCmp },
|
||||||
@ -150,9 +159,7 @@ describe("Integration", () => {
|
|||||||
] }
|
] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
router.navigateByUrl('/team/22/(user/victor//right:simple)');
|
||||||
|
|
||||||
router.navigateByUrl('/team/22/user/victor(right:simple)');
|
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
|
||||||
router.navigateByUrl('/team/22/user/victor');
|
router.navigateByUrl('/team/22/user/victor');
|
||||||
@ -163,16 +170,18 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it('should deactivate nested outlets',
|
it('should deactivate nested outlets',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, children: [
|
{ path: 'team/:id', component: TeamCmp, children: [
|
||||||
{ path: 'user/:name', component: UserCmp },
|
{ path: 'user/:name', component: UserCmp },
|
||||||
{ path: 'simple', component: SimpleCmp, outlet: 'right' }
|
{ path: 'simple', component: SimpleCmp, outlet: 'right' }
|
||||||
] }
|
] },
|
||||||
|
{ path: '', component: BlankCmp}
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
router.navigateByUrl('/team/22/(user/victor//right:simple)');
|
||||||
|
|
||||||
router.navigateByUrl('/team/22/user/victor(right:simple)');
|
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
|
||||||
router.navigateByUrl('/');
|
router.navigateByUrl('/');
|
||||||
@ -183,12 +192,13 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it('should set query params and fragment',
|
it('should set query params and fragment',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'query', component: QueryParamsAndFragmentCmp }
|
{ path: 'query', component: QueryParamsAndFragmentCmp }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
|
|
||||||
router.navigateByUrl('/query?name=1#fragment1');
|
router.navigateByUrl('/query?name=1#fragment1');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(fixture.debugElement.nativeElement).toHaveText('query: 1 fragment: fragment1');
|
expect(fixture.debugElement.nativeElement).toHaveText('query: 1 fragment: fragment1');
|
||||||
@ -200,14 +210,15 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it('should push params only when they change',
|
it('should push params only when they change',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb:TestComponentBuilder) => {
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb:TestComponentBuilder) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, children: [
|
{ path: 'team/:id', component: TeamCmp, children: [
|
||||||
{ path: 'user/:name', component: UserCmp }
|
{ path: 'user/:name', component: UserCmp }
|
||||||
] }
|
] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/22/user/victor');
|
router.navigateByUrl('/team/22/user/victor');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
const team = fixture.debugElement.children[1].componentInstance;
|
const team = fixture.debugElement.children[1].componentInstance;
|
||||||
@ -225,13 +236,14 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it('should work when navigating to /',
|
it('should work when navigating to /',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb:TestComponentBuilder) => {
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb:TestComponentBuilder) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ index: true, component: SimpleCmp },
|
{ index: true, component: SimpleCmp },
|
||||||
{ path: '/user/:name', component: UserCmp }
|
{ path: '/user/:name', component: UserCmp }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
|
|
||||||
router.navigateByUrl('/user/victor');
|
router.navigateByUrl('/user/victor');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
|
||||||
@ -245,6 +257,9 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it("should cancel in-flight navigations",
|
it("should cancel in-flight navigations",
|
||||||
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb:TestComponentBuilder) => {
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb:TestComponentBuilder) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: '/user/:name', component: UserCmp }
|
{ path: '/user/:name', component: UserCmp }
|
||||||
]);
|
]);
|
||||||
@ -252,7 +267,6 @@ describe("Integration", () => {
|
|||||||
const recordedEvents = [];
|
const recordedEvents = [];
|
||||||
router.events.forEach(e => recordedEvents.push(e));
|
router.events.forEach(e => recordedEvents.push(e));
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
router.navigateByUrl('/user/init');
|
router.navigateByUrl('/user/init');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
|
||||||
@ -269,7 +283,7 @@ describe("Integration", () => {
|
|||||||
expect(fixture.debugElement.nativeElement).toHaveText('user fedor');
|
expect(fixture.debugElement.nativeElement).toHaveText('user fedor');
|
||||||
expect(user.recordedParams).toEqual([{name: 'init'}, {name: 'fedor'}]);
|
expect(user.recordedParams).toEqual([{name: 'init'}, {name: 'fedor'}]);
|
||||||
|
|
||||||
expectEvents(router, recordedEvents.slice(2), [
|
expectEvents(recordedEvents, [
|
||||||
[NavigationStart, '/user/init'],
|
[NavigationStart, '/user/init'],
|
||||||
[RoutesRecognized, '/user/init'],
|
[RoutesRecognized, '/user/init'],
|
||||||
[NavigationEnd, '/user/init'],
|
[NavigationEnd, '/user/init'],
|
||||||
@ -285,6 +299,9 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it("should handle failed navigations gracefully",
|
it("should handle failed navigations gracefully",
|
||||||
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb:TestComponentBuilder) => {
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb:TestComponentBuilder) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: '/user/:name', component: UserCmp }
|
{ path: '/user/:name', component: UserCmp }
|
||||||
]);
|
]);
|
||||||
@ -292,9 +309,6 @@ describe("Integration", () => {
|
|||||||
const recordedEvents = [];
|
const recordedEvents = [];
|
||||||
router.events.forEach(e => recordedEvents.push(e));
|
router.events.forEach(e => recordedEvents.push(e));
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
advance(fixture);
|
|
||||||
|
|
||||||
let e;
|
let e;
|
||||||
router.navigateByUrl('/invalid').catch(_ => e = _);
|
router.navigateByUrl('/invalid').catch(_ => e = _);
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
@ -305,7 +319,7 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
expect(fixture.debugElement.nativeElement).toHaveText('user fedor');
|
expect(fixture.debugElement.nativeElement).toHaveText('user fedor');
|
||||||
|
|
||||||
expectEvents(router, recordedEvents.slice(2), [
|
expectEvents(recordedEvents, [
|
||||||
[NavigationStart, '/invalid'],
|
[NavigationStart, '/invalid'],
|
||||||
[NavigationError, '/invalid'],
|
[NavigationError, '/invalid'],
|
||||||
|
|
||||||
@ -318,6 +332,9 @@ describe("Integration", () => {
|
|||||||
describe("router links", () => {
|
describe("router links", () => {
|
||||||
it("should support string router links",
|
it("should support string router links",
|
||||||
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, children: [
|
{ path: 'team/:id', component: TeamCmp, children: [
|
||||||
{ path: 'link', component: StringLinkCmp },
|
{ path: 'link', component: StringLinkCmp },
|
||||||
@ -325,9 +342,6 @@ describe("Integration", () => {
|
|||||||
] }
|
] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
advance(fixture);
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/22/link');
|
router.navigateByUrl('/team/22/link');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { link, right: }');
|
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { link, right: }');
|
||||||
@ -342,6 +356,9 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it("should support absolute router links",
|
it("should support absolute router links",
|
||||||
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, children: [
|
{ path: 'team/:id', component: TeamCmp, children: [
|
||||||
{ path: 'link', component: AbsoluteLinkCmp },
|
{ path: 'link', component: AbsoluteLinkCmp },
|
||||||
@ -349,9 +366,6 @@ describe("Integration", () => {
|
|||||||
] }
|
] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
advance(fixture);
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/22/link');
|
router.navigateByUrl('/team/22/link');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { link, right: }');
|
expect(fixture.debugElement.nativeElement).toHaveText('team 22 { link, right: }');
|
||||||
@ -366,6 +380,9 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it("should support relative router links",
|
it("should support relative router links",
|
||||||
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, children: [
|
{ path: 'team/:id', component: TeamCmp, children: [
|
||||||
{ path: 'link', component: RelativeLinkCmp },
|
{ path: 'link', component: RelativeLinkCmp },
|
||||||
@ -373,9 +390,6 @@ describe("Integration", () => {
|
|||||||
] }
|
] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
advance(fixture);
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/22/link');
|
router.navigateByUrl('/team/22/link');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(fixture.debugElement.nativeElement)
|
expect(fixture.debugElement.nativeElement)
|
||||||
@ -394,11 +408,15 @@ describe("Integration", () => {
|
|||||||
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
fakeAsync(inject([Router, TestComponentBuilder], (router, tcb) => {
|
||||||
let fixture = tcb.createFakeAsync(AbsoluteLinkCmp);
|
let fixture = tcb.createFakeAsync(AbsoluteLinkCmp);
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
|
||||||
expect(fixture.debugElement.nativeElement).toHaveText('link');
|
expect(fixture.debugElement.nativeElement).toHaveText('link');
|
||||||
})));
|
})));
|
||||||
|
|
||||||
it("should support query params and fragments",
|
it("should support query params and fragments",
|
||||||
fakeAsync(inject([Router, Location, TestComponentBuilder], (router, location, tcb) => {
|
fakeAsync(inject([Router, Location, TestComponentBuilder], (router, location, tcb) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, children: [
|
{ path: 'team/:id', component: TeamCmp, children: [
|
||||||
{ path: 'link', component: LinkWithQueryParamsAndFragment },
|
{ path: 'link', component: LinkWithQueryParamsAndFragment },
|
||||||
@ -406,9 +424,6 @@ describe("Integration", () => {
|
|||||||
] }
|
] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
advance(fixture);
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/22/link');
|
router.navigateByUrl('/team/22/link');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
|
||||||
@ -426,14 +441,14 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
describe("redirects", () => {
|
describe("redirects", () => {
|
||||||
it("should work", fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
it("should work", fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: '/old/team/:id', redirectTo: 'team/:id' },
|
{ path: '/old/team/:id', redirectTo: 'team/:id' },
|
||||||
{ path: '/team/:id', component: TeamCmp }
|
{ path: '/team/:id', component: TeamCmp }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
advance(fixture);
|
|
||||||
|
|
||||||
router.navigateByUrl('old/team/22');
|
router.navigateByUrl('old/team/22');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
|
||||||
@ -450,17 +465,17 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it('works',
|
it('works',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, canActivate: ["alwaysFalse"] }
|
{ path: 'team/:id', component: TeamCmp, canActivate: ["alwaysFalse"] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
advance(fixture);
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/22');
|
router.navigateByUrl('/team/22');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
|
||||||
expect(location.path()).toEqual('');
|
expect(location.path()).toEqual('/');
|
||||||
})));
|
})));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -471,13 +486,13 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it('works',
|
it('works',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, canActivate: ["alwaysTrue"] }
|
{ path: 'team/:id', component: TeamCmp, canActivate: ["alwaysTrue"] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
advance(fixture);
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/22');
|
router.navigateByUrl('/team/22');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
|
||||||
@ -496,13 +511,13 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it('works',
|
it('works',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, canActivate: [AlwaysTrue] }
|
{ path: 'team/:id', component: TeamCmp, canActivate: [AlwaysTrue] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
advance(fixture);
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/22');
|
router.navigateByUrl('/team/22');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
|
||||||
@ -519,16 +534,16 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it('works',
|
it('works',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, canActivate: ['CanActivate'] }
|
{ path: 'team/:id', component: TeamCmp, canActivate: ['CanActivate'] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
advance(fixture);
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/22');
|
router.navigateByUrl('/team/22');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(location.path()).toEqual('');
|
expect(location.path()).toEqual('/');
|
||||||
})));
|
})));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -544,13 +559,13 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it('works',
|
it('works',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, canDeactivate: ["CanDeactivate"] }
|
{ path: 'team/:id', component: TeamCmp, canDeactivate: ["CanDeactivate"] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
advance(fixture);
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/22');
|
router.navigateByUrl('/team/22');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
|
|
||||||
@ -579,13 +594,13 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it('works',
|
it('works',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, canDeactivate: [AlwaysTrue] }
|
{ path: 'team/:id', component: TeamCmp, canDeactivate: [AlwaysTrue] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
advance(fixture);
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/22');
|
router.navigateByUrl('/team/22');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(location.path()).toEqual('/team/22');
|
expect(location.path()).toEqual('/team/22');
|
||||||
@ -606,13 +621,13 @@ describe("Integration", () => {
|
|||||||
|
|
||||||
it('works',
|
it('works',
|
||||||
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
fakeAsync(inject([Router, TestComponentBuilder, Location], (router, tcb, location) => {
|
||||||
|
const fixture = tcb.createFakeAsync(RootCmp);
|
||||||
|
advance(fixture);
|
||||||
|
|
||||||
router.resetConfig([
|
router.resetConfig([
|
||||||
{ path: 'team/:id', component: TeamCmp, canDeactivate: ['CanDeactivate'] }
|
{ path: 'team/:id', component: TeamCmp, canDeactivate: ['CanDeactivate'] }
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const fixture = tcb.createFakeAsync(RootCmp);
|
|
||||||
advance(fixture);
|
|
||||||
|
|
||||||
router.navigateByUrl('/team/22');
|
router.navigateByUrl('/team/22');
|
||||||
advance(fixture);
|
advance(fixture);
|
||||||
expect(location.path()).toEqual('/team/22');
|
expect(location.path()).toEqual('/team/22');
|
||||||
@ -625,10 +640,10 @@ describe("Integration", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function expectEvents(router: Router, events:Event[], pairs: any[]) {
|
function expectEvents(events:Event[], pairs: any[]) {
|
||||||
for (let i = 0; i < events.length; ++i) {
|
for (let i = 0; i < events.length; ++i) {
|
||||||
expect((<any>events[i].constructor).name).toBe(pairs[i][0].name);
|
expect((<any>events[i].constructor).name).toBe(pairs[i][0].name);
|
||||||
expect(router.serializeUrl((<any>events[i]).url)).toBe(pairs[i][1]);
|
expect((<any>events[i]).url).toBe(pairs[i][1]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -641,7 +656,7 @@ class StringLinkCmp {}
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'link-cmp',
|
selector: 'link-cmp',
|
||||||
template: `<a [routerLink]="['/team/33/simple']">link</a>`,
|
template: `<router-outlet></router-outlet><a [routerLink]="['/team/33/simple']">link</a>`,
|
||||||
directives: ROUTER_DIRECTIVES
|
directives: ROUTER_DIRECTIVES
|
||||||
})
|
})
|
||||||
class AbsoluteLinkCmp {}
|
class AbsoluteLinkCmp {}
|
||||||
@ -668,6 +683,14 @@ class LinkWithQueryParamsAndFragment {}
|
|||||||
class SimpleCmp {
|
class SimpleCmp {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'blank-cmp',
|
||||||
|
template: ``,
|
||||||
|
directives: ROUTER_DIRECTIVES
|
||||||
|
})
|
||||||
|
class BlankCmp {
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'team-cmp',
|
selector: 'team-cmp',
|
||||||
template: `team {{id | async}} { <router-outlet></router-outlet>, right: <router-outlet name="right"></router-outlet> }`,
|
template: `team {{id | async}} { <router-outlet></router-outlet>, right: <router-outlet name="right"></router-outlet> }`,
|
||||||
|
@ -1,76 +1,80 @@
|
|||||||
import {DefaultUrlSerializer, serializeSegment} from '../src/url_serializer';
|
import {DefaultUrlSerializer, serializePath} from '../src/url_serializer';
|
||||||
import {UrlSegment} from '../src/url_tree';
|
import {UrlSegment} from '../src/url_tree';
|
||||||
|
import {PRIMARY_OUTLET} from '../src/shared';
|
||||||
|
|
||||||
describe('url serializer', () => {
|
describe('url serializer', () => {
|
||||||
const url = new DefaultUrlSerializer();
|
const url = new DefaultUrlSerializer();
|
||||||
|
|
||||||
it('should parse the root url', () => {
|
it('should parse the root url', () => {
|
||||||
const tree = url.parse("/");
|
const tree = url.parse("/");
|
||||||
|
|
||||||
expectSegment(tree.root, "");
|
expectSegment(tree.root, "");
|
||||||
expect(url.serialize(tree)).toEqual("");
|
expect(url.serialize(tree)).toEqual("/");
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should parse non-empty urls', () => {
|
it('should parse non-empty urls', () => {
|
||||||
const tree = url.parse("one/two");
|
const tree = url.parse("one/two");
|
||||||
const one = tree.firstChild(tree.root);
|
expectSegment(tree.root.children[PRIMARY_OUTLET], "one/two");
|
||||||
|
|
||||||
expectSegment(one, "one");
|
|
||||||
expectSegment(tree.firstChild(<any>one), "two");
|
|
||||||
expect(url.serialize(tree)).toEqual("/one/two");
|
expect(url.serialize(tree)).toEqual("/one/two");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should parse multiple secondary segments", () => {
|
it("should parse multiple secondary segments", () => {
|
||||||
const tree = url.parse("/one/two(left:three//right:four)/five");
|
const tree = url.parse("/one/two(left:three//right:four)");
|
||||||
const c = tree.children(<any>tree.firstChild(tree.root));
|
|
||||||
|
|
||||||
expectSegment(c[0], "two");
|
expectSegment(tree.root.children[PRIMARY_OUTLET], "one/two");
|
||||||
expectSegment(c[1], "left:three");
|
expectSegment(tree.root.children['left'], "three");
|
||||||
expectSegment(c[2], "right:four");
|
expectSegment(tree.root.children['right'], "four");
|
||||||
|
|
||||||
expectSegment(tree.firstChild(c[0]), "five");
|
expect(url.serialize(tree)).toEqual("/one/two(left:three//right:four)");
|
||||||
|
|
||||||
expect(url.serialize(tree)).toEqual("/one/two(left:three//right:four)/five");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should parse secondary segments that have secondary segments", () => {
|
it("should parse scoped secondary segments", () => {
|
||||||
const tree = url.parse("/one(left:two(right:three))");
|
const tree = url.parse("/one/(two//left:three)");
|
||||||
const c = tree.children(tree.root);
|
|
||||||
|
|
||||||
expectSegment(c[0], "one");
|
const primary = tree.root.children[PRIMARY_OUTLET];
|
||||||
expectSegment(c[1], "left:two");
|
expectSegment(primary, "one", true);
|
||||||
expectSegment(c[2], "right:three");
|
|
||||||
|
|
||||||
expect(url.serialize(tree)).toEqual("/one(left:two//right:three)");
|
expectSegment(primary.children[PRIMARY_OUTLET], "two");
|
||||||
|
expectSegment(primary.children["left"], "three");
|
||||||
|
|
||||||
|
expect(url.serialize(tree)).toEqual("/one/(two//left:three)");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should parse scoped secondary segments with unscoped ones", () => {
|
||||||
|
const tree = url.parse("/one/(two//left:three)(right:four)");
|
||||||
|
|
||||||
|
const primary = tree.root.children[PRIMARY_OUTLET];
|
||||||
|
expectSegment(primary, "one", true);
|
||||||
|
expectSegment(primary.children[PRIMARY_OUTLET], "two");
|
||||||
|
expectSegment(primary.children["left"], "three");
|
||||||
|
expectSegment(tree.root.children["right"], "four");
|
||||||
|
|
||||||
|
expect(url.serialize(tree)).toEqual("/one/(two//left:three)(right:four)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should parse secondary segments that have children", () => {
|
it("should parse secondary segments that have children", () => {
|
||||||
const tree = url.parse("/one(left:two/three)");
|
const tree = url.parse("/one(left:two/three)");
|
||||||
const c = tree.children(tree.root);
|
|
||||||
|
|
||||||
expectSegment(c[0], "one");
|
expectSegment(tree.root.children[PRIMARY_OUTLET], "one");
|
||||||
expectSegment(c[1], "left:two");
|
expectSegment(tree.root.children['left'], "two/three");
|
||||||
expectSegment(tree.firstChild(c[1]), "three");
|
|
||||||
|
|
||||||
expect(url.serialize(tree)).toEqual("/one(left:two/three)");
|
expect(url.serialize(tree)).toEqual("/one(left:two/three)");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should parse an empty secondary segment group", () => {
|
it("should parse an empty secondary segment group", () => {
|
||||||
const tree = url.parse("/one()");
|
const tree = url.parse("/one()");
|
||||||
const c = tree.children(tree.root);
|
|
||||||
|
|
||||||
expectSegment(c[0], "one");
|
expectSegment(tree.root.children[PRIMARY_OUTLET], "one");
|
||||||
expect(tree.children(c[0]).length).toEqual(0);
|
|
||||||
|
|
||||||
expect(url.serialize(tree)).toEqual("/one");
|
expect(url.serialize(tree)).toEqual("/one");
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should parse key-value matrix params", () => {
|
it("should parse key-value matrix params", () => {
|
||||||
const tree = url.parse("/one;a=11a;b=11b(left:two;c=22//right:three;d=33)");
|
const tree = url.parse("/one;a=11a;b=11b(left:two;c=22//right:three;d=33)");
|
||||||
const c = tree.children(tree.root);
|
|
||||||
|
|
||||||
expectSegment(c[0], "one;a=11a;b=11b");
|
expectSegment(tree.root.children[PRIMARY_OUTLET], "one;a=11a;b=11b");
|
||||||
expectSegment(c[1], "left:two;c=22");
|
expectSegment(tree.root.children["left"], "two;c=22");
|
||||||
expectSegment(c[2], "right:three;d=33");
|
expectSegment(tree.root.children["right"], "three;d=33");
|
||||||
|
|
||||||
expect(url.serialize(tree)).toEqual("/one;a=11a;b=11b(left:two;c=22//right:three;d=33)");
|
expect(url.serialize(tree)).toEqual("/one;a=11a;b=11b(left:two;c=22//right:three;d=33)");
|
||||||
});
|
});
|
||||||
@ -78,8 +82,7 @@ describe('url serializer', () => {
|
|||||||
it("should parse key only matrix params", () => {
|
it("should parse key only matrix params", () => {
|
||||||
const tree = url.parse("/one;a");
|
const tree = url.parse("/one;a");
|
||||||
|
|
||||||
const c = tree.firstChild(tree.root);
|
expectSegment(tree.root.children[PRIMARY_OUTLET], "one;a=true");
|
||||||
expectSegment(c, "one;a=true");
|
|
||||||
|
|
||||||
expect(url.serialize(tree)).toEqual("/one;a=true");
|
expect(url.serialize(tree)).toEqual("/one;a=true");
|
||||||
});
|
});
|
||||||
@ -112,6 +115,8 @@ describe('url serializer', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function expectSegment(segment:UrlSegment | null, expected:string):void {
|
function expectSegment(segment:UrlSegment, expected:string, hasChildren: boolean = false):void {
|
||||||
expect(segment ? serializeSegment(segment) : null).toEqual(expected);
|
const p = segment.pathsWithParams.map(p => serializePath(p)).join("/");
|
||||||
|
expect(p).toEqual(expected);
|
||||||
|
expect(Object.keys(segment.children).length > 0).toEqual(hasChildren);
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user