feat(router): rename UrlPathWithParams into UrlSegment

BREAKING CHANGE:

UrlPathWithParams => UrlSegment
UrlSegment => UrlSegmentGroup
This commit is contained in:
vsavkin 2016-07-25 12:15:07 -07:00
parent 2b63330a36
commit 6f68330fa5
12 changed files with 407 additions and 379 deletions

View File

@ -19,4 +19,4 @@ export {RouterOutletMap} from './src/router_outlet_map';
export {provideRouter} from './src/router_providers';
export {ActivatedRoute, ActivatedRouteSnapshot, RouterState, RouterStateSnapshot} from './src/router_state';
export {PRIMARY_OUTLET, Params} from './src/shared';
export {DefaultUrlSerializer, UrlPathWithParams, UrlSerializer, UrlTree} from './src/url_tree';
export {DefaultUrlSerializer, UrlSegment, UrlSerializer, UrlTree} from './src/url_tree';

View File

@ -19,77 +19,82 @@ import {EmptyError} from 'rxjs/util/EmptyError';
import {Route, Routes} from './config';
import {LoadedRouterConfig, RouterConfigLoader} from './router_config_loader';
import {PRIMARY_OUTLET} from './shared';
import {UrlPathWithParams, UrlSegment, UrlTree} from './url_tree';
import {UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree';
import {merge, waitForMap} from './utils/collection';
class NoMatch {
constructor(public segment: UrlSegment = null) {}
constructor(public segmentGroup: UrlSegmentGroup = null) {}
}
class AbsoluteRedirect {
constructor(public paths: UrlPathWithParams[]) {}
constructor(public segments: UrlSegment[]) {}
}
function noMatch(segment: UrlSegment): Observable<UrlSegment> {
return new Observable<UrlSegment>((obs: Observer<UrlSegment>) => obs.error(new NoMatch(segment)));
function noMatch(segmentGroup: UrlSegmentGroup): Observable<UrlSegmentGroup> {
return new Observable<UrlSegmentGroup>(
(obs: Observer<UrlSegmentGroup>) => obs.error(new NoMatch(segmentGroup)));
}
function absoluteRedirect(newPaths: UrlPathWithParams[]): Observable<UrlSegment> {
return new Observable<UrlSegment>(
(obs: Observer<UrlSegment>) => obs.error(new AbsoluteRedirect(newPaths)));
function absoluteRedirect(segments: UrlSegment[]): Observable<UrlSegmentGroup> {
return new Observable<UrlSegmentGroup>(
(obs: Observer<UrlSegmentGroup>) => obs.error(new AbsoluteRedirect(segments)));
}
export function applyRedirects(
injector: Injector, configLoader: RouterConfigLoader, urlTree: UrlTree,
config: Routes): Observable<UrlTree> {
return expandSegment(injector, configLoader, config, urlTree.root, PRIMARY_OUTLET)
.map(rootSegment => createUrlTree(urlTree, rootSegment))
return expandSegmentGroup(injector, configLoader, config, urlTree.root, PRIMARY_OUTLET)
.map(rootSegmentGroup => createUrlTree(urlTree, rootSegmentGroup))
.catch(e => {
if (e instanceof AbsoluteRedirect) {
return of (createUrlTree(
urlTree, new UrlSegment([], {[PRIMARY_OUTLET]: new UrlSegment(e.paths, {})})));
urlTree,
new UrlSegmentGroup([], {[PRIMARY_OUTLET]: new UrlSegmentGroup(e.segments, {})})));
} else if (e instanceof NoMatch) {
throw new Error(`Cannot match any routes: '${e.segment}'`);
throw new Error(`Cannot match any routes: '${e.segmentGroup}'`);
} else {
throw e;
}
});
}
function createUrlTree(urlTree: UrlTree, rootCandidate: UrlSegment): UrlTree {
const root = rootCandidate.pathsWithParams.length > 0 ?
new UrlSegment([], {[PRIMARY_OUTLET]: rootCandidate}) :
function createUrlTree(urlTree: UrlTree, rootCandidate: UrlSegmentGroup): UrlTree {
const root = rootCandidate.segments.length > 0 ?
new UrlSegmentGroup([], {[PRIMARY_OUTLET]: rootCandidate}) :
rootCandidate;
return new UrlTree(root, urlTree.queryParams, urlTree.fragment);
}
function expandSegment(
injector: Injector, configLoader: RouterConfigLoader, routes: Route[], segment: UrlSegment,
outlet: string): Observable<UrlSegment> {
if (segment.pathsWithParams.length === 0 && segment.hasChildren()) {
return expandSegmentChildren(injector, configLoader, routes, segment)
.map(children => new UrlSegment([], children));
} else {
return expandPathsWithParams(
injector, configLoader, segment, routes, segment.pathsWithParams, outlet, true);
}
}
function expandSegmentChildren(
function expandSegmentGroup(
injector: Injector, configLoader: RouterConfigLoader, routes: Route[],
segment: UrlSegment): Observable<{[name: string]: UrlSegment}> {
return waitForMap(
segment.children,
(childOutlet, child) => expandSegment(injector, configLoader, routes, child, childOutlet));
segmentGroup: UrlSegmentGroup, outlet: string): Observable<UrlSegmentGroup> {
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
return expandChildren(injector, configLoader, routes, segmentGroup)
.map(children => new UrlSegmentGroup([], children));
} else {
return expandSegment(
injector, configLoader, segmentGroup, routes, segmentGroup.segments, outlet, true);
}
}
function expandPathsWithParams(
injector: Injector, configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[],
paths: UrlPathWithParams[], outlet: string, allowRedirects: boolean): Observable<UrlSegment> {
const processRoutes =
of (...routes)
function expandChildren(
injector: Injector, configLoader: RouterConfigLoader, routes: Route[],
segmentGroup: UrlSegmentGroup): Observable<{[name: string]: UrlSegmentGroup}> {
return waitForMap(
segmentGroup.children, (childOutlet, child) => expandSegmentGroup(
injector, configLoader, routes, child, childOutlet));
}
function expandSegment(
injector: Injector, configLoader: RouterConfigLoader, segmentGroup: UrlSegmentGroup,
routes: Route[], segments: UrlSegment[], outlet: string,
allowRedirects: boolean): Observable<UrlSegmentGroup> {
const processRoutes = of (...routes)
.map(r => {
return expandPathsWithParamsAgainstRoute(
injector, configLoader, segment, routes, r, paths, outlet, allowRedirects)
return expandSegmentAgainstRoute(
injector, configLoader, segmentGroup, routes, r, segments,
outlet, allowRedirects)
.catch((e) => {
if (e instanceof NoMatch)
return of (null);
@ -99,96 +104,101 @@ function expandPathsWithParams(
})
.concatAll();
return processRoutes.first(s => !!s).catch((e: any, _: any): Observable<UrlSegment> => {
return processRoutes.first(s => !!s).catch((e: any, _: any): Observable<UrlSegmentGroup> => {
if (e instanceof EmptyError) {
throw new NoMatch(segment);
throw new NoMatch(segmentGroup);
} else {
throw e;
}
});
}
function expandPathsWithParamsAgainstRoute(
injector: Injector, configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[],
route: Route, paths: UrlPathWithParams[], outlet: string,
allowRedirects: boolean): Observable<UrlSegment> {
if (getOutlet(route) !== outlet) return noMatch(segment);
if (route.redirectTo !== undefined && !allowRedirects) return noMatch(segment);
function expandSegmentAgainstRoute(
injector: Injector, configLoader: RouterConfigLoader, segmentGroup: UrlSegmentGroup,
routes: Route[], route: Route, paths: UrlSegment[], outlet: string,
allowRedirects: boolean): Observable<UrlSegmentGroup> {
if (getOutlet(route) !== outlet) return noMatch(segmentGroup);
if (route.redirectTo !== undefined && !allowRedirects) return noMatch(segmentGroup);
if (route.redirectTo !== undefined) {
return expandPathsWithParamsAgainstRouteUsingRedirect(
injector, configLoader, segment, routes, route, paths, outlet);
return expandSegmentAgainstRouteUsingRedirect(
injector, configLoader, segmentGroup, routes, route, paths, outlet);
} else {
return matchPathsWithParamsAgainstRoute(injector, configLoader, segment, route, paths);
return matchSegmentAgainstRoute(injector, configLoader, segmentGroup, route, paths);
}
}
function expandPathsWithParamsAgainstRouteUsingRedirect(
injector: Injector, configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[],
route: Route, paths: UrlPathWithParams[], outlet: string): Observable<UrlSegment> {
function expandSegmentAgainstRouteUsingRedirect(
injector: Injector, configLoader: RouterConfigLoader, segmentGroup: UrlSegmentGroup,
routes: Route[], route: Route, segments: UrlSegment[],
outlet: string): Observable<UrlSegmentGroup> {
if (route.path === '**') {
return expandWildCardWithParamsAgainstRouteUsingRedirect(route);
} else {
return expandRegularPathWithParamsAgainstRouteUsingRedirect(
injector, configLoader, segment, routes, route, paths, outlet);
return expandRegularSegmentAgainstRouteUsingRedirect(
injector, configLoader, segmentGroup, routes, route, segments, outlet);
}
}
function expandWildCardWithParamsAgainstRouteUsingRedirect(route: Route): Observable<UrlSegment> {
const newPaths = applyRedirectCommands([], route.redirectTo, {});
function expandWildCardWithParamsAgainstRouteUsingRedirect(route: Route):
Observable<UrlSegmentGroup> {
const newSegments = applyRedirectCommands([], route.redirectTo, {});
if (route.redirectTo.startsWith('/')) {
return absoluteRedirect(newPaths);
return absoluteRedirect(newSegments);
} else {
return of (new UrlSegment(newPaths, {}));
return of (new UrlSegmentGroup(newSegments, {}));
}
}
function expandRegularPathWithParamsAgainstRouteUsingRedirect(
injector: Injector, configLoader: RouterConfigLoader, segment: UrlSegment, routes: Route[],
route: Route, paths: UrlPathWithParams[], outlet: string): Observable<UrlSegment> {
const {matched, consumedPaths, lastChild, positionalParamSegments} = match(segment, route, paths);
if (!matched) return noMatch(segment);
function expandRegularSegmentAgainstRouteUsingRedirect(
injector: Injector, configLoader: RouterConfigLoader, segmentGroup: UrlSegmentGroup,
routes: Route[], route: Route, segments: UrlSegment[],
outlet: string): Observable<UrlSegmentGroup> {
const {matched, consumedSegments, lastChild, positionalParamSegments} =
match(segmentGroup, route, segments);
if (!matched) return noMatch(segmentGroup);
const newPaths =
applyRedirectCommands(consumedPaths, route.redirectTo, <any>positionalParamSegments);
const newSegments =
applyRedirectCommands(consumedSegments, route.redirectTo, <any>positionalParamSegments);
if (route.redirectTo.startsWith('/')) {
return absoluteRedirect(newPaths);
return absoluteRedirect(newSegments);
} else {
return expandPathsWithParams(
injector, configLoader, segment, routes, newPaths.concat(paths.slice(lastChild)), outlet,
false);
return expandSegment(
injector, configLoader, segmentGroup, routes, newSegments.concat(segments.slice(lastChild)),
outlet, false);
}
}
function matchPathsWithParamsAgainstRoute(
injector: Injector, configLoader: RouterConfigLoader, rawSegment: UrlSegment, route: Route,
paths: UrlPathWithParams[]): Observable<UrlSegment> {
function matchSegmentAgainstRoute(
injector: Injector, configLoader: RouterConfigLoader, rawSegmentGroup: UrlSegmentGroup,
route: Route, segments: UrlSegment[]): Observable<UrlSegmentGroup> {
if (route.path === '**') {
return of (new UrlSegment(paths, {}));
return of (new UrlSegmentGroup(segments, {}));
} else {
const {matched, consumedPaths, lastChild} = match(rawSegment, route, paths);
if (!matched) return noMatch(rawSegment);
const {matched, consumedSegments, lastChild} = match(rawSegmentGroup, route, segments);
if (!matched) return noMatch(rawSegmentGroup);
const rawSlicedPath = paths.slice(lastChild);
const rawSlicedSegments = segments.slice(lastChild);
return getChildConfig(injector, configLoader, route).mergeMap(routerConfig => {
const childInjector = routerConfig.injector;
const childConfig = routerConfig.routes;
const {segment, slicedPath} = split(rawSegment, consumedPaths, rawSlicedPath, childConfig);
const {segmentGroup, slicedSegments} =
split(rawSegmentGroup, consumedSegments, rawSlicedSegments, childConfig);
if (slicedPath.length === 0 && segment.hasChildren()) {
return expandSegmentChildren(childInjector, configLoader, childConfig, segment)
.map(children => new UrlSegment(consumedPaths, children));
if (slicedSegments.length === 0 && segmentGroup.hasChildren()) {
return expandChildren(childInjector, configLoader, childConfig, segmentGroup)
.map(children => new UrlSegmentGroup(consumedSegments, children));
} else if (childConfig.length === 0 && slicedPath.length === 0) {
return of (new UrlSegment(consumedPaths, {}));
} else if (childConfig.length === 0 && slicedSegments.length === 0) {
return of (new UrlSegmentGroup(consumedSegments, {}));
} else {
return expandPathsWithParams(
childInjector, configLoader, segment, childConfig, slicedPath, PRIMARY_OUTLET,
true)
.map(cs => new UrlSegment(consumedPaths.concat(cs.pathsWithParams), cs.children));
return expandSegment(
childInjector, configLoader, segmentGroup, childConfig, slicedSegments,
PRIMARY_OUTLET, true)
.map(cs => new UrlSegmentGroup(consumedSegments.concat(cs.segments), cs.children));
}
});
}
@ -208,33 +218,33 @@ function getChildConfig(injector: Injector, configLoader: RouterConfigLoader, ro
}
}
function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]): {
function match(segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[]): {
matched: boolean,
consumedPaths: UrlPathWithParams[],
consumedSegments: UrlSegment[],
lastChild: number,
positionalParamSegments: {[k: string]: UrlPathWithParams}
positionalParamSegments: {[k: string]: UrlSegment}
} {
const noMatch =
{matched: false, consumedPaths: <any[]>[], lastChild: 0, positionalParamSegments: {}};
{matched: false, consumedSegments: <any[]>[], lastChild: 0, positionalParamSegments: {}};
if (route.path === '') {
if ((route.terminal || route.pathMatch === 'full') &&
(segment.hasChildren() || paths.length > 0)) {
return {matched: false, consumedPaths: [], lastChild: 0, positionalParamSegments: {}};
(segmentGroup.hasChildren() || segments.length > 0)) {
return {matched: false, consumedSegments: [], lastChild: 0, positionalParamSegments: {}};
} else {
return {matched: true, consumedPaths: [], lastChild: 0, positionalParamSegments: {}};
return {matched: true, consumedSegments: [], lastChild: 0, positionalParamSegments: {}};
}
}
const path = route.path;
const parts = path.split('/');
const positionalParamSegments: {[k: string]: UrlPathWithParams} = {};
const consumedPaths: UrlPathWithParams[] = [];
const positionalParamSegments: {[k: string]: UrlSegment} = {};
const consumedSegments: UrlSegment[] = [];
let currentIndex = 0;
for (let i = 0; i < parts.length; ++i) {
if (currentIndex >= paths.length) return noMatch;
const current = paths[currentIndex];
if (currentIndex >= segments.length) return noMatch;
const current = segments[currentIndex];
const p = parts[i];
const isPosParam = p.startsWith(':');
@ -243,128 +253,131 @@ function match(segment: UrlSegment, route: Route, paths: UrlPathWithParams[]): {
if (isPosParam) {
positionalParamSegments[p.substring(1)] = current;
}
consumedPaths.push(current);
consumedSegments.push(current);
currentIndex++;
}
if (route.terminal && (segment.hasChildren() || currentIndex < paths.length)) {
return {matched: false, consumedPaths: [], lastChild: 0, positionalParamSegments: {}};
if (route.terminal && (segmentGroup.hasChildren() || currentIndex < segments.length)) {
return {matched: false, consumedSegments: [], lastChild: 0, positionalParamSegments: {}};
}
return {matched: true, consumedPaths, lastChild: currentIndex, positionalParamSegments};
return {matched: true, consumedSegments, lastChild: currentIndex, positionalParamSegments};
}
function applyRedirectCommands(
paths: UrlPathWithParams[], redirectTo: string,
posParams: {[k: string]: UrlPathWithParams}): UrlPathWithParams[] {
segments: UrlSegment[], redirectTo: string,
posParams: {[k: string]: UrlSegment}): UrlSegment[] {
const r = redirectTo.startsWith('/') ? redirectTo.substring(1) : redirectTo;
if (r === '') {
return [];
} else {
return createPaths(redirectTo, r.split('/'), paths, posParams);
return createSegments(redirectTo, r.split('/'), segments, posParams);
}
}
function createPaths(
redirectTo: string, parts: string[], segments: UrlPathWithParams[],
posParams: {[k: string]: UrlPathWithParams}): UrlPathWithParams[] {
function createSegments(
redirectTo: string, parts: string[], segments: UrlSegment[],
posParams: {[k: string]: UrlSegment}): UrlSegment[] {
return parts.map(
p => p.startsWith(':') ? findPosParam(p, posParams, redirectTo) :
findOrCreatePath(p, segments));
findOrCreateSegment(p, segments));
}
function findPosParam(
part: string, posParams: {[k: string]: UrlPathWithParams},
redirectTo: string): UrlPathWithParams {
part: string, posParams: {[k: string]: UrlSegment}, redirectTo: string): UrlSegment {
const paramName = part.substring(1);
const pos = posParams[paramName];
if (!pos) throw new Error(`Cannot redirect to '${redirectTo}'. Cannot find '${part}'.`);
return pos;
}
function findOrCreatePath(part: string, paths: UrlPathWithParams[]): UrlPathWithParams {
function findOrCreateSegment(part: string, segments: UrlSegment[]): UrlSegment {
let idx = 0;
for (const s of paths) {
for (const s of segments) {
if (s.path === part) {
paths.splice(idx);
segments.splice(idx);
return s;
}
idx++;
}
return new UrlPathWithParams(part, {});
return new UrlSegment(part, {});
}
function split(
segment: UrlSegment, consumedPaths: UrlPathWithParams[], slicedPath: UrlPathWithParams[],
segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[],
config: Route[]) {
if (slicedPath.length > 0 &&
containsEmptyPathRedirectsWithNamedOutlets(segment, slicedPath, config)) {
const s = new UrlSegment(
consumedPaths,
createChildrenForEmptyPaths(config, new UrlSegment(slicedPath, segment.children)));
return {segment: mergeTrivialChildren(s), slicedPath: []};
if (slicedSegments.length > 0 &&
containsEmptyPathRedirectsWithNamedOutlets(segmentGroup, slicedSegments, config)) {
const s = new UrlSegmentGroup(
consumedSegments, createChildrenForEmptySegments(
config, new UrlSegmentGroup(slicedSegments, segmentGroup.children)));
return {segmentGroup: mergeTrivialChildren(s), slicedSegments: []};
} else if (slicedPath.length === 0 && containsEmptyPathRedirects(segment, slicedPath, config)) {
const s = new UrlSegment(
segment.pathsWithParams,
addEmptyPathsToChildrenIfNeeded(segment, slicedPath, config, segment.children));
return {segment: mergeTrivialChildren(s), slicedPath};
} else if (
slicedSegments.length === 0 &&
containsEmptyPathRedirects(segmentGroup, slicedSegments, config)) {
const s = new UrlSegmentGroup(
segmentGroup.segments, addEmptySegmentsToChildrenIfNeeded(
segmentGroup, slicedSegments, config, segmentGroup.children));
return {segmentGroup: mergeTrivialChildren(s), slicedSegments};
} else {
return {segment, slicedPath};
return {segmentGroup, slicedSegments};
}
}
function mergeTrivialChildren(s: UrlSegment): UrlSegment {
function mergeTrivialChildren(s: UrlSegmentGroup): UrlSegmentGroup {
if (s.numberOfChildren === 1 && s.children[PRIMARY_OUTLET]) {
const c = s.children[PRIMARY_OUTLET];
return new UrlSegment(s.pathsWithParams.concat(c.pathsWithParams), c.children);
return new UrlSegmentGroup(s.segments.concat(c.segments), c.children);
} else {
return s;
}
}
function addEmptyPathsToChildrenIfNeeded(
segment: UrlSegment, slicedPath: UrlPathWithParams[], routes: Route[],
children: {[name: string]: UrlSegment}): {[name: string]: UrlSegment} {
const res: {[name: string]: UrlSegment} = {};
function addEmptySegmentsToChildrenIfNeeded(
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[],
children: {[name: string]: UrlSegmentGroup}): {[name: string]: UrlSegmentGroup} {
const res: {[name: string]: UrlSegmentGroup} = {};
for (let r of routes) {
if (emptyPathRedirect(segment, slicedPath, r) && !children[getOutlet(r)]) {
res[getOutlet(r)] = new UrlSegment([], {});
if (emptyPathRedirect(segmentGroup, slicedSegments, r) && !children[getOutlet(r)]) {
res[getOutlet(r)] = new UrlSegmentGroup([], {});
}
}
return merge(children, res);
}
function createChildrenForEmptyPaths(
routes: Route[], primarySegment: UrlSegment): {[name: string]: UrlSegment} {
const res: {[name: string]: UrlSegment} = {};
res[PRIMARY_OUTLET] = primarySegment;
function createChildrenForEmptySegments(
routes: Route[], primarySegmentGroup: UrlSegmentGroup): {[name: string]: UrlSegmentGroup} {
const res: {[name: string]: UrlSegmentGroup} = {};
res[PRIMARY_OUTLET] = primarySegmentGroup;
for (let r of routes) {
if (r.path === '' && getOutlet(r) !== PRIMARY_OUTLET) {
res[getOutlet(r)] = new UrlSegment([], {});
res[getOutlet(r)] = new UrlSegmentGroup([], {});
}
}
return res;
}
function containsEmptyPathRedirectsWithNamedOutlets(
segment: UrlSegment, slicedPath: UrlPathWithParams[], routes: Route[]): boolean {
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean {
return routes
.filter(
r => emptyPathRedirect(segment, slicedPath, r) && getOutlet(r) !== PRIMARY_OUTLET)
r => emptyPathRedirect(segmentGroup, slicedSegments, r) &&
getOutlet(r) !== PRIMARY_OUTLET)
.length > 0;
}
function containsEmptyPathRedirects(
segment: UrlSegment, slicedPath: UrlPathWithParams[], routes: Route[]): boolean {
return routes.filter(r => emptyPathRedirect(segment, slicedPath, r)).length > 0;
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean {
return routes.filter(r => emptyPathRedirect(segmentGroup, slicedSegments, r)).length > 0;
}
function emptyPathRedirect(
segment: UrlSegment, slicedPath: UrlPathWithParams[], r: Route): boolean {
if ((segment.hasChildren() || slicedPath.length > 0) && (r.terminal || r.pathMatch === 'full'))
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], r: Route): boolean {
if ((segmentGroup.hasChildren() || slicedSegments.length > 0) &&
(r.terminal || r.pathMatch === 'full'))
return false;
return r.path === '' && r.redirectTo !== undefined;
}

View File

@ -8,7 +8,7 @@
import {ActivatedRoute} from './router_state';
import {PRIMARY_OUTLET, Params} from './shared';
import {UrlPathWithParams, UrlSegment, UrlTree} from './url_tree';
import {UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree';
import {forEach, shallowEqual} from './utils/collection';
export function createUrlTree(
@ -22,15 +22,16 @@ export function createUrlTree(
validateCommands(normalizedCommands);
if (navigateToRoot(normalizedCommands)) {
return tree(urlTree.root, new UrlSegment([], {}), urlTree, queryParams, fragment);
return tree(urlTree.root, new UrlSegmentGroup([], {}), urlTree, queryParams, fragment);
}
const startingPosition = findStartingPosition(normalizedCommands, urlTree, route);
const segment = startingPosition.processChildren ?
updateSegmentChildren(
startingPosition.segment, startingPosition.index, normalizedCommands.commands) :
updateSegment(startingPosition.segment, startingPosition.index, normalizedCommands.commands);
return tree(startingPosition.segment, segment, urlTree, queryParams, fragment);
const segmentGroup = startingPosition.processChildren ?
updateSegmentGroupChildren(
startingPosition.segmentGroup, startingPosition.index, normalizedCommands.commands) :
updateSegmentGroup(
startingPosition.segmentGroup, startingPosition.index, normalizedCommands.commands);
return tree(startingPosition.segmentGroup, segmentGroup, urlTree, queryParams, fragment);
}
function validateCommands(n: NormalizedNavigationCommands): void {
@ -40,27 +41,29 @@ function validateCommands(n: NormalizedNavigationCommands): void {
}
function tree(
oldSegment: UrlSegment, newSegment: UrlSegment, urlTree: UrlTree, queryParams: Params,
fragment: string): UrlTree {
if (urlTree.root === oldSegment) {
return new UrlTree(newSegment, stringify(queryParams), fragment);
oldSegmentGroup: UrlSegmentGroup, newSegmentGroup: UrlSegmentGroup, urlTree: UrlTree,
queryParams: Params, fragment: string): UrlTree {
if (urlTree.root === oldSegmentGroup) {
return new UrlTree(newSegmentGroup, stringify(queryParams), fragment);
} else {
return new UrlTree(
replaceSegment(urlTree.root, oldSegment, newSegment), stringify(queryParams), fragment);
replaceSegment(urlTree.root, oldSegmentGroup, newSegmentGroup), stringify(queryParams),
fragment);
}
}
function replaceSegment(
current: UrlSegment, oldSegment: UrlSegment, newSegment: UrlSegment): UrlSegment {
const children: {[key: string]: UrlSegment} = {};
forEach(current.children, (c: UrlSegment, outletName: string) => {
current: UrlSegmentGroup, oldSegment: UrlSegmentGroup,
newSegment: UrlSegmentGroup): UrlSegmentGroup {
const children: {[key: string]: UrlSegmentGroup} = {};
forEach(current.children, (c: UrlSegmentGroup, outletName: string) => {
if (c === oldSegment) {
children[outletName] = newSegment;
} else {
children[outletName] = replaceSegment(c, oldSegment, newSegment);
}
});
return new UrlSegment(current.pathsWithParams, children);
return new UrlSegmentGroup(current.segments, children);
}
function navigateToRoot(normalizedChange: NormalizedNavigationCommands): boolean {
@ -131,7 +134,9 @@ function normalizeCommands(commands: any[]): NormalizedNavigationCommands {
}
class Position {
constructor(public segment: UrlSegment, public processChildren: boolean, public index: number) {}
constructor(
public segmentGroup: UrlSegmentGroup, public processChildren: boolean, public index: number) {
}
}
function findStartingPosition(
@ -160,58 +165,59 @@ function getOutlets(commands: any[]): {[k: string]: any[]} {
return commands[0].outlets;
}
function updateSegment(segment: UrlSegment, startIndex: number, commands: any[]): UrlSegment {
if (!segment) {
segment = new UrlSegment([], {});
function updateSegmentGroup(
segmentGroup: UrlSegmentGroup, startIndex: number, commands: any[]): UrlSegmentGroup {
if (!segmentGroup) {
segmentGroup = new UrlSegmentGroup([], {});
}
if (segment.pathsWithParams.length === 0 && segment.hasChildren()) {
return updateSegmentChildren(segment, startIndex, commands);
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
return updateSegmentGroupChildren(segmentGroup, startIndex, commands);
}
const m = prefixedWith(segment, startIndex, commands);
const m = prefixedWith(segmentGroup, startIndex, commands);
const slicedCommands = commands.slice(m.lastIndex);
if (m.match && slicedCommands.length === 0) {
return new UrlSegment(segment.pathsWithParams, {});
} else if (m.match && !segment.hasChildren()) {
return createNewSegment(segment, startIndex, commands);
return new UrlSegmentGroup(segmentGroup.segments, {});
} else if (m.match && !segmentGroup.hasChildren()) {
return createNewSegmentGroup(segmentGroup, startIndex, commands);
} else if (m.match) {
return updateSegmentChildren(segment, 0, slicedCommands);
return updateSegmentGroupChildren(segmentGroup, 0, slicedCommands);
} else {
return createNewSegment(segment, startIndex, commands);
return createNewSegmentGroup(segmentGroup, startIndex, commands);
}
}
function updateSegmentChildren(
segment: UrlSegment, startIndex: number, commands: any[]): UrlSegment {
function updateSegmentGroupChildren(
segmentGroup: UrlSegmentGroup, startIndex: number, commands: any[]): UrlSegmentGroup {
if (commands.length === 0) {
return new UrlSegment(segment.pathsWithParams, {});
return new UrlSegmentGroup(segmentGroup.segments, {});
} else {
const outlets = getOutlets(commands);
const children: {[key: string]: UrlSegment} = {};
const children: {[key: string]: UrlSegmentGroup} = {};
forEach(outlets, (commands: any, outlet: string) => {
if (commands !== null) {
children[outlet] = updateSegment(segment.children[outlet], startIndex, commands);
children[outlet] = updateSegmentGroup(segmentGroup.children[outlet], startIndex, commands);
}
});
forEach(segment.children, (child: UrlSegment, childOutlet: string) => {
forEach(segmentGroup.children, (child: UrlSegmentGroup, childOutlet: string) => {
if (outlets[childOutlet] === undefined) {
children[childOutlet] = child;
}
});
return new UrlSegment(segment.pathsWithParams, children);
return new UrlSegmentGroup(segmentGroup.segments, children);
}
}
function prefixedWith(segment: UrlSegment, startIndex: number, commands: any[]) {
function prefixedWith(segmentGroup: UrlSegmentGroup, startIndex: number, commands: any[]) {
let currentCommandIndex = 0;
let currentPathIndex = startIndex;
const noMatch = {match: false, lastIndex: 0};
while (currentPathIndex < segment.pathsWithParams.length) {
while (currentPathIndex < segmentGroup.segments.length) {
if (currentCommandIndex >= commands.length) return noMatch;
const path = segment.pathsWithParams[currentPathIndex];
const path = segmentGroup.segments[currentPathIndex];
const curr = getPath(commands[currentCommandIndex]);
const next =
currentCommandIndex < commands.length - 1 ? commands[currentCommandIndex + 1] : null;
@ -229,14 +235,15 @@ function prefixedWith(segment: UrlSegment, startIndex: number, commands: any[])
return {match: true, lastIndex: currentCommandIndex};
}
function createNewSegment(segment: UrlSegment, startIndex: number, commands: any[]): UrlSegment {
const paths = segment.pathsWithParams.slice(0, startIndex);
function createNewSegmentGroup(
segmentGroup: UrlSegmentGroup, startIndex: number, commands: any[]): UrlSegmentGroup {
const paths = segmentGroup.segments.slice(0, startIndex);
let i = 0;
while (i < commands.length) {
// if we start with an object literal, we need to reuse the path part from the segment
if (i === 0 && (typeof commands[0] === 'object')) {
const p = segment.pathsWithParams[startIndex];
paths.push(new UrlPathWithParams(p.path, commands[0]));
const p = segmentGroup.segments[startIndex];
paths.push(new UrlSegment(p.path, commands[0]));
i++;
continue;
}
@ -244,14 +251,14 @@ function createNewSegment(segment: UrlSegment, startIndex: number, commands: any
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)));
paths.push(new UrlSegment(curr, stringify(next)));
i += 2;
} else {
paths.push(new UrlPathWithParams(curr, {}));
paths.push(new UrlSegment(curr, {}));
i++;
}
}
return new UrlSegment(paths, {});
return new UrlSegmentGroup(paths, {});
}
function stringify(params: {[key: string]: any}): {[key: string]: string} {
@ -260,7 +267,6 @@ function stringify(params: {[key: string]: any}): {[key: string]: string} {
return res;
}
function compare(
path: string, params: {[key: string]: any}, pathWithParams: UrlPathWithParams): boolean {
return path == pathWithParams.path && shallowEqual(params, pathWithParams.parameters);
function compare(path: string, params: {[key: string]: any}, segment: UrlSegment): boolean {
return path == segment.path && shallowEqual(params, segment.parameters);
}

View File

@ -14,12 +14,12 @@ import {of } from 'rxjs/observable/of';
import {Data, ResolveData, Route, Routes} from './config';
import {ActivatedRouteSnapshot, InheritedResolve, RouterStateSnapshot} from './router_state';
import {PRIMARY_OUTLET, Params} from './shared';
import {UrlPathWithParams, UrlSegment, UrlTree, mapChildrenIntoArray} from './url_tree';
import {UrlSegment, UrlSegmentGroup, UrlTree, mapChildrenIntoArray} from './url_tree';
import {last, merge} from './utils/collection';
import {TreeNode} from './utils/tree';
class NoMatch {
constructor(public segment: UrlSegment = null) {}
constructor(public segmentGroup: UrlSegmentGroup = null) {}
}
class InheritedFromParent {
@ -41,9 +41,9 @@ class InheritedFromParent {
export function recognize(rootComponentType: Type, config: Routes, urlTree: UrlTree, url: string):
Observable<RouterStateSnapshot> {
try {
const rootSegment = split(urlTree.root, [], [], config).segment;
const children =
processSegment(config, rootSegment, InheritedFromParent.empty(null), PRIMARY_OUTLET);
const rootSegmentGroup = split(urlTree.root, [], [], config).segmentGroup;
const children = processSegmentGroup(
config, rootSegmentGroup, InheritedFromParent.empty(null), PRIMARY_OUTLET);
const root = new ActivatedRouteSnapshot(
[], Object.freeze({}), {}, PRIMARY_OUTLET, rootComponentType, null, urlTree.root, -1,
InheritedResolve.empty);
@ -54,7 +54,7 @@ export function recognize(rootComponentType: Type, config: Routes, urlTree: UrlT
if (e instanceof NoMatch) {
return new Observable<RouterStateSnapshot>(
(obs: Observer<RouterStateSnapshot>) =>
obs.error(new Error(`Cannot match any routes: '${e.segment}'`)));
obs.error(new Error(`Cannot match any routes: '${e.segmentGroup}'`)));
} else {
return new Observable<RouterStateSnapshot>(
(obs: Observer<RouterStateSnapshot>) => obs.error(e));
@ -62,21 +62,22 @@ export function recognize(rootComponentType: Type, config: Routes, urlTree: UrlT
}
}
function processSegment(
config: Route[], segment: UrlSegment, inherited: InheritedFromParent,
function processSegmentGroup(
config: Route[], segmentGroup: UrlSegmentGroup, inherited: InheritedFromParent,
outlet: string): TreeNode<ActivatedRouteSnapshot>[] {
if (segment.pathsWithParams.length === 0 && segment.hasChildren()) {
return processSegmentChildren(config, segment, inherited);
if (segmentGroup.segments.length === 0 && segmentGroup.hasChildren()) {
return processChildren(config, segmentGroup, inherited);
} else {
return processPathsWithParams(config, segment, 0, segment.pathsWithParams, inherited, outlet);
return processSegment(config, segmentGroup, 0, segmentGroup.segments, inherited, outlet);
}
}
function processSegmentChildren(
config: Route[], segment: UrlSegment,
function processChildren(
config: Route[], segmentGroup: UrlSegmentGroup,
inherited: InheritedFromParent): TreeNode<ActivatedRouteSnapshot>[] {
const children = mapChildrenIntoArray(
segment, (child, childOutlet) => processSegment(config, child, inherited, childOutlet));
segmentGroup,
(child, childOutlet) => processSegmentGroup(config, child, inherited, childOutlet));
checkOutletNameUniqueness(children);
sortActivatedRouteSnapshots(children);
return children;
@ -90,21 +91,21 @@ function sortActivatedRouteSnapshots(nodes: TreeNode<ActivatedRouteSnapshot>[]):
});
}
function processPathsWithParams(
config: Route[], segment: UrlSegment, pathIndex: number, paths: UrlPathWithParams[],
function processSegment(
config: Route[], segmentGroup: UrlSegmentGroup, pathIndex: number, segments: UrlSegment[],
inherited: InheritedFromParent, outlet: string): TreeNode<ActivatedRouteSnapshot>[] {
for (let r of config) {
try {
return processPathsWithParamsAgainstRoute(r, segment, pathIndex, paths, inherited, outlet);
return processSegmentAgainstRoute(r, segmentGroup, pathIndex, segments, inherited, outlet);
} catch (e) {
if (!(e instanceof NoMatch)) throw e;
}
}
throw new NoMatch(segment);
throw new NoMatch(segmentGroup);
}
function processPathsWithParamsAgainstRoute(
route: Route, rawSegment: UrlSegment, pathIndex: number, paths: UrlPathWithParams[],
function processSegmentAgainstRoute(
route: Route, rawSegment: UrlSegmentGroup, pathIndex: number, segments: UrlSegment[],
inherited: InheritedFromParent, outlet: string): TreeNode<ActivatedRouteSnapshot>[] {
if (route.redirectTo) throw new NoMatch();
@ -113,42 +114,44 @@ function processPathsWithParamsAgainstRoute(
const newInheritedResolve = new InheritedResolve(inherited.resolve, getResolve(route));
if (route.path === '**') {
const params = paths.length > 0 ? last(paths).parameters : {};
const params = segments.length > 0 ? last(segments).parameters : {};
const snapshot = new ActivatedRouteSnapshot(
paths, Object.freeze(merge(inherited.allParams, params)),
segments, Object.freeze(merge(inherited.allParams, params)),
merge(inherited.allData, getData(route)), outlet, route.component, route,
getSourceSegment(rawSegment), getPathIndexShift(rawSegment) + paths.length,
getSourceSegmentGroup(rawSegment), getPathIndexShift(rawSegment) + segments.length,
newInheritedResolve);
return [new TreeNode<ActivatedRouteSnapshot>(snapshot, [])];
}
const {consumedPaths, parameters, lastChild} =
match(rawSegment, route, paths, inherited.snapshot);
const rawSlicedPath = paths.slice(lastChild);
const {consumedSegments, parameters, lastChild} =
match(rawSegment, route, segments, inherited.snapshot);
const rawSlicedSegments = segments.slice(lastChild);
const childConfig = getChildConfig(route);
const {segment, slicedPath} = split(rawSegment, consumedPaths, rawSlicedPath, childConfig);
const {segmentGroup, slicedSegments} =
split(rawSegment, consumedSegments, rawSlicedSegments, childConfig);
const snapshot = new ActivatedRouteSnapshot(
consumedPaths, Object.freeze(merge(inherited.allParams, parameters)),
consumedSegments, Object.freeze(merge(inherited.allParams, parameters)),
merge(inherited.allData, getData(route)), outlet, route.component, route,
getSourceSegment(rawSegment), getPathIndexShift(rawSegment) + consumedPaths.length,
getSourceSegmentGroup(rawSegment), getPathIndexShift(rawSegment) + consumedSegments.length,
newInheritedResolve);
const newInherited = route.component ?
InheritedFromParent.empty(snapshot) :
new InheritedFromParent(inherited, snapshot, parameters, getData(route), newInheritedResolve);
if (slicedPath.length === 0 && segment.hasChildren()) {
const children = processSegmentChildren(childConfig, segment, newInherited);
if (slicedSegments.length === 0 && segmentGroup.hasChildren()) {
const children = processChildren(childConfig, segmentGroup, newInherited);
return [new TreeNode<ActivatedRouteSnapshot>(snapshot, children)];
} else if (childConfig.length === 0 && slicedPath.length === 0) {
} else if (childConfig.length === 0 && slicedSegments.length === 0) {
return [new TreeNode<ActivatedRouteSnapshot>(snapshot, [])];
} else {
const children = processPathsWithParams(
childConfig, segment, pathIndex + lastChild, slicedPath, newInherited, PRIMARY_OUTLET);
const children = processSegment(
childConfig, segmentGroup, pathIndex + lastChild, slicedSegments, newInherited,
PRIMARY_OUTLET);
return [new TreeNode<ActivatedRouteSnapshot>(snapshot, children)];
}
}
@ -164,27 +167,28 @@ function getChildConfig(route: Route): Route[] {
}
function match(
segment: UrlSegment, route: Route, paths: UrlPathWithParams[], parent: ActivatedRouteSnapshot) {
segmentGroup: UrlSegmentGroup, route: Route, segments: UrlSegment[],
parent: ActivatedRouteSnapshot) {
if (route.path === '') {
if ((route.terminal || route.pathMatch === 'full') &&
(segment.hasChildren() || paths.length > 0)) {
(segmentGroup.hasChildren() || segments.length > 0)) {
throw new NoMatch();
} else {
const params = parent ? parent.params : {};
return {consumedPaths: [], lastChild: 0, parameters: params};
return {consumedSegments: [], lastChild: 0, parameters: params};
}
}
const path = route.path;
const parts = path.split('/');
const posParameters: {[key: string]: any} = {};
const consumedPaths: UrlPathWithParams[] = [];
const consumedSegments: UrlSegment[] = [];
let currentIndex = 0;
for (let i = 0; i < parts.length; ++i) {
if (currentIndex >= paths.length) throw new NoMatch();
const current = paths[currentIndex];
if (currentIndex >= segments.length) throw new NoMatch();
const current = segments[currentIndex];
const p = parts[i];
const isPosParam = p.startsWith(':');
@ -193,17 +197,17 @@ function match(
if (isPosParam) {
posParameters[p.substring(1)] = current.path;
}
consumedPaths.push(current);
consumedSegments.push(current);
currentIndex++;
}
if ((route.terminal || route.pathMatch === 'full') &&
(segment.hasChildren() || currentIndex < paths.length)) {
(segmentGroup.hasChildren() || currentIndex < segments.length)) {
throw new NoMatch();
}
const parameters = merge(posParameters, consumedPaths[consumedPaths.length - 1].parameters);
return {consumedPaths, lastChild: currentIndex, parameters};
const parameters = merge(posParameters, consumedSegments[consumedSegments.length - 1].parameters);
return {consumedSegments, lastChild: currentIndex, parameters};
}
function checkOutletNameUniqueness(nodes: TreeNode<ActivatedRouteSnapshot>[]): void {
@ -219,62 +223,64 @@ function checkOutletNameUniqueness(nodes: TreeNode<ActivatedRouteSnapshot>[]): v
});
}
function getSourceSegment(segment: UrlSegment): UrlSegment {
let s = segment;
function getSourceSegmentGroup(segmentGroup: UrlSegmentGroup): UrlSegmentGroup {
let s = segmentGroup;
while (s._sourceSegment) {
s = s._sourceSegment;
}
return s;
}
function getPathIndexShift(segment: UrlSegment): number {
let s = segment;
let res = (s._pathIndexShift ? s._pathIndexShift : 0);
function getPathIndexShift(segmentGroup: UrlSegmentGroup): number {
let s = segmentGroup;
let res = (s._segmentIndexShift ? s._segmentIndexShift : 0);
while (s._sourceSegment) {
s = s._sourceSegment;
res += (s._pathIndexShift ? s._pathIndexShift : 0);
res += (s._segmentIndexShift ? s._segmentIndexShift : 0);
}
return res - 1;
}
function split(
segment: UrlSegment, consumedPaths: UrlPathWithParams[], slicedPath: UrlPathWithParams[],
segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], slicedSegments: UrlSegment[],
config: Route[]) {
if (slicedPath.length > 0 &&
containsEmptyPathMatchesWithNamedOutlets(segment, slicedPath, config)) {
const s = new UrlSegment(
consumedPaths,
createChildrenForEmptyPaths(
segment, consumedPaths, config, new UrlSegment(slicedPath, segment.children)));
s._sourceSegment = segment;
s._pathIndexShift = consumedPaths.length;
return {segment: s, slicedPath: []};
if (slicedSegments.length > 0 &&
containsEmptyPathMatchesWithNamedOutlets(segmentGroup, slicedSegments, config)) {
const s = new UrlSegmentGroup(
consumedSegments, createChildrenForEmptyPaths(
segmentGroup, consumedSegments, config,
new UrlSegmentGroup(slicedSegments, segmentGroup.children)));
s._sourceSegment = segmentGroup;
s._segmentIndexShift = consumedSegments.length;
return {segmentGroup: s, slicedSegments: []};
} else if (slicedPath.length === 0 && containsEmptyPathMatches(segment, slicedPath, config)) {
const s = new UrlSegment(
segment.pathsWithParams,
addEmptyPathsToChildrenIfNeeded(segment, slicedPath, config, segment.children));
s._sourceSegment = segment;
s._pathIndexShift = consumedPaths.length;
return {segment: s, slicedPath};
} else if (
slicedSegments.length === 0 &&
containsEmptyPathMatches(segmentGroup, slicedSegments, config)) {
const s = new UrlSegmentGroup(
segmentGroup.segments, addEmptyPathsToChildrenIfNeeded(
segmentGroup, slicedSegments, config, segmentGroup.children));
s._sourceSegment = segmentGroup;
s._segmentIndexShift = consumedSegments.length;
return {segmentGroup: s, slicedSegments};
} else {
const s = new UrlSegment(segment.pathsWithParams, segment.children);
s._sourceSegment = segment;
s._pathIndexShift = consumedPaths.length;
return {segment: s, slicedPath};
const s = new UrlSegmentGroup(segmentGroup.segments, segmentGroup.children);
s._sourceSegment = segmentGroup;
s._segmentIndexShift = consumedSegments.length;
return {segmentGroup: s, slicedSegments};
}
}
function addEmptyPathsToChildrenIfNeeded(
segment: UrlSegment, slicedPath: UrlPathWithParams[], routes: Route[],
children: {[name: string]: UrlSegment}): {[name: string]: UrlSegment} {
const res: {[name: string]: UrlSegment} = {};
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[],
children: {[name: string]: UrlSegmentGroup}): {[name: string]: UrlSegmentGroup} {
const res: {[name: string]: UrlSegmentGroup} = {};
for (let r of routes) {
if (emptyPathMatch(segment, slicedPath, r) && !children[getOutlet(r)]) {
const s = new UrlSegment([], {});
s._sourceSegment = segment;
s._pathIndexShift = segment.pathsWithParams.length;
if (emptyPathMatch(segmentGroup, slicedSegments, r) && !children[getOutlet(r)]) {
const s = new UrlSegmentGroup([], {});
s._sourceSegment = segmentGroup;
s._segmentIndexShift = segmentGroup.segments.length;
res[getOutlet(r)] = s;
}
}
@ -282,18 +288,18 @@ function addEmptyPathsToChildrenIfNeeded(
}
function createChildrenForEmptyPaths(
segment: UrlSegment, consumedPaths: UrlPathWithParams[], routes: Route[],
primarySegment: UrlSegment): {[name: string]: UrlSegment} {
const res: {[name: string]: UrlSegment} = {};
segmentGroup: UrlSegmentGroup, consumedSegments: UrlSegment[], routes: Route[],
primarySegment: UrlSegmentGroup): {[name: string]: UrlSegmentGroup} {
const res: {[name: string]: UrlSegmentGroup} = {};
res[PRIMARY_OUTLET] = primarySegment;
primarySegment._sourceSegment = segment;
primarySegment._pathIndexShift = consumedPaths.length;
primarySegment._sourceSegment = segmentGroup;
primarySegment._segmentIndexShift = consumedSegments.length;
for (let r of routes) {
if (r.path === '' && getOutlet(r) !== PRIMARY_OUTLET) {
const s = new UrlSegment([], {});
s._sourceSegment = segment;
s._pathIndexShift = consumedPaths.length;
const s = new UrlSegmentGroup([], {});
s._sourceSegment = segmentGroup;
s._segmentIndexShift = consumedSegments.length;
res[getOutlet(r)] = s;
}
}
@ -301,19 +307,23 @@ function createChildrenForEmptyPaths(
}
function containsEmptyPathMatchesWithNamedOutlets(
segment: UrlSegment, slicedPath: UrlPathWithParams[], routes: Route[]): boolean {
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean {
return routes
.filter(r => emptyPathMatch(segment, slicedPath, r) && getOutlet(r) !== PRIMARY_OUTLET)
.filter(
r => emptyPathMatch(segmentGroup, slicedSegments, r) &&
getOutlet(r) !== PRIMARY_OUTLET)
.length > 0;
}
function containsEmptyPathMatches(
segment: UrlSegment, slicedPath: UrlPathWithParams[], routes: Route[]): boolean {
return routes.filter(r => emptyPathMatch(segment, slicedPath, r)).length > 0;
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], routes: Route[]): boolean {
return routes.filter(r => emptyPathMatch(segmentGroup, slicedSegments, r)).length > 0;
}
function emptyPathMatch(segment: UrlSegment, slicedPath: UrlPathWithParams[], r: Route): boolean {
if ((segment.hasChildren() || slicedPath.length > 0) && (r.terminal || r.pathMatch === 'full'))
function emptyPathMatch(
segmentGroup: UrlSegmentGroup, slicedSegments: UrlSegment[], r: Route): boolean {
if ((segmentGroup.hasChildren() || slicedSegments.length > 0) &&
(r.terminal || r.pathMatch === 'full'))
return false;
return r.path === '' && r.redirectTo === undefined;
}

View File

@ -12,7 +12,7 @@ import {Observable} from 'rxjs/Observable';
import {Data, ResolveData, Route} from './config';
import {PRIMARY_OUTLET, Params} from './shared';
import {UrlPathWithParams, UrlSegment, UrlTree} from './url_tree';
import {UrlSegment, UrlSegmentGroup, UrlTree} from './url_tree';
import {merge, shallowEqual, shallowEqualArrays} from './utils/collection';
import {Tree, TreeNode} from './utils/tree';
@ -49,7 +49,7 @@ export class RouterState extends Tree<ActivatedRoute> {
export function createEmptyState(urlTree: UrlTree, rootComponent: Type): RouterState {
const snapshot = createEmptyStateSnapshot(urlTree, rootComponent);
const emptyUrl = new BehaviorSubject([new UrlPathWithParams('', {})]);
const emptyUrl = new BehaviorSubject([new UrlSegment('', {})]);
const emptyParams = new BehaviorSubject({});
const emptyData = new BehaviorSubject({});
const emptyQueryParams = new BehaviorSubject({});
@ -99,7 +99,7 @@ export class ActivatedRoute {
* @internal
*/
constructor(
public url: Observable<UrlPathWithParams[]>, public params: Observable<Params>,
public url: Observable<UrlSegment[]>, public params: Observable<Params>,
public data: Observable<Data>, public outlet: string, public component: Type|string,
futureSnapshot: ActivatedRouteSnapshot) {
this._futureSnapshot = futureSnapshot;
@ -158,7 +158,7 @@ export class ActivatedRouteSnapshot {
_routeConfig: Route;
/** @internal **/
_urlSegment: UrlSegment;
_urlSegment: UrlSegmentGroup;
/** @internal */
_lastPathIndex: number;
@ -170,9 +170,9 @@ export class ActivatedRouteSnapshot {
* @internal
*/
constructor(
public url: UrlPathWithParams[], public params: Params, public data: Data,
public outlet: string, public component: Type|string, routeConfig: Route,
urlSegment: UrlSegment, lastPathIndex: number, resolve: InheritedResolve) {
public url: UrlSegment[], public params: Params, public data: Data, public outlet: string,
public component: Type|string, routeConfig: Route, urlSegment: UrlSegmentGroup,
lastPathIndex: number, resolve: InheritedResolve) {
this._routeConfig = routeConfig;
this._urlSegment = urlSegment;
this._lastPathIndex = lastPathIndex;

View File

@ -10,53 +10,53 @@ import {PRIMARY_OUTLET} from './shared';
import {forEach, shallowEqual} from './utils/collection';
export function createEmptyUrlTree() {
return new UrlTree(new UrlSegment([], {}), {}, null);
return new UrlTree(new UrlSegmentGroup([], {}), {}, null);
}
export function containsTree(container: UrlTree, containee: UrlTree, exact: boolean): boolean {
if (exact) {
return equalSegments(container.root, containee.root);
return equalSegmentGroups(container.root, containee.root);
} else {
return containsSegment(container.root, containee.root);
return containsSegmentGroup(container.root, containee.root);
}
}
function equalSegments(container: UrlSegment, containee: UrlSegment): boolean {
if (!equalPath(container.pathsWithParams, containee.pathsWithParams)) return false;
function equalSegmentGroups(container: UrlSegmentGroup, containee: UrlSegmentGroup): boolean {
if (!equalPath(container.segments, containee.segments)) return false;
if (container.numberOfChildren !== containee.numberOfChildren) return false;
for (let c in containee.children) {
if (!container.children[c]) return false;
if (!equalSegments(container.children[c], containee.children[c])) return false;
if (!equalSegmentGroups(container.children[c], containee.children[c])) return false;
}
return true;
}
function containsSegment(container: UrlSegment, containee: UrlSegment): boolean {
return containsSegmentHelper(container, containee, containee.pathsWithParams);
function containsSegmentGroup(container: UrlSegmentGroup, containee: UrlSegmentGroup): boolean {
return containsSegmentGroupHelper(container, containee, containee.segments);
}
function containsSegmentHelper(
container: UrlSegment, containee: UrlSegment, containeePaths: UrlPathWithParams[]): boolean {
if (container.pathsWithParams.length > containeePaths.length) {
const current = container.pathsWithParams.slice(0, containeePaths.length);
function containsSegmentGroupHelper(
container: UrlSegmentGroup, containee: UrlSegmentGroup, containeePaths: UrlSegment[]): boolean {
if (container.segments.length > containeePaths.length) {
const current = container.segments.slice(0, containeePaths.length);
if (!equalPath(current, containeePaths)) return false;
if (containee.hasChildren()) return false;
return true;
} else if (container.pathsWithParams.length === containeePaths.length) {
if (!equalPath(container.pathsWithParams, containeePaths)) return false;
} else if (container.segments.length === containeePaths.length) {
if (!equalPath(container.segments, containeePaths)) return false;
for (let c in containee.children) {
if (!container.children[c]) return false;
if (!containsSegment(container.children[c], containee.children[c])) return false;
if (!containsSegmentGroup(container.children[c], containee.children[c])) return false;
}
return true;
} else {
const current = containeePaths.slice(0, container.pathsWithParams.length);
const next = containeePaths.slice(container.pathsWithParams.length);
if (!equalPath(container.pathsWithParams, current)) return false;
const current = containeePaths.slice(0, container.segments.length);
const next = containeePaths.slice(container.segments.length);
if (!equalPath(container.segments, current)) return false;
if (!container.children[PRIMARY_OUTLET]) return false;
return containsSegmentHelper(container.children[PRIMARY_OUTLET], containee, next);
return containsSegmentGroupHelper(container.children[PRIMARY_OUTLET], containee, next);
}
}
@ -70,7 +70,7 @@ export class UrlTree {
* @internal
*/
constructor(
public root: UrlSegment, public queryParams: {[key: string]: string},
public root: UrlSegmentGroup, public queryParams: {[key: string]: string},
public fragment: string) {}
toString(): string { return new DefaultUrlSerializer().serialize(this); }
@ -79,20 +79,19 @@ export class UrlTree {
/**
* @stable
*/
export class UrlSegment {
export class UrlSegmentGroup {
/**
* @internal
*/
_sourceSegment: UrlSegment;
_sourceSegment: UrlSegmentGroup;
/**
* @internal
*/
_pathIndexShift: number;
_segmentIndexShift: number;
public parent: UrlSegment = null;
constructor(
public pathsWithParams: UrlPathWithParams[], public children: {[key: string]: UrlSegment}) {
public parent: UrlSegmentGroup = null;
constructor(public segments: UrlSegment[], public children: {[key: string]: UrlSegmentGroup}) {
forEach(children, (v: any, k: any) => v.parent = this);
}
@ -113,12 +112,12 @@ export class UrlSegment {
/**
* @stable
*/
export class UrlPathWithParams {
export class UrlSegment {
constructor(public path: string, public parameters: {[key: string]: string}) {}
toString(): string { return serializePath(this); }
}
export function equalPathsWithParams(a: UrlPathWithParams[], b: UrlPathWithParams[]): boolean {
export function equalSegments(a: UrlSegment[], b: UrlSegment[]): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; ++i) {
if (a[i].path !== b[i].path) return false;
@ -127,7 +126,7 @@ export function equalPathsWithParams(a: UrlPathWithParams[], b: UrlPathWithParam
return true;
}
export function equalPath(a: UrlPathWithParams[], b: UrlPathWithParams[]): boolean {
export function equalPath(a: UrlSegment[], b: UrlSegment[]): boolean {
if (a.length !== b.length) return false;
for (let i = 0; i < a.length; ++i) {
if (a[i].path !== b[i].path) return false;
@ -136,14 +135,14 @@ export function equalPath(a: UrlPathWithParams[], b: UrlPathWithParams[]): boole
}
export function mapChildrenIntoArray<T>(
segment: UrlSegment, fn: (v: UrlSegment, k: string) => T[]): T[] {
segment: UrlSegmentGroup, fn: (v: UrlSegmentGroup, k: string) => T[]): T[] {
let res: T[] = [];
forEach(segment.children, (child: UrlSegment, childOutlet: string) => {
forEach(segment.children, (child: UrlSegmentGroup, childOutlet: string) => {
if (childOutlet === PRIMARY_OUTLET) {
res = res.concat(fn(child, childOutlet));
}
});
forEach(segment.children, (child: UrlSegment, childOutlet: string) => {
forEach(segment.children, (child: UrlSegmentGroup, childOutlet: string) => {
if (childOutlet !== PRIMARY_OUTLET) {
res = res.concat(fn(child, childOutlet));
}
@ -190,17 +189,17 @@ export class DefaultUrlSerializer implements UrlSerializer {
}
}
export function serializePaths(segment: UrlSegment): string {
return segment.pathsWithParams.map(p => serializePath(p)).join('/');
export function serializePaths(segment: UrlSegmentGroup): string {
return segment.segments.map(p => serializePath(p)).join('/');
}
function serializeSegment(segment: UrlSegment, root: boolean): string {
function serializeSegment(segment: UrlSegmentGroup, root: boolean): string {
if (segment.hasChildren() && root) {
const primary = segment.children[PRIMARY_OUTLET] ?
serializeSegment(segment.children[PRIMARY_OUTLET], false) :
'';
const children: string[] = [];
forEach(segment.children, (v: UrlSegment, k: string) => {
forEach(segment.children, (v: UrlSegmentGroup, k: string) => {
if (k !== PRIMARY_OUTLET) {
children.push(`${k}:${serializeSegment(v, false)}`);
}
@ -212,7 +211,7 @@ function serializeSegment(segment: UrlSegment, root: boolean): string {
}
} else if (segment.hasChildren() && !root) {
const children = mapChildrenIntoArray(segment, (v: UrlSegment, k: string) => {
const children = mapChildrenIntoArray(segment, (v: UrlSegmentGroup, k: string) => {
if (k === PRIMARY_OUTLET) {
return [serializeSegment(segment.children[PRIMARY_OUTLET], false)];
} else {
@ -226,7 +225,7 @@ function serializeSegment(segment: UrlSegment, root: boolean): string {
}
}
export function serializePath(path: UrlPathWithParams): string {
export function serializePath(path: UrlSegment): string {
return `${encodeURIComponent(path.path)}${serializeParams(path.parameters)}`;
}
@ -256,7 +255,7 @@ function pairs<T>(obj: {[key: string]: T}): Pair<string, T>[] {
}
const SEGMENT_RE = /^[^\/\(\)\?;=&#]+/;
function matchPathWithParams(str: string): string {
function matchSegments(str: string): string {
SEGMENT_RE.lastIndex = 0;
const match = SEGMENT_RE.exec(str);
return match ? match[0] : '';
@ -289,19 +288,19 @@ class UrlParser {
this.remaining = this.remaining.substring(str.length);
}
parseRootSegment(): UrlSegment {
parseRootSegment(): UrlSegmentGroup {
if (this.remaining.startsWith('/')) {
this.capture('/');
}
if (this.remaining === '' || this.remaining.startsWith('?') || this.remaining.startsWith('#')) {
return new UrlSegment([], {});
return new UrlSegmentGroup([], {});
} else {
return new UrlSegment([], this.parseSegmentChildren());
return new UrlSegmentGroup([], this.parseChildren());
}
}
parseSegmentChildren(): {[key: string]: UrlSegment} {
parseChildren(): {[key: string]: UrlSegmentGroup} {
if (this.remaining.length == 0) {
return {};
}
@ -312,34 +311,34 @@ class UrlParser {
let paths: any[] = [];
if (!this.peekStartsWith('(')) {
paths.push(this.parsePathWithParams());
paths.push(this.parseSegments());
}
while (this.peekStartsWith('/') && !this.peekStartsWith('//') && !this.peekStartsWith('/(')) {
this.capture('/');
paths.push(this.parsePathWithParams());
paths.push(this.parseSegments());
}
let children: {[key: string]: UrlSegment} = {};
let children: {[key: string]: UrlSegmentGroup} = {};
if (this.peekStartsWith('/(')) {
this.capture('/');
children = this.parseParens(true);
}
let res: {[key: string]: UrlSegment} = {};
let res: {[key: string]: UrlSegmentGroup} = {};
if (this.peekStartsWith('(')) {
res = this.parseParens(false);
}
if (paths.length > 0 || Object.keys(children).length > 0) {
res[PRIMARY_OUTLET] = new UrlSegment(paths, children);
res[PRIMARY_OUTLET] = new UrlSegmentGroup(paths, children);
}
return res;
}
parsePathWithParams(): UrlPathWithParams {
const path = matchPathWithParams(this.remaining);
parseSegments(): UrlSegment {
const path = matchSegments(this.remaining);
if (path === '' && this.peekStartsWith(';')) {
throw new Error(`Empty path url segment cannot have parameters: '${this.remaining}'.`);
}
@ -349,7 +348,7 @@ class UrlParser {
if (this.peekStartsWith(';')) {
matrixParams = this.parseMatrixParams();
}
return new UrlPathWithParams(decodeURIComponent(path), matrixParams);
return new UrlSegment(decodeURIComponent(path), matrixParams);
}
parseQueryParams(): {[key: string]: any} {
@ -383,7 +382,7 @@ class UrlParser {
}
parseParam(params: {[key: string]: any}): void {
const key = matchPathWithParams(this.remaining);
const key = matchSegments(this.remaining);
if (!key) {
return;
}
@ -391,7 +390,7 @@ class UrlParser {
let value: any = 'true';
if (this.peekStartsWith('=')) {
this.capture('=');
const valueMatch = matchPathWithParams(this.remaining);
const valueMatch = matchSegments(this.remaining);
if (valueMatch) {
value = valueMatch;
this.capture(value);
@ -419,11 +418,11 @@ class UrlParser {
params[decodeURIComponent(key)] = decodeURIComponent(value);
}
parseParens(allowPrimary: boolean): {[key: string]: UrlSegment} {
const segments: {[key: string]: UrlSegment} = {};
parseParens(allowPrimary: boolean): {[key: string]: UrlSegmentGroup} {
const segments: {[key: string]: UrlSegmentGroup} = {};
this.capture('(');
while (!this.peekStartsWith(')') && this.remaining.length > 0) {
const path = matchPathWithParams(this.remaining);
const path = matchSegments(this.remaining);
const next = this.remaining[path.length];
@ -442,9 +441,9 @@ class UrlParser {
outletName = PRIMARY_OUTLET;
}
const children = this.parseSegmentChildren();
const children = this.parseChildren();
segments[outletName] = Object.keys(children).length === 1 ? children[PRIMARY_OUTLET] :
new UrlSegment([], children);
new UrlSegmentGroup([], children);
if (this.peekStartsWith('//')) {
this.capture('//');
}

View File

@ -12,7 +12,7 @@ import {of } from 'rxjs/observable/of';
import {applyRedirects} from '../src/apply_redirects';
import {Routes} from '../src/config';
import {LoadedRouterConfig} from '../src/router_config_loader';
import {DefaultUrlSerializer, UrlSegment, UrlTree, equalPathsWithParams} from '../src/url_tree';
import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree, equalSegments} from '../src/url_tree';
describe('applyRedirects', () => {
@ -367,10 +367,9 @@ function compareTrees(actual: UrlTree, expected: UrlTree): void {
compareSegments(actual.root, expected.root, error);
}
function compareSegments(actual: UrlSegment, expected: UrlSegment, error: string): void {
function compareSegments(actual: UrlSegmentGroup, expected: UrlSegmentGroup, error: string): void {
expect(actual).toBeDefined(error);
expect(equalPathsWithParams(actual.pathsWithParams, expected.pathsWithParams))
.toEqual(true, error);
expect(equalSegments(actual.segments, expected.segments)).toEqual(true, error);
expect(Object.keys(actual.children).length).toEqual(Object.keys(expected.children).length, error);

View File

@ -11,12 +11,12 @@ import {createRouterState} from '../src/create_router_state';
import {recognize} from '../src/recognize';
import {ActivatedRoute, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from '../src/router_state';
import {PRIMARY_OUTLET, Params} from '../src/shared';
import {DefaultUrlSerializer, UrlSegment, UrlTree} from '../src/url_tree';
import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree} from '../src/url_tree';
import {TreeNode} from '../src/utils/tree';
describe('create router state', () => {
const emptyState = () =>
createEmptyState(new UrlTree(new UrlSegment([], {}), {}, null), RootComponent);
createEmptyState(new UrlTree(new UrlSegmentGroup([], {}), {}, null), RootComponent);
it('should work create new state', () => {
const state = createRouterState(

View File

@ -11,7 +11,7 @@ import {BehaviorSubject} from 'rxjs/BehaviorSubject';
import {createUrlTree} from '../src/create_url_tree';
import {ActivatedRoute, ActivatedRouteSnapshot, advanceActivatedRoute} from '../src/router_state';
import {PRIMARY_OUTLET, Params} from '../src/shared';
import {DefaultUrlSerializer, UrlPathWithParams, UrlSegment, UrlTree} from '../src/url_tree';
import {DefaultUrlSerializer, UrlSegment, UrlSegmentGroup, UrlTree} from '../src/url_tree';
describe('createUrlTree', () => {
const serializer = new DefaultUrlSerializer();
@ -37,7 +37,7 @@ describe('createUrlTree', () => {
it('should stringify positional parameters', () => {
const p = serializer.parse('/a/b');
const t = createRoot(p, ['/one', 11]);
const params = t.root.children[PRIMARY_OUTLET].pathsWithParams;
const params = t.root.children[PRIMARY_OUTLET].segments;
expect(params[0].path).toEqual('one');
expect(params[1].path).toEqual('11');
});
@ -209,8 +209,8 @@ function createRoot(tree: UrlTree, commands: any[], queryParams?: Params, fragme
}
function create(
segment: UrlSegment, startIndex: number, tree: UrlTree, commands: any[], queryParams?: Params,
fragment?: string) {
segment: UrlSegmentGroup, startIndex: number, tree: UrlTree, commands: any[],
queryParams?: Params, fragment?: string) {
if (!segment) {
expect(segment).toBeDefined();
}

View File

@ -10,7 +10,7 @@ import {Routes} from '../src/config';
import {recognize} from '../src/recognize';
import {resolve} from '../src/resolve';
import {RouterStateSnapshot} from '../src/router_state';
import {DefaultUrlSerializer, UrlSegment, UrlTree} from '../src/url_tree';
import {DefaultUrlSerializer, UrlSegmentGroup, UrlTree} from '../src/url_tree';
describe('resolve', () => {
it('should resolve components', () => {

View File

@ -7,7 +7,7 @@
*/
import {PRIMARY_OUTLET} from '../src/shared';
import {DefaultUrlSerializer, UrlSegment, serializePath} from '../src/url_tree';
import {DefaultUrlSerializer, UrlSegmentGroup, serializePath} from '../src/url_tree';
describe('url serializer', () => {
const url = new DefaultUrlSerializer();
@ -174,8 +174,8 @@ describe('url serializer', () => {
`/${encodeURIComponent("one two")};${encodeURIComponent("p 1")}=${encodeURIComponent("v 1")};${encodeURIComponent("p 2")}=${encodeURIComponent("v 2")}`;
const tree = url.parse(u);
expect(tree.root.children[PRIMARY_OUTLET].pathsWithParams[0].path).toEqual('one two');
expect(tree.root.children[PRIMARY_OUTLET].pathsWithParams[0].parameters)
expect(tree.root.children[PRIMARY_OUTLET].segments[0].path).toEqual('one two');
expect(tree.root.children[PRIMARY_OUTLET].segments[0].parameters)
.toEqual({['p 1']: 'v 1', ['p 2']: 'v 2'});
expect(url.serialize(tree)).toEqual(u);
});
@ -210,11 +210,12 @@ describe('url serializer', () => {
});
});
function expectSegment(segment: UrlSegment, expected: string, hasChildren: boolean = false): void {
if (segment.pathsWithParams.filter(s => s.path === '').length > 0) {
throw new Error(`UrlPathWithParams cannot be empty ${segment.pathsWithParams}`);
function expectSegment(
segment: UrlSegmentGroup, expected: string, hasChildren: boolean = false): void {
if (segment.segments.filter(s => s.path === '').length > 0) {
throw new Error(`UrlSegments cannot be empty ${segment.segments}`);
}
const p = segment.pathsWithParams.map(p => serializePath(p)).join('/');
const p = segment.segments.map(p => serializePath(p)).join('/');
expect(p).toEqual(expected);
expect(Object.keys(segment.children).length > 0).toEqual(hasChildren);
}

View File

@ -5,7 +5,7 @@ export declare class ActivatedRoute {
outlet: string;
params: Observable<Params>;
snapshot: ActivatedRouteSnapshot;
url: Observable<UrlPathWithParams[]>;
url: Observable<UrlSegment[]>;
toString(): string;
}
@ -15,7 +15,7 @@ export declare class ActivatedRouteSnapshot {
data: Data;
outlet: string;
params: Params;
url: UrlPathWithParams[];
url: UrlSegment[];
toString(): string;
}
@ -251,7 +251,7 @@ export declare class RoutesRecognized {
}
/** @stable */
export declare class UrlPathWithParams {
export declare class UrlSegment {
parameters: {
[key: string]: string;
};
@ -274,6 +274,6 @@ export declare class UrlTree {
queryParams: {
[key: string]: string;
};
root: UrlSegment;
root: UrlSegmentGroup;
toString(): string;
}