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

View File

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

View File

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

View File

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

View File

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

View File

@ -12,7 +12,7 @@ import {of } from 'rxjs/observable/of';
import {applyRedirects} from '../src/apply_redirects'; import {applyRedirects} from '../src/apply_redirects';
import {Routes} from '../src/config'; import {Routes} from '../src/config';
import {LoadedRouterConfig} from '../src/router_config_loader'; 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', () => { describe('applyRedirects', () => {
@ -367,10 +367,9 @@ function compareTrees(actual: UrlTree, expected: UrlTree): void {
compareSegments(actual.root, expected.root, error); 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(actual).toBeDefined(error);
expect(equalPathsWithParams(actual.pathsWithParams, expected.pathsWithParams)) expect(equalSegments(actual.segments, expected.segments)).toEqual(true, error);
.toEqual(true, error);
expect(Object.keys(actual.children).length).toEqual(Object.keys(expected.children).length, 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 {recognize} from '../src/recognize';
import {ActivatedRoute, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from '../src/router_state'; import {ActivatedRoute, RouterState, RouterStateSnapshot, advanceActivatedRoute, createEmptyState} from '../src/router_state';
import {PRIMARY_OUTLET, Params} from '../src/shared'; 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'; import {TreeNode} from '../src/utils/tree';
describe('create router state', () => { describe('create router state', () => {
const emptyState = () => const emptyState = () =>
createEmptyState(new UrlTree(new UrlSegment([], {}), {}, null), RootComponent); createEmptyState(new UrlTree(new UrlSegmentGroup([], {}), {}, null), RootComponent);
it('should work create new state', () => { it('should work create new state', () => {
const state = createRouterState( const state = createRouterState(

View File

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

View File

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

View File

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

View File

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