@ -1,19 +0,0 @@
|
||||
import {isPresent} from 'angular2/src/facade/lang';
|
||||
|
||||
export function parseAndAssignParamString(splitToken: string, paramString: string,
|
||||
keyValueMap: StringMap<string, string>): void {
|
||||
var first = paramString[0];
|
||||
if (first == '?' || first == ';') {
|
||||
paramString = paramString.substring(1);
|
||||
}
|
||||
|
||||
paramString.split(splitToken)
|
||||
.forEach((entry) => {
|
||||
var tuple = entry.split('=');
|
||||
var key = tuple[0];
|
||||
if (!isPresent(keyValueMap[key])) {
|
||||
var value = tuple.length > 1 ? tuple[1] : true;
|
||||
keyValueMap[key] = value;
|
||||
}
|
||||
});
|
||||
}
|
@ -6,9 +6,11 @@ import {
|
||||
List,
|
||||
ListWrapper
|
||||
} from 'angular2/src/facade/collection';
|
||||
import {isPresent, isBlank, normalizeBlank} from 'angular2/src/facade/lang';
|
||||
import {isPresent, isBlank, normalizeBlank, Type} from 'angular2/src/facade/lang';
|
||||
import {Promise} from 'angular2/src/facade/async';
|
||||
|
||||
import {PathRecognizer} from './path_recognizer';
|
||||
import {Url} from './url_parser';
|
||||
|
||||
export class RouteParams {
|
||||
constructor(public params: StringMap<string, string>) {}
|
||||
@ -18,34 +20,82 @@ export class RouteParams {
|
||||
|
||||
|
||||
/**
|
||||
* An `Instruction` represents the component hierarchy of the application based on a given route
|
||||
* `Instruction` is a tree of `ComponentInstructions`, with all the information needed
|
||||
* to transition each component in the app to a given route, including all auxiliary routes.
|
||||
*
|
||||
* This is a public API.
|
||||
*/
|
||||
export class Instruction {
|
||||
// "capturedUrl" is the part of the URL captured by this instruction
|
||||
// "accumulatedUrl" is the part of the URL captured by this instruction and all children
|
||||
accumulatedUrl: string;
|
||||
reuse: boolean = false;
|
||||
specificity: number;
|
||||
constructor(public component: ComponentInstruction, public child: Instruction,
|
||||
public auxInstruction: StringMap<string, Instruction>) {}
|
||||
|
||||
constructor(public component: any, public capturedUrl: string,
|
||||
private _recognizer: PathRecognizer, public child: Instruction = null,
|
||||
private _params: StringMap<string, any> = null) {
|
||||
this.accumulatedUrl = capturedUrl;
|
||||
this.specificity = _recognizer.specificity;
|
||||
if (isPresent(child)) {
|
||||
this.child = child;
|
||||
this.specificity += child.specificity;
|
||||
var childUrl = child.accumulatedUrl;
|
||||
if (isPresent(childUrl)) {
|
||||
this.accumulatedUrl += childUrl;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
params(): StringMap<string, string> {
|
||||
if (isBlank(this._params)) {
|
||||
this._params = this._recognizer.parseParams(this.capturedUrl);
|
||||
}
|
||||
return this._params;
|
||||
replaceChild(child: Instruction): Instruction {
|
||||
return new Instruction(this.component, child, this.auxInstruction);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a partially completed instruction during recognition that only has the
|
||||
* primary (non-aux) route instructions matched.
|
||||
*
|
||||
* `PrimaryInstruction` is an internal class used by `RouteRecognizer` while it's
|
||||
* figuring out where to navigate.
|
||||
*/
|
||||
export class PrimaryInstruction {
|
||||
constructor(public component: ComponentInstruction, public child: PrimaryInstruction,
|
||||
public auxUrls: List<Url>) {}
|
||||
}
|
||||
|
||||
export function stringifyInstruction(instruction: Instruction): string {
|
||||
var params = instruction.component.urlParams.length > 0 ?
|
||||
('?' + instruction.component.urlParams.join('&')) :
|
||||
'';
|
||||
|
||||
return instruction.component.urlPath + stringifyAux(instruction) +
|
||||
stringifyPrimary(instruction.child) + params;
|
||||
}
|
||||
|
||||
function stringifyPrimary(instruction: Instruction): string {
|
||||
if (isBlank(instruction)) {
|
||||
return '';
|
||||
}
|
||||
var params = instruction.component.urlParams.length > 0 ?
|
||||
(';' + instruction.component.urlParams.join(';')) :
|
||||
'';
|
||||
return '/' + instruction.component.urlPath + params + stringifyAux(instruction) +
|
||||
stringifyPrimary(instruction.child);
|
||||
}
|
||||
|
||||
function stringifyAux(instruction: Instruction): string {
|
||||
var routes = [];
|
||||
StringMapWrapper.forEach(instruction.auxInstruction, (auxInstruction, _) => {
|
||||
routes.push(stringifyPrimary(auxInstruction));
|
||||
});
|
||||
if (routes.length > 0) {
|
||||
return '(' + routes.join('//') + ')';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* A `ComponentInstruction` represents the route state for a single component. An `Instruction` is
|
||||
* composed of a tree of these `ComponentInstruction`s.
|
||||
*
|
||||
* `ComponentInstructions` is a public API. Instances of `ComponentInstruction` are passed
|
||||
* to route lifecycle hooks, like {@link CanActivate}.
|
||||
*/
|
||||
export class ComponentInstruction {
|
||||
reuse: boolean = false;
|
||||
|
||||
constructor(public urlPath: string, public urlParams: List<string>,
|
||||
private _recognizer: PathRecognizer, public params: StringMap<string, any> = null) {}
|
||||
|
||||
get componentType() { return this._recognizer.handler.componentType; }
|
||||
|
||||
resolveComponentType(): Promise<Type> { return this._recognizer.handler.resolveComponentType(); }
|
||||
|
||||
get specificity() { return this._recognizer.specificity; }
|
||||
|
||||
get terminal() { return this._recognizer.terminal; }
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {Instruction} from './instruction';
|
||||
import {ComponentInstruction} from './instruction';
|
||||
import {global} from 'angular2/src/facade/lang';
|
||||
|
||||
// This is here only so that after TS transpilation the file is not empty.
|
||||
@ -11,33 +11,33 @@ var __ignore_me = global;
|
||||
* Defines route lifecycle method [onActivate]
|
||||
*/
|
||||
export interface OnActivate {
|
||||
onActivate(nextInstruction: Instruction, prevInstruction: Instruction): any;
|
||||
onActivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines route lifecycle method [onReuse]
|
||||
*/
|
||||
export interface OnReuse {
|
||||
onReuse(nextInstruction: Instruction, prevInstruction: Instruction): any;
|
||||
onReuse(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines route lifecycle method [onDeactivate]
|
||||
*/
|
||||
export interface OnDeactivate {
|
||||
onDeactivate(nextInstruction: Instruction, prevInstruction: Instruction): any;
|
||||
onDeactivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines route lifecycle method [canReuse]
|
||||
*/
|
||||
export interface CanReuse {
|
||||
canReuse(nextInstruction: Instruction, prevInstruction: Instruction): any;
|
||||
canReuse(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any;
|
||||
}
|
||||
|
||||
/**
|
||||
* Defines route lifecycle method [canDeactivate]
|
||||
*/
|
||||
export interface CanDeactivate {
|
||||
canDeactivate(nextInstruction: Instruction, prevInstruction: Instruction): any;
|
||||
canDeactivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any;
|
||||
}
|
||||
|
@ -6,7 +6,7 @@
|
||||
import {makeDecorator} from 'angular2/src/util/decorators';
|
||||
import {CanActivate as CanActivateAnnotation} from './lifecycle_annotations_impl';
|
||||
import {Promise} from 'angular2/src/facade/async';
|
||||
import {Instruction} from 'angular2/src/router/instruction';
|
||||
import {ComponentInstruction} from 'angular2/src/router/instruction';
|
||||
|
||||
export {
|
||||
canReuse,
|
||||
@ -17,5 +17,5 @@ export {
|
||||
} from './lifecycle_annotations_impl';
|
||||
|
||||
export var CanActivate:
|
||||
(hook: (next: Instruction, prev: Instruction) => Promise<boolean>| boolean) => ClassDecorator =
|
||||
(hook: (next: ComponentInstruction, prev: ComponentInstruction) => Promise<boolean>| boolean) => ClassDecorator =
|
||||
makeDecorator(CanActivateAnnotation);
|
||||
|
@ -7,7 +7,6 @@ import {
|
||||
isBlank,
|
||||
BaseException
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
import {
|
||||
Map,
|
||||
MapWrapper,
|
||||
@ -17,21 +16,14 @@ import {
|
||||
ListWrapper
|
||||
} from 'angular2/src/facade/collection';
|
||||
import {IMPLEMENTS} from 'angular2/src/facade/lang';
|
||||
import {parseAndAssignParamString} from 'angular2/src/router/helpers';
|
||||
import {escapeRegex} from './url';
|
||||
|
||||
import {RouteHandler} from './route_handler';
|
||||
import {Url, RootUrl, serializeParams} from './url_parser';
|
||||
import {ComponentInstruction} from './instruction';
|
||||
|
||||
// TODO(jeffbcross): implement as interface when ts2dart adds support:
|
||||
// https://github.com/angular/ts2dart/issues/173
|
||||
export class Segment {
|
||||
name: string;
|
||||
regex: string;
|
||||
generate(params: TouchMap): string { return ''; }
|
||||
}
|
||||
|
||||
class TouchMap {
|
||||
map: StringMap<string, string> = StringMapWrapper.create();
|
||||
keys: StringMap<string, boolean> = StringMapWrapper.create();
|
||||
export class TouchMap {
|
||||
map: StringMap<string, string> = {};
|
||||
keys: StringMap<string, boolean> = {};
|
||||
|
||||
constructor(map: StringMap<string, any>) {
|
||||
if (isPresent(map)) {
|
||||
@ -63,31 +55,28 @@ function normalizeString(obj: any): string {
|
||||
}
|
||||
}
|
||||
|
||||
class ContinuationSegment extends Segment {}
|
||||
|
||||
class StaticSegment extends Segment {
|
||||
regex: string;
|
||||
name: string = '';
|
||||
|
||||
constructor(public string: string) {
|
||||
super();
|
||||
this.regex = escapeRegex(string);
|
||||
|
||||
// we add this property so that the route matcher still sees
|
||||
// this segment as a valid path even if do not use the matrix
|
||||
// parameters
|
||||
this.regex += '(;[^\/]+)?';
|
||||
}
|
||||
|
||||
generate(params: TouchMap): string { return this.string; }
|
||||
export interface Segment {
|
||||
name: string;
|
||||
generate(params: TouchMap): string;
|
||||
match(path: string): boolean;
|
||||
}
|
||||
|
||||
@IMPLEMENTS(Segment)
|
||||
class DynamicSegment {
|
||||
regex: string = "([^/]+)";
|
||||
class ContinuationSegment implements Segment {
|
||||
name: string = '';
|
||||
generate(params: TouchMap): string { return ''; }
|
||||
match(path: string): boolean { return true; }
|
||||
}
|
||||
|
||||
class StaticSegment implements Segment {
|
||||
name: string = '';
|
||||
constructor(public path: string) {}
|
||||
match(path: string): boolean { return path == this.path; }
|
||||
generate(params: TouchMap): string { return this.path; }
|
||||
}
|
||||
|
||||
class DynamicSegment implements Segment {
|
||||
constructor(public name: string) {}
|
||||
|
||||
match(path: string): boolean { return true; }
|
||||
generate(params: TouchMap): string {
|
||||
if (!StringMapWrapper.contains(params.map, this.name)) {
|
||||
throw new BaseException(
|
||||
@ -98,11 +87,9 @@ class DynamicSegment {
|
||||
}
|
||||
|
||||
|
||||
class StarSegment {
|
||||
regex: string = "(.+)";
|
||||
|
||||
class StarSegment implements Segment {
|
||||
constructor(public name: string) {}
|
||||
|
||||
match(path: string): boolean { return true; }
|
||||
generate(params: TouchMap): string { return normalizeString(params.get(this.name)); }
|
||||
}
|
||||
|
||||
@ -150,7 +137,7 @@ function parsePathString(route: string): StringMap<string, any> {
|
||||
throw new BaseException(`Unexpected "..." before the end of the path for "${route}".`);
|
||||
}
|
||||
results.push(new ContinuationSegment());
|
||||
} else if (segment.length > 0) {
|
||||
} else {
|
||||
results.push(new StaticSegment(segment));
|
||||
specificity += 100 * (100 - i);
|
||||
}
|
||||
@ -161,6 +148,23 @@ function parsePathString(route: string): StringMap<string, any> {
|
||||
return result;
|
||||
}
|
||||
|
||||
// this function is used to determine whether a route config path like `/foo/:id` collides with
|
||||
// `/foo/:name`
|
||||
function pathDslHash(segments: List<Segment>): string {
|
||||
return segments.map((segment) => {
|
||||
if (segment instanceof StarSegment) {
|
||||
return '*';
|
||||
} else if (segment instanceof ContinuationSegment) {
|
||||
return '...';
|
||||
} else if (segment instanceof DynamicSegment) {
|
||||
return ':';
|
||||
} else if (segment instanceof StaticSegment) {
|
||||
return segment.path;
|
||||
}
|
||||
})
|
||||
.join('/');
|
||||
}
|
||||
|
||||
function splitBySlash(url: string): List<string> {
|
||||
return url.split('/');
|
||||
}
|
||||
@ -178,125 +182,106 @@ function assertPath(path: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export class PathMatch {
|
||||
constructor(public instruction: ComponentInstruction, public remaining: Url,
|
||||
public remainingAux: List<Url>) {}
|
||||
}
|
||||
|
||||
// represents something like '/foo/:bar'
|
||||
export class PathRecognizer {
|
||||
segments: List<Segment>;
|
||||
regex: RegExp;
|
||||
private _segments: List<Segment>;
|
||||
specificity: number;
|
||||
terminal: boolean = true;
|
||||
hash: string;
|
||||
|
||||
static matrixRegex: RegExp = RegExpWrapper.create('^(.*\/[^\/]+?)(;[^\/]+)?\/?$');
|
||||
static queryRegex: RegExp = RegExpWrapper.create('^(.*\/[^\/]+?)(\\?[^\/]+)?$');
|
||||
// TODO: cache component instruction instances by params and by ParsedUrl instance
|
||||
|
||||
constructor(public path: string, public handler: RouteHandler, public isRoot: boolean = false) {
|
||||
constructor(public path: string, public handler: RouteHandler) {
|
||||
assertPath(path);
|
||||
var parsed = parsePathString(path);
|
||||
var specificity = parsed['specificity'];
|
||||
var segments = parsed['segments'];
|
||||
var regexString = '^';
|
||||
|
||||
ListWrapper.forEach(segments, (segment) => {
|
||||
if (segment instanceof ContinuationSegment) {
|
||||
this.terminal = false;
|
||||
} else {
|
||||
regexString += '/' + segment.regex;
|
||||
}
|
||||
});
|
||||
this._segments = parsed['segments'];
|
||||
this.specificity = parsed['specificity'];
|
||||
this.hash = pathDslHash(this._segments);
|
||||
|
||||
if (this.terminal) {
|
||||
regexString += '$';
|
||||
}
|
||||
|
||||
this.regex = RegExpWrapper.create(regexString);
|
||||
this.segments = segments;
|
||||
this.specificity = specificity;
|
||||
var lastSegment = this._segments[this._segments.length - 1];
|
||||
this.terminal = !(lastSegment instanceof ContinuationSegment);
|
||||
}
|
||||
|
||||
parseParams(url: string): StringMap<string, string> {
|
||||
// the last segment is always the star one since it's terminal
|
||||
var segmentsLimit = this.segments.length - 1;
|
||||
var containsStarSegment =
|
||||
segmentsLimit >= 0 && this.segments[segmentsLimit] instanceof StarSegment;
|
||||
|
||||
var paramsString, useQueryString = this.isRoot && this.terminal;
|
||||
if (!containsStarSegment) {
|
||||
var matches = RegExpWrapper.firstMatch(
|
||||
useQueryString ? PathRecognizer.queryRegex : PathRecognizer.matrixRegex, url);
|
||||
if (isPresent(matches)) {
|
||||
url = matches[1];
|
||||
paramsString = matches[2];
|
||||
recognize(beginningSegment: Url): PathMatch {
|
||||
var nextSegment = beginningSegment;
|
||||
var currentSegment: Url;
|
||||
var positionalParams = {};
|
||||
var captured = [];
|
||||
|
||||
for (var i = 0; i < this._segments.length; i += 1) {
|
||||
if (isBlank(nextSegment)) {
|
||||
return null;
|
||||
}
|
||||
currentSegment = nextSegment;
|
||||
|
||||
url = StringWrapper.replaceAll(url, /(;[^\/]+)(?=(\/|$))/g, '');
|
||||
}
|
||||
var segment = this._segments[i];
|
||||
|
||||
var params = StringMapWrapper.create();
|
||||
var urlPart = url;
|
||||
|
||||
for (var i = 0; i <= segmentsLimit; i++) {
|
||||
var segment = this.segments[i];
|
||||
if (segment instanceof ContinuationSegment) {
|
||||
continue;
|
||||
break;
|
||||
}
|
||||
|
||||
var match = RegExpWrapper.firstMatch(RegExpWrapper.create('/' + segment.regex), urlPart);
|
||||
urlPart = StringWrapper.substring(urlPart, match[0].length);
|
||||
if (segment.name.length > 0) {
|
||||
params[segment.name] = match[1];
|
||||
captured.push(currentSegment.path);
|
||||
|
||||
// the star segment consumes all of the remaining URL, including matrix params
|
||||
if (segment instanceof StarSegment) {
|
||||
positionalParams[segment.name] = currentSegment.toString();
|
||||
nextSegment = null;
|
||||
break;
|
||||
}
|
||||
|
||||
if (segment instanceof DynamicSegment) {
|
||||
positionalParams[segment.name] = currentSegment.path;
|
||||
} else if (!segment.match(currentSegment.path)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
nextSegment = currentSegment.child;
|
||||
}
|
||||
|
||||
if (isPresent(paramsString) && paramsString.length > 0) {
|
||||
var expectedStartingValue = useQueryString ? '?' : ';';
|
||||
if (paramsString[0] == expectedStartingValue) {
|
||||
parseAndAssignParamString(expectedStartingValue, paramsString, params);
|
||||
}
|
||||
if (this.terminal && isPresent(nextSegment)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return params;
|
||||
var urlPath = captured.join('/');
|
||||
|
||||
// If this is the root component, read query params. Otherwise, read matrix params.
|
||||
var paramsSegment = beginningSegment instanceof RootUrl ? beginningSegment : currentSegment;
|
||||
|
||||
|
||||
var allParams = isPresent(paramsSegment.params) ?
|
||||
StringMapWrapper.merge(paramsSegment.params, positionalParams) :
|
||||
positionalParams;
|
||||
var urlParams = serializeParams(paramsSegment.params);
|
||||
|
||||
var instruction = new ComponentInstruction(urlPath, urlParams, this, allParams);
|
||||
|
||||
return new PathMatch(instruction, nextSegment, currentSegment.auxiliary);
|
||||
}
|
||||
|
||||
generate(params: StringMap<string, any>): string {
|
||||
|
||||
generate(params: StringMap<string, any>): ComponentInstruction {
|
||||
var paramTokens = new TouchMap(params);
|
||||
var applyLeadingSlash = false;
|
||||
var useQueryString = this.isRoot && this.terminal;
|
||||
|
||||
var url = '';
|
||||
for (var i = 0; i < this.segments.length; i++) {
|
||||
let segment = this.segments[i];
|
||||
let s = segment.generate(paramTokens);
|
||||
applyLeadingSlash = applyLeadingSlash || (segment instanceof ContinuationSegment);
|
||||
var path = [];
|
||||
|
||||
if (s.length > 0) {
|
||||
url += (i > 0 ? '/' : '') + s;
|
||||
for (var i = 0; i < this._segments.length; i++) {
|
||||
let segment = this._segments[i];
|
||||
if (!(segment instanceof ContinuationSegment)) {
|
||||
path.push(segment.generate(paramTokens));
|
||||
}
|
||||
}
|
||||
var urlPath = path.join('/');
|
||||
|
||||
var unusedParams = paramTokens.getUnused();
|
||||
if (!StringMapWrapper.isEmpty(unusedParams)) {
|
||||
url += useQueryString ? '?' : ';';
|
||||
var paramToken = useQueryString ? '&' : ';';
|
||||
var i = 0;
|
||||
StringMapWrapper.forEach(unusedParams, (value, key) => {
|
||||
if (i++ > 0) {
|
||||
url += paramToken;
|
||||
}
|
||||
url += key;
|
||||
if (!isPresent(value) && useQueryString) {
|
||||
value = 'true';
|
||||
}
|
||||
if (isPresent(value)) {
|
||||
url += '=' + value;
|
||||
}
|
||||
});
|
||||
}
|
||||
var nonPositionalParams = paramTokens.getUnused();
|
||||
var urlParams = serializeParams(nonPositionalParams);
|
||||
|
||||
if (applyLeadingSlash) {
|
||||
url += '/';
|
||||
}
|
||||
|
||||
return url;
|
||||
return new ComponentInstruction(urlPath, urlParams, this, params);
|
||||
}
|
||||
|
||||
resolveComponentType(): Promise<any> { return this.handler.resolveComponentType(); }
|
||||
}
|
||||
|
@ -2,6 +2,6 @@ import {RouteConfig as RouteConfigAnnotation, RouteDefinition} from './route_con
|
||||
import {makeDecorator} from 'angular2/src/util/decorators';
|
||||
import {List} from 'angular2/src/facade/collection';
|
||||
|
||||
export {Route, Redirect, AsyncRoute, RouteDefinition} from './route_config_impl';
|
||||
export {Route, Redirect, AuxRoute, AsyncRoute, RouteDefinition} from './route_config_impl';
|
||||
export var RouteConfig: (configs: List<RouteDefinition>) => ClassDecorator =
|
||||
makeDecorator(RouteConfigAnnotation);
|
||||
|
@ -8,7 +8,7 @@ export {RouteDefinition} from './route_definition';
|
||||
*
|
||||
* Supported keys:
|
||||
* - `path` (required)
|
||||
* - `component`, `redirectTo` (requires exactly one of these)
|
||||
* - `component`, `loader`, `redirectTo` (requires exactly one of these)
|
||||
* - `as` (optional)
|
||||
*/
|
||||
@CONST()
|
||||
@ -34,6 +34,21 @@ export class Route implements RouteDefinition {
|
||||
}
|
||||
}
|
||||
|
||||
@CONST()
|
||||
export class AuxRoute implements RouteDefinition {
|
||||
path: string;
|
||||
component: Type;
|
||||
as: string;
|
||||
// added next two properties to work around https://github.com/Microsoft/TypeScript/issues/4107
|
||||
loader: Function = null;
|
||||
redirectTo: string = null;
|
||||
constructor({path, component, as}: {path: string, component: Type, as?: string}) {
|
||||
this.path = path;
|
||||
this.component = component;
|
||||
this.as = as;
|
||||
}
|
||||
}
|
||||
|
||||
@CONST()
|
||||
export class AsyncRoute implements RouteDefinition {
|
||||
path: string;
|
||||
@ -51,6 +66,8 @@ export class Redirect implements RouteDefinition {
|
||||
path: string;
|
||||
redirectTo: string;
|
||||
as: string = null;
|
||||
// added next property to work around https://github.com/Microsoft/TypeScript/issues/4107
|
||||
loader: Function = null;
|
||||
constructor({path, redirectTo}: {path: string, redirectTo: string}) {
|
||||
this.path = path;
|
||||
this.redirectTo = redirectTo;
|
||||
|
@ -1,4 +1,4 @@
|
||||
import {AsyncRoute, Route, Redirect, RouteDefinition} from './route_config_decorator';
|
||||
import {AsyncRoute, AuxRoute, Route, Redirect, RouteDefinition} from './route_config_decorator';
|
||||
import {ComponentDefinition} from './route_definition';
|
||||
import {Type, BaseException} from 'angular2/src/facade/lang';
|
||||
|
||||
@ -6,13 +6,14 @@ import {Type, BaseException} from 'angular2/src/facade/lang';
|
||||
* Given a JS Object that represents... returns a corresponding Route, AsyncRoute, or Redirect
|
||||
*/
|
||||
export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
||||
if (config instanceof Route || config instanceof Redirect || config instanceof AsyncRoute) {
|
||||
if (config instanceof Route || config instanceof Redirect || config instanceof AsyncRoute ||
|
||||
config instanceof AuxRoute) {
|
||||
return <RouteDefinition>config;
|
||||
}
|
||||
|
||||
if ((!config.component) == (!config.redirectTo)) {
|
||||
throw new BaseException(
|
||||
`Route config should contain exactly one 'component', or 'redirectTo' property`);
|
||||
`Route config should contain exactly one "component", "loader", or "redirectTo" property.`);
|
||||
}
|
||||
if (config.component) {
|
||||
if (typeof config.component == 'object') {
|
||||
@ -28,7 +29,7 @@ export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
||||
{path: config.path, loader: componentDefinitionObject.loader, as: config.as});
|
||||
} else {
|
||||
throw new BaseException(
|
||||
`Invalid component type '${componentDefinitionObject.type}'. Valid types are "constructor" and "loader".`);
|
||||
`Invalid component type "${componentDefinitionObject.type}". Valid types are "constructor" and "loader".`);
|
||||
}
|
||||
}
|
||||
return new Route(<{
|
||||
|
@ -6,7 +6,8 @@ import {
|
||||
isPresent,
|
||||
isType,
|
||||
isStringMap,
|
||||
BaseException
|
||||
BaseException,
|
||||
Type
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {
|
||||
Map,
|
||||
@ -17,12 +18,13 @@ import {
|
||||
StringMapWrapper
|
||||
} from 'angular2/src/facade/collection';
|
||||
|
||||
import {PathRecognizer} from './path_recognizer';
|
||||
import {RouteHandler} from './route_handler';
|
||||
import {Route, AsyncRoute, Redirect, RouteDefinition} from './route_config_impl';
|
||||
import {PathRecognizer, PathMatch} from './path_recognizer';
|
||||
import {Route, AsyncRoute, AuxRoute, Redirect, RouteDefinition} from './route_config_impl';
|
||||
import {AsyncRouteHandler} from './async_route_handler';
|
||||
import {SyncRouteHandler} from './sync_route_handler';
|
||||
import {parseAndAssignParamString} from 'angular2/src/router/helpers';
|
||||
import {Url} from './url_parser';
|
||||
import {ComponentInstruction} from './instruction';
|
||||
|
||||
|
||||
/**
|
||||
* `RouteRecognizer` is responsible for recognizing routes for a single component.
|
||||
@ -31,30 +33,45 @@ import {parseAndAssignParamString} from 'angular2/src/router/helpers';
|
||||
*/
|
||||
export class RouteRecognizer {
|
||||
names: Map<string, PathRecognizer> = new Map();
|
||||
redirects: Map<string, string> = new Map();
|
||||
matchers: Map<RegExp, PathRecognizer> = new Map();
|
||||
|
||||
constructor(public isRoot: boolean = false) {}
|
||||
auxRoutes: Map<string, PathRecognizer> = new Map();
|
||||
|
||||
// TODO: optimize this into a trie
|
||||
matchers: List<PathRecognizer> = [];
|
||||
|
||||
// TODO: optimize this into a trie
|
||||
redirects: List<Redirector> = [];
|
||||
|
||||
config(config: RouteDefinition): boolean {
|
||||
var handler;
|
||||
|
||||
if (config instanceof AuxRoute) {
|
||||
handler = new SyncRouteHandler(config.component);
|
||||
let path = config.path.startsWith('/') ? config.path.substring(1) : config.path;
|
||||
var recognizer = new PathRecognizer(config.path, handler);
|
||||
this.auxRoutes.set(path, recognizer);
|
||||
return recognizer.terminal;
|
||||
}
|
||||
if (config instanceof Redirect) {
|
||||
let path = config.path == '/' ? '' : config.path;
|
||||
this.redirects.set(path, config.redirectTo);
|
||||
this.redirects.push(new Redirector(config.path, config.redirectTo));
|
||||
return true;
|
||||
} else if (config instanceof Route) {
|
||||
}
|
||||
|
||||
if (config instanceof Route) {
|
||||
handler = new SyncRouteHandler(config.component);
|
||||
} else if (config instanceof AsyncRoute) {
|
||||
handler = new AsyncRouteHandler(config.loader);
|
||||
}
|
||||
var recognizer = new PathRecognizer(config.path, handler, this.isRoot);
|
||||
MapWrapper.forEach(this.matchers, (matcher, _) => {
|
||||
if (recognizer.regex.toString() == matcher.regex.toString()) {
|
||||
var recognizer = new PathRecognizer(config.path, handler);
|
||||
|
||||
this.matchers.forEach((matcher) => {
|
||||
if (recognizer.hash == matcher.hash) {
|
||||
throw new BaseException(
|
||||
`Configuration '${config.path}' conflicts with existing route '${matcher.path}'`);
|
||||
}
|
||||
});
|
||||
this.matchers.set(recognizer.regex, recognizer);
|
||||
|
||||
this.matchers.push(recognizer);
|
||||
if (isPresent(config.as)) {
|
||||
this.names.set(config.as, recognizer);
|
||||
}
|
||||
@ -66,102 +83,87 @@ export class RouteRecognizer {
|
||||
* Given a URL, returns a list of `RouteMatch`es, which are partial recognitions for some route.
|
||||
*
|
||||
*/
|
||||
recognize(url: string): List<RouteMatch> {
|
||||
recognize(urlParse: Url): List<PathMatch> {
|
||||
var solutions = [];
|
||||
if (url.length > 0 && url[url.length - 1] == '/') {
|
||||
url = url.substring(0, url.length - 1);
|
||||
}
|
||||
|
||||
MapWrapper.forEach(this.redirects, (target, path) => {
|
||||
// "/" redirect case
|
||||
if (path == '/' || path == '') {
|
||||
if (path == url) {
|
||||
url = target;
|
||||
}
|
||||
} else if (url.startsWith(path)) {
|
||||
url = target + url.substring(path.length);
|
||||
}
|
||||
});
|
||||
urlParse = this._redirect(urlParse);
|
||||
|
||||
var queryParams = StringMapWrapper.create();
|
||||
var queryString = '';
|
||||
var queryIndex = url.indexOf('?');
|
||||
if (queryIndex >= 0) {
|
||||
queryString = url.substring(queryIndex + 1);
|
||||
url = url.substring(0, queryIndex);
|
||||
}
|
||||
if (this.isRoot && queryString.length > 0) {
|
||||
parseAndAssignParamString('&', queryString, queryParams);
|
||||
}
|
||||
this.matchers.forEach((pathRecognizer: PathRecognizer) => {
|
||||
var pathMatch = pathRecognizer.recognize(urlParse);
|
||||
|
||||
MapWrapper.forEach(this.matchers, (pathRecognizer, regex) => {
|
||||
var match;
|
||||
if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) {
|
||||
var matchedUrl = '/';
|
||||
var unmatchedUrl = '';
|
||||
if (url != '/') {
|
||||
matchedUrl = match[0];
|
||||
unmatchedUrl = url.substring(match[0].length);
|
||||
}
|
||||
var params = null;
|
||||
if (pathRecognizer.terminal && !StringMapWrapper.isEmpty(queryParams)) {
|
||||
params = queryParams;
|
||||
matchedUrl += '?' + queryString;
|
||||
}
|
||||
solutions.push(new RouteMatch(pathRecognizer, matchedUrl, unmatchedUrl, params));
|
||||
if (isPresent(pathMatch)) {
|
||||
solutions.push(pathMatch);
|
||||
}
|
||||
});
|
||||
|
||||
return solutions;
|
||||
}
|
||||
|
||||
_redirect(urlParse: Url): Url {
|
||||
for (var i = 0; i < this.redirects.length; i += 1) {
|
||||
let redirector = this.redirects[i];
|
||||
var redirectedUrl = redirector.redirect(urlParse);
|
||||
if (isPresent(redirectedUrl)) {
|
||||
return redirectedUrl;
|
||||
}
|
||||
}
|
||||
|
||||
return urlParse;
|
||||
}
|
||||
|
||||
recognizeAuxiliary(urlParse: Url): PathMatch {
|
||||
var pathRecognizer = this.auxRoutes.get(urlParse.path);
|
||||
if (isBlank(pathRecognizer)) {
|
||||
return null;
|
||||
}
|
||||
return pathRecognizer.recognize(urlParse);
|
||||
}
|
||||
|
||||
hasRoute(name: string): boolean { return this.names.has(name); }
|
||||
|
||||
generate(name: string, params: any): StringMap<string, any> {
|
||||
generate(name: string, params: any): ComponentInstruction {
|
||||
var pathRecognizer: PathRecognizer = this.names.get(name);
|
||||
if (isBlank(pathRecognizer)) {
|
||||
return null;
|
||||
}
|
||||
var url = pathRecognizer.generate(params);
|
||||
return {url, 'nextComponent': pathRecognizer.handler.componentType};
|
||||
return pathRecognizer.generate(params);
|
||||
}
|
||||
}
|
||||
|
||||
export class RouteMatch {
|
||||
private _params: StringMap<string, any>;
|
||||
private _paramsParsed: boolean = false;
|
||||
export class Redirector {
|
||||
segments: List<string> = [];
|
||||
toSegments: List<string> = [];
|
||||
|
||||
constructor(public recognizer: PathRecognizer, public matchedUrl: string,
|
||||
public unmatchedUrl: string, p: StringMap<string, any> = null) {
|
||||
this._params = isPresent(p) ? p : StringMapWrapper.create();
|
||||
constructor(path: string, redirectTo: string) {
|
||||
if (path.startsWith('/')) {
|
||||
path = path.substring(1);
|
||||
}
|
||||
this.segments = path.split('/');
|
||||
if (redirectTo.startsWith('/')) {
|
||||
redirectTo = redirectTo.substring(1);
|
||||
}
|
||||
this.toSegments = redirectTo.split('/');
|
||||
}
|
||||
|
||||
params(): StringMap<string, any> {
|
||||
if (!this._paramsParsed) {
|
||||
this._paramsParsed = true;
|
||||
StringMapWrapper.forEach(this.recognizer.parseParams(this.matchedUrl),
|
||||
(value, key) => { StringMapWrapper.set(this._params, key, value); });
|
||||
/**
|
||||
* Returns `null` or a `ParsedUrl` representing the new path to match
|
||||
*/
|
||||
redirect(urlParse: Url): Url {
|
||||
for (var i = 0; i < this.segments.length; i += 1) {
|
||||
if (isBlank(urlParse)) {
|
||||
return null;
|
||||
}
|
||||
let segment = this.segments[i];
|
||||
if (segment != urlParse.path) {
|
||||
return null;
|
||||
}
|
||||
urlParse = urlParse.child;
|
||||
}
|
||||
return this._params;
|
||||
|
||||
for (var i = this.toSegments.length - 1; i >= 0; i -= 1) {
|
||||
let segment = this.toSegments[i];
|
||||
urlParse = new Url(segment, urlParse);
|
||||
}
|
||||
return urlParse;
|
||||
}
|
||||
}
|
||||
|
||||
function configObjToHandler(config: any): RouteHandler {
|
||||
if (isType(config)) {
|
||||
return new SyncRouteHandler(config);
|
||||
} else if (isStringMap(config)) {
|
||||
if (isBlank(config['type'])) {
|
||||
throw new BaseException(
|
||||
`Component declaration when provided as a map should include a 'type' property`);
|
||||
}
|
||||
var componentType = config['type'];
|
||||
if (componentType == 'constructor') {
|
||||
return new SyncRouteHandler(config['constructor']);
|
||||
} else if (componentType == 'loader') {
|
||||
return new AsyncRouteHandler(config['loader']);
|
||||
} else {
|
||||
throw new BaseException(`oops`);
|
||||
}
|
||||
}
|
||||
throw new BaseException(`Unexpected component "${config}".`);
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
import {RouteRecognizer, RouteMatch} from './route_recognizer';
|
||||
import {Instruction} from './instruction';
|
||||
import {PathMatch} from './path_recognizer';
|
||||
import {RouteRecognizer} from './route_recognizer';
|
||||
import {Instruction, ComponentInstruction, PrimaryInstruction} from './instruction';
|
||||
import {
|
||||
List,
|
||||
ListWrapper,
|
||||
@ -24,6 +25,9 @@ import {RouteConfig, AsyncRoute, Route, Redirect, RouteDefinition} from './route
|
||||
import {reflector} from 'angular2/src/reflection/reflection';
|
||||
import {Injectable} from 'angular2/di';
|
||||
import {normalizeRouteConfig} from './route_config_nomalizer';
|
||||
import {parser, Url} from './url_parser';
|
||||
|
||||
var _resolveToNull = PromiseWrapper.resolve(null);
|
||||
|
||||
/**
|
||||
* The RouteRegistry holds route configurations for each component in an Angular app.
|
||||
@ -37,13 +41,13 @@ export class RouteRegistry {
|
||||
/**
|
||||
* Given a component and a configuration object, add the route to this registry
|
||||
*/
|
||||
config(parentComponent: any, config: RouteDefinition, isRootLevelRoute: boolean = false): void {
|
||||
config(parentComponent: any, config: RouteDefinition): void {
|
||||
config = normalizeRouteConfig(config);
|
||||
|
||||
var recognizer: RouteRecognizer = this._rules.get(parentComponent);
|
||||
|
||||
if (isBlank(recognizer)) {
|
||||
recognizer = new RouteRecognizer(isRootLevelRoute);
|
||||
recognizer = new RouteRecognizer();
|
||||
this._rules.set(parentComponent, recognizer);
|
||||
}
|
||||
|
||||
@ -61,7 +65,7 @@ export class RouteRegistry {
|
||||
/**
|
||||
* Reads the annotations of a component and configures the registry based on them
|
||||
*/
|
||||
configFromComponent(component: any, isRootComponent: boolean = false): void {
|
||||
configFromComponent(component: any): void {
|
||||
if (!isType(component)) {
|
||||
return;
|
||||
}
|
||||
@ -77,8 +81,7 @@ export class RouteRegistry {
|
||||
var annotation = annotations[i];
|
||||
|
||||
if (annotation instanceof RouteConfig) {
|
||||
ListWrapper.forEach(annotation.configs,
|
||||
(config) => this.config(component, config, isRootComponent));
|
||||
ListWrapper.forEach(annotation.configs, (config) => this.config(component, config));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -90,63 +93,100 @@ export class RouteRegistry {
|
||||
* the application into the state specified by the url
|
||||
*/
|
||||
recognize(url: string, parentComponent: any): Promise<Instruction> {
|
||||
var parsedUrl = parser.parse(url);
|
||||
return this._recognize(parsedUrl, parentComponent);
|
||||
}
|
||||
|
||||
private _recognize(parsedUrl: Url, parentComponent): Promise<Instruction> {
|
||||
return this._recognizePrimaryRoute(parsedUrl, parentComponent)
|
||||
.then((instruction: PrimaryInstruction) =>
|
||||
this._completeAuxiliaryRouteMatches(instruction, parentComponent));
|
||||
}
|
||||
|
||||
private _recognizePrimaryRoute(parsedUrl: Url, parentComponent): Promise<PrimaryInstruction> {
|
||||
var componentRecognizer = this._rules.get(parentComponent);
|
||||
if (isBlank(componentRecognizer)) {
|
||||
return PromiseWrapper.resolve(null);
|
||||
}
|
||||
|
||||
// Matches some beginning part of the given URL
|
||||
var possibleMatches = componentRecognizer.recognize(url);
|
||||
var possibleMatches = componentRecognizer.recognize(parsedUrl);
|
||||
|
||||
var matchPromises =
|
||||
ListWrapper.map(possibleMatches, (candidate) => this._completeRouteMatch(candidate));
|
||||
ListWrapper.map(possibleMatches, (candidate) => this._completePrimaryRouteMatch(candidate));
|
||||
|
||||
return PromiseWrapper.all(matchPromises)
|
||||
.then((solutions: List<Instruction>) => {
|
||||
// remove nulls
|
||||
var fullSolutions = ListWrapper.filter(solutions, (solution) => isPresent(solution));
|
||||
|
||||
if (fullSolutions.length > 0) {
|
||||
return mostSpecific(fullSolutions);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return PromiseWrapper.all(matchPromises).then(mostSpecific);
|
||||
}
|
||||
|
||||
|
||||
_completeRouteMatch(partialMatch: RouteMatch): Promise<Instruction> {
|
||||
var recognizer = partialMatch.recognizer;
|
||||
var handler = recognizer.handler;
|
||||
return handler.resolveComponentType().then((componentType) => {
|
||||
private _completePrimaryRouteMatch(partialMatch: PathMatch): Promise<PrimaryInstruction> {
|
||||
var instruction = partialMatch.instruction;
|
||||
return instruction.resolveComponentType().then((componentType) => {
|
||||
this.configFromComponent(componentType);
|
||||
|
||||
if (partialMatch.unmatchedUrl.length == 0) {
|
||||
if (recognizer.terminal) {
|
||||
return new Instruction(componentType, partialMatch.matchedUrl, recognizer, null,
|
||||
partialMatch.params());
|
||||
if (isBlank(partialMatch.remaining)) {
|
||||
if (instruction.terminal) {
|
||||
return new PrimaryInstruction(instruction, null, partialMatch.remainingAux);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return this.recognize(partialMatch.unmatchedUrl, componentType)
|
||||
.then(childInstruction => {
|
||||
return this._recognizePrimaryRoute(partialMatch.remaining, componentType)
|
||||
.then((childInstruction) => {
|
||||
if (isBlank(childInstruction)) {
|
||||
return null;
|
||||
} else {
|
||||
return new Instruction(componentType, partialMatch.matchedUrl, recognizer,
|
||||
childInstruction);
|
||||
return new PrimaryInstruction(instruction, childInstruction,
|
||||
partialMatch.remainingAux);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private _completeAuxiliaryRouteMatches(instruction: PrimaryInstruction,
|
||||
parentComponent: any): Promise<Instruction> {
|
||||
if (isBlank(instruction)) {
|
||||
return _resolveToNull;
|
||||
}
|
||||
|
||||
var componentRecognizer = this._rules.get(parentComponent);
|
||||
var auxInstructions = {};
|
||||
|
||||
var promises = instruction.auxUrls.map((auxSegment: Url) => {
|
||||
var match = componentRecognizer.recognizeAuxiliary(auxSegment);
|
||||
if (isBlank(match)) {
|
||||
return _resolveToNull;
|
||||
}
|
||||
return this._completePrimaryRouteMatch(match).then((auxInstruction: PrimaryInstruction) => {
|
||||
if (isPresent(auxInstruction)) {
|
||||
return this._completeAuxiliaryRouteMatches(auxInstruction, parentComponent)
|
||||
.then((finishedAuxRoute: Instruction) => {
|
||||
auxInstructions[auxSegment.path] = finishedAuxRoute;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return PromiseWrapper.all(promises).then((_) => {
|
||||
if (isBlank(instruction.child)) {
|
||||
return new Instruction(instruction.component, null, auxInstructions);
|
||||
}
|
||||
return this._completeAuxiliaryRouteMatches(instruction.child,
|
||||
instruction.component.componentType)
|
||||
.then((completeChild) => {
|
||||
return new Instruction(instruction.component, completeChild, auxInstructions);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a normalized list with component names and params like: `['user', {id: 3 }]`
|
||||
* generates a url with a leading slash relative to the provided `parentComponent`.
|
||||
*/
|
||||
generate(linkParams: List<any>, parentComponent: any): string {
|
||||
let url = '';
|
||||
generate(linkParams: List<any>, parentComponent: any): Instruction {
|
||||
let segments = [];
|
||||
let componentCursor = parentComponent;
|
||||
|
||||
for (let i = 0; i < linkParams.length; i += 1) {
|
||||
let segment = linkParams[i];
|
||||
if (isBlank(componentCursor)) {
|
||||
@ -172,15 +212,22 @@ export class RouteRegistry {
|
||||
`Component "${getTypeNameForDebugging(componentCursor)}" has no route config.`);
|
||||
}
|
||||
var response = componentRecognizer.generate(segment, params);
|
||||
|
||||
if (isBlank(response)) {
|
||||
throw new BaseException(
|
||||
`Component "${getTypeNameForDebugging(componentCursor)}" has no route named "${segment}".`);
|
||||
}
|
||||
url += response['url'];
|
||||
componentCursor = response['nextComponent'];
|
||||
segments.push(response);
|
||||
componentCursor = response.componentType;
|
||||
}
|
||||
|
||||
return url;
|
||||
var instruction = null;
|
||||
|
||||
while (segments.length > 0) {
|
||||
instruction = new Instruction(segments.pop(), instruction, {});
|
||||
}
|
||||
|
||||
return instruction;
|
||||
}
|
||||
}
|
||||
|
||||
@ -188,11 +235,17 @@ export class RouteRegistry {
|
||||
/*
|
||||
* Given a list of instructions, returns the most specific instruction
|
||||
*/
|
||||
function mostSpecific(instructions: List<Instruction>): Instruction {
|
||||
function mostSpecific(instructions: List<PrimaryInstruction>): PrimaryInstruction {
|
||||
if (instructions.length == 0) {
|
||||
return null;
|
||||
}
|
||||
var mostSpecificSolution = instructions[0];
|
||||
for (var solutionIndex = 1; solutionIndex < instructions.length; solutionIndex++) {
|
||||
var solution = instructions[solutionIndex];
|
||||
if (solution.specificity > mostSpecificSolution.specificity) {
|
||||
var solution: PrimaryInstruction = instructions[solutionIndex];
|
||||
if (isBlank(solution)) {
|
||||
continue;
|
||||
}
|
||||
if (solution.component.specificity > mostSpecificSolution.component.specificity) {
|
||||
mostSpecificSolution = solution;
|
||||
}
|
||||
}
|
||||
|
@ -12,7 +12,7 @@ import {
|
||||
|
||||
import {RouteRegistry} from './route_registry';
|
||||
import {Pipeline} from './pipeline';
|
||||
import {Instruction} from './instruction';
|
||||
import {ComponentInstruction, Instruction, stringifyInstruction} from './instruction';
|
||||
import {RouterOutlet} from './router_outlet';
|
||||
import {Location} from './location';
|
||||
import {getCanActivateHook} from './route_lifecycle_reflector';
|
||||
@ -45,10 +45,9 @@ export class Router {
|
||||
private _currentInstruction: Instruction = null;
|
||||
private _currentNavigation: Promise<any> = _resolveToTrue;
|
||||
private _outlet: RouterOutlet = null;
|
||||
private _auxOutlets: Map<string, RouterOutlet> = new Map();
|
||||
private _subject: EventEmitter = new EventEmitter();
|
||||
|
||||
// todo(jeffbcross): rename _registry to registry since it is accessed from subclasses
|
||||
// todo(jeffbcross): rename _pipeline to pipeline since it is accessed from subclasses
|
||||
constructor(public registry: RouteRegistry, public _pipeline: Pipeline, public parent: Router,
|
||||
public hostComponent: any) {}
|
||||
|
||||
@ -65,8 +64,11 @@ export class Router {
|
||||
* you're writing a reusable component.
|
||||
*/
|
||||
registerOutlet(outlet: RouterOutlet): Promise<boolean> {
|
||||
// TODO: sibling routes
|
||||
this._outlet = outlet;
|
||||
if (isPresent(outlet.name)) {
|
||||
this._auxOutlets.set(outlet.name, outlet);
|
||||
} else {
|
||||
this._outlet = outlet;
|
||||
}
|
||||
if (isPresent(this._currentInstruction)) {
|
||||
return outlet.commit(this._currentInstruction);
|
||||
}
|
||||
@ -87,9 +89,8 @@ export class Router {
|
||||
* ```
|
||||
*/
|
||||
config(definitions: List<RouteDefinition>): Promise<any> {
|
||||
definitions.forEach((routeDefinition) => {
|
||||
this.registry.config(this.hostComponent, routeDefinition, this instanceof RootRouter);
|
||||
});
|
||||
definitions.forEach(
|
||||
(routeDefinition) => { this.registry.config(this.hostComponent, routeDefinition); });
|
||||
return this.renavigate();
|
||||
}
|
||||
|
||||
@ -104,31 +105,51 @@ export class Router {
|
||||
return this._currentNavigation = this._currentNavigation.then((_) => {
|
||||
this.lastNavigationAttempt = url;
|
||||
this._startNavigating();
|
||||
return this._afterPromiseFinishNavigating(this.recognize(url).then((matchedInstruction) => {
|
||||
if (isBlank(matchedInstruction)) {
|
||||
return this._afterPromiseFinishNavigating(this.recognize(url).then((instruction) => {
|
||||
if (isBlank(instruction)) {
|
||||
return false;
|
||||
}
|
||||
return this._reuse(matchedInstruction)
|
||||
.then((_) => this._canActivate(matchedInstruction))
|
||||
.then((result) => {
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
return this._canDeactivate(matchedInstruction)
|
||||
.then((result) => {
|
||||
if (result) {
|
||||
return this.commit(matchedInstruction, _skipLocationChange)
|
||||
.then((_) => {
|
||||
this._emitNavigationFinish(matchedInstruction.accumulatedUrl);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return this._navigate(instruction, _skipLocationChange);
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Navigate via the provided instruction. Returns a promise that resolves when navigation is
|
||||
* complete.
|
||||
*/
|
||||
navigateInstruction(instruction: Instruction,
|
||||
_skipLocationChange: boolean = false): Promise<any> {
|
||||
if (isBlank(instruction)) {
|
||||
return _resolveToFalse;
|
||||
}
|
||||
return this._currentNavigation = this._currentNavigation.then((_) => {
|
||||
this._startNavigating();
|
||||
return this._afterPromiseFinishNavigating(this._navigate(instruction, _skipLocationChange));
|
||||
});
|
||||
}
|
||||
|
||||
_navigate(instruction: Instruction, _skipLocationChange: boolean): Promise<any> {
|
||||
return this._reuse(instruction)
|
||||
.then((_) => this._canActivate(instruction))
|
||||
.then((result) => {
|
||||
if (!result) {
|
||||
return false;
|
||||
}
|
||||
return this._canDeactivate(instruction)
|
||||
.then((result) => {
|
||||
if (result) {
|
||||
return this.commit(instruction, _skipLocationChange)
|
||||
.then((_) => {
|
||||
this._emitNavigationFinish(stringifyInstruction(instruction));
|
||||
return true;
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private _emitNavigationFinish(url): void { ObservableWrapper.callNext(this._subject, url); }
|
||||
|
||||
private _afterPromiseFinishNavigating(promise: Promise<any>): Promise<any> {
|
||||
@ -138,21 +159,20 @@ export class Router {
|
||||
});
|
||||
}
|
||||
|
||||
_reuse(instruction): Promise<any> {
|
||||
_reuse(instruction: Instruction): Promise<any> {
|
||||
if (isBlank(this._outlet)) {
|
||||
return _resolveToFalse;
|
||||
}
|
||||
return this._outlet.canReuse(instruction)
|
||||
.then((result) => {
|
||||
instruction.reuse = result;
|
||||
if (isPresent(this._outlet.childRouter) && isPresent(instruction.child)) {
|
||||
return this._outlet.childRouter._reuse(instruction.child);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private _canActivate(instruction: Instruction): Promise<boolean> {
|
||||
return canActivateOne(instruction, this._currentInstruction);
|
||||
private _canActivate(nextInstruction: Instruction): Promise<boolean> {
|
||||
return canActivateOne(nextInstruction, this._currentInstruction);
|
||||
}
|
||||
|
||||
private _canDeactivate(instruction: Instruction): Promise<boolean> {
|
||||
@ -160,11 +180,12 @@ export class Router {
|
||||
return _resolveToTrue;
|
||||
}
|
||||
var next: Promise<boolean>;
|
||||
if (isPresent(instruction) && instruction.reuse) {
|
||||
if (isPresent(instruction) && instruction.component.reuse) {
|
||||
next = _resolveToTrue;
|
||||
} else {
|
||||
next = this._outlet.canDeactivate(instruction);
|
||||
}
|
||||
// TODO: aux route lifecycle hooks
|
||||
return next.then((result) => {
|
||||
if (result == false) {
|
||||
return false;
|
||||
@ -182,10 +203,14 @@ export class Router {
|
||||
*/
|
||||
commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> {
|
||||
this._currentInstruction = instruction;
|
||||
var next = _resolveToTrue;
|
||||
if (isPresent(this._outlet)) {
|
||||
return this._outlet.commit(instruction);
|
||||
next = this._outlet.commit(instruction);
|
||||
}
|
||||
return _resolveToTrue;
|
||||
var promises = [];
|
||||
MapWrapper.forEach(this._auxOutlets,
|
||||
(outlet, _) => { promises.push(outlet.commit(instruction)); });
|
||||
return next.then((_) => PromiseWrapper.all(promises));
|
||||
}
|
||||
|
||||
|
||||
@ -237,7 +262,7 @@ export class Router {
|
||||
* Generate a URL from a component name and optional map of parameters. The URL is relative to the
|
||||
* app's base href.
|
||||
*/
|
||||
generate(linkParams: List<any>): string {
|
||||
generate(linkParams: List<any>): Instruction {
|
||||
let normalizedLinkParams = splitAndFlattenLinkParams(linkParams);
|
||||
|
||||
var first = ListWrapper.first(normalizedLinkParams);
|
||||
@ -275,11 +300,22 @@ export class Router {
|
||||
throw new BaseException(msg);
|
||||
}
|
||||
|
||||
let url = '';
|
||||
if (isPresent(router.parent) && isPresent(router.parent._currentInstruction)) {
|
||||
url = router.parent._currentInstruction.capturedUrl;
|
||||
// TODO: structural cloning and whatnot
|
||||
|
||||
var url = [];
|
||||
var parent = router.parent;
|
||||
while (isPresent(parent)) {
|
||||
url.unshift(parent._currentInstruction);
|
||||
parent = parent.parent;
|
||||
}
|
||||
return url + '/' + this.registry.generate(rest, router.hostComponent);
|
||||
|
||||
var nextInstruction = this.registry.generate(rest, router.hostComponent);
|
||||
|
||||
while (url.length > 0) {
|
||||
nextInstruction = url.pop().replaceChild(nextInstruction);
|
||||
}
|
||||
|
||||
return nextInstruction;
|
||||
}
|
||||
}
|
||||
|
||||
@ -291,14 +327,19 @@ export class RootRouter extends Router {
|
||||
super(registry, pipeline, null, hostComponent);
|
||||
this._location = location;
|
||||
this._location.subscribe((change) => this.navigate(change['url'], isPresent(change['pop'])));
|
||||
this.registry.configFromComponent(hostComponent, true);
|
||||
|
||||
this.registry.configFromComponent(hostComponent);
|
||||
this.navigate(location.path());
|
||||
}
|
||||
|
||||
commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> {
|
||||
var emitUrl = stringifyInstruction(instruction);
|
||||
if (emitUrl.length > 0) {
|
||||
emitUrl = '/' + emitUrl;
|
||||
}
|
||||
var promise = super.commit(instruction);
|
||||
if (!_skipLocationChange) {
|
||||
promise = promise.then((_) => { this._location.go(instruction.accumulatedUrl); });
|
||||
promise = promise.then((_) => { this._location.go(emitUrl); });
|
||||
}
|
||||
return promise;
|
||||
}
|
||||
@ -315,6 +356,12 @@ class ChildRouter extends Router {
|
||||
// Delegate navigation to the root router
|
||||
return this.parent.navigate(url, _skipLocationChange);
|
||||
}
|
||||
|
||||
navigateInstruction(instruction: Instruction,
|
||||
_skipLocationChange: boolean = false): Promise<any> {
|
||||
// Delegate navigation to the root router
|
||||
return this.parent.navigateInstruction(instruction, _skipLocationChange);
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
@ -332,22 +379,24 @@ function splitAndFlattenLinkParams(linkParams: List<any>): List<any> {
|
||||
}, []);
|
||||
}
|
||||
|
||||
function canActivateOne(nextInstruction, currentInstruction): Promise<boolean> {
|
||||
function canActivateOne(nextInstruction: Instruction, prevInstruction: Instruction):
|
||||
Promise<boolean> {
|
||||
var next = _resolveToTrue;
|
||||
if (isPresent(nextInstruction.child)) {
|
||||
next = canActivateOne(nextInstruction.child,
|
||||
isPresent(currentInstruction) ? currentInstruction.child : null);
|
||||
isPresent(prevInstruction) ? prevInstruction.child : null);
|
||||
}
|
||||
return next.then((res) => {
|
||||
if (res == false) {
|
||||
return next.then((result) => {
|
||||
if (result == false) {
|
||||
return false;
|
||||
}
|
||||
if (nextInstruction.reuse) {
|
||||
if (nextInstruction.component.reuse) {
|
||||
return true;
|
||||
}
|
||||
var hook = getCanActivateHook(nextInstruction.component);
|
||||
var hook = getCanActivateHook(nextInstruction.component.componentType);
|
||||
if (isPresent(hook)) {
|
||||
return hook(nextInstruction, currentInstruction);
|
||||
return hook(nextInstruction.component,
|
||||
isPresent(prevInstruction) ? prevInstruction.component : null);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
@ -3,6 +3,7 @@ import {List, StringMap, StringMapWrapper} from 'angular2/src/facade/collection'
|
||||
|
||||
import {Router} from './router';
|
||||
import {Location} from './location';
|
||||
import {Instruction, stringifyInstruction} from './instruction';
|
||||
|
||||
/**
|
||||
* The RouterLink directive lets you link to specific parts of your app.
|
||||
@ -43,19 +44,23 @@ export class RouterLink {
|
||||
|
||||
// the url displayed on the anchor element.
|
||||
visibleHref: string;
|
||||
// the url passed to the router navigation.
|
||||
_navigationHref: string;
|
||||
|
||||
// the instruction passed to the router to navigate
|
||||
private _navigationInstruction: Instruction;
|
||||
|
||||
constructor(private _router: Router, private _location: Location) {}
|
||||
|
||||
set routeParams(changes: List<any>) {
|
||||
this._routeParams = changes;
|
||||
this._navigationHref = this._router.generate(this._routeParams);
|
||||
this.visibleHref = this._location.normalizeAbsolutely(this._navigationHref);
|
||||
this._navigationInstruction = this._router.generate(this._routeParams);
|
||||
|
||||
// TODO: is this the right spot for this?
|
||||
var navigationHref = '/' + stringifyInstruction(this._navigationInstruction);
|
||||
this.visibleHref = this._location.normalizeAbsolutely(navigationHref);
|
||||
}
|
||||
|
||||
onClick(): boolean {
|
||||
this._router.navigate(this._navigationHref);
|
||||
this._router.navigateInstruction(this._navigationInstruction);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -7,7 +7,7 @@ import {DynamicComponentLoader, ComponentRef, ElementRef} from 'angular2/core';
|
||||
import {Injector, bind, Dependency, undefinedValue} from 'angular2/di';
|
||||
|
||||
import * as routerMod from './router';
|
||||
import {Instruction, RouteParams} from './instruction';
|
||||
import {Instruction, ComponentInstruction, RouteParams} from './instruction';
|
||||
import * as hookMod from './lifecycle_annotations';
|
||||
import {hasLifecycleHook} from './route_lifecycle_reflector';
|
||||
|
||||
@ -23,16 +23,16 @@ import {hasLifecycleHook} from './route_lifecycle_reflector';
|
||||
@Directive({selector: 'router-outlet'})
|
||||
export class RouterOutlet {
|
||||
childRouter: routerMod.Router = null;
|
||||
name: string = null;
|
||||
|
||||
private _componentRef: ComponentRef = null;
|
||||
private _currentInstruction: Instruction = null;
|
||||
private _currentInstruction: ComponentInstruction = null;
|
||||
|
||||
constructor(private _elementRef: ElementRef, private _loader: DynamicComponentLoader,
|
||||
private _parentRouter: routerMod.Router, @Attribute('name') nameAttr: string) {
|
||||
// TODO: reintroduce with new // sibling routes
|
||||
// if (isBlank(nameAttr)) {
|
||||
// nameAttr = 'default';
|
||||
//}
|
||||
if (isPresent(nameAttr)) {
|
||||
this.name = nameAttr;
|
||||
}
|
||||
this._parentRouter.registerOutlet(this);
|
||||
}
|
||||
|
||||
@ -40,15 +40,28 @@ export class RouterOutlet {
|
||||
* Given an instruction, update the contents of this outlet.
|
||||
*/
|
||||
commit(instruction: Instruction): Promise<any> {
|
||||
instruction = this._getInstruction(instruction);
|
||||
var componentInstruction = instruction.component;
|
||||
if (isBlank(componentInstruction)) {
|
||||
return PromiseWrapper.resolve(true);
|
||||
}
|
||||
var next;
|
||||
if (instruction.reuse) {
|
||||
next = this._reuse(instruction);
|
||||
if (componentInstruction.reuse) {
|
||||
next = this._reuse(componentInstruction);
|
||||
} else {
|
||||
next = this.deactivate(instruction).then((_) => this._activate(instruction));
|
||||
next = this.deactivate(instruction).then((_) => this._activate(componentInstruction));
|
||||
}
|
||||
return next.then((_) => this._commitChild(instruction));
|
||||
}
|
||||
|
||||
private _getInstruction(instruction: Instruction): Instruction {
|
||||
if (isPresent(this.name)) {
|
||||
return instruction.auxInstruction[this.name];
|
||||
} else {
|
||||
return instruction;
|
||||
}
|
||||
}
|
||||
|
||||
private _commitChild(instruction: Instruction): Promise<any> {
|
||||
if (isPresent(this.childRouter)) {
|
||||
return this.childRouter.commit(instruction.child);
|
||||
@ -57,20 +70,21 @@ export class RouterOutlet {
|
||||
}
|
||||
}
|
||||
|
||||
private _activate(instruction: Instruction): Promise<any> {
|
||||
private _activate(instruction: ComponentInstruction): Promise<any> {
|
||||
var previousInstruction = this._currentInstruction;
|
||||
this._currentInstruction = instruction;
|
||||
this.childRouter = this._parentRouter.childRouter(instruction.component);
|
||||
var componentType = instruction.componentType;
|
||||
this.childRouter = this._parentRouter.childRouter(componentType);
|
||||
|
||||
var bindings = Injector.resolve([
|
||||
bind(RouteParams)
|
||||
.toValue(new RouteParams(instruction.params())),
|
||||
.toValue(new RouteParams(instruction.params)),
|
||||
bind(routerMod.Router).toValue(this.childRouter)
|
||||
]);
|
||||
return this._loader.loadNextToLocation(instruction.component, this._elementRef, bindings)
|
||||
return this._loader.loadNextToLocation(componentType, this._elementRef, bindings)
|
||||
.then((componentRef) => {
|
||||
this._componentRef = componentRef;
|
||||
if (hasLifecycleHook(hookMod.onActivate, instruction.component)) {
|
||||
if (hasLifecycleHook(hookMod.onActivate, componentType)) {
|
||||
return this._componentRef.instance.onActivate(instruction, previousInstruction);
|
||||
}
|
||||
});
|
||||
@ -84,9 +98,11 @@ export class RouterOutlet {
|
||||
if (isBlank(this._currentInstruction)) {
|
||||
return PromiseWrapper.resolve(true);
|
||||
}
|
||||
if (hasLifecycleHook(hookMod.canDeactivate, this._currentInstruction.component)) {
|
||||
return PromiseWrapper.resolve(
|
||||
this._componentRef.instance.canDeactivate(nextInstruction, this._currentInstruction));
|
||||
var outletInstruction = this._getInstruction(nextInstruction);
|
||||
if (hasLifecycleHook(hookMod.canDeactivate, this._currentInstruction.componentType)) {
|
||||
return PromiseWrapper.resolve(this._componentRef.instance.canDeactivate(
|
||||
isPresent(outletInstruction) ? outletInstruction.component : null,
|
||||
this._currentInstruction));
|
||||
}
|
||||
return PromiseWrapper.resolve(true);
|
||||
}
|
||||
@ -97,24 +113,34 @@ export class RouterOutlet {
|
||||
*/
|
||||
canReuse(nextInstruction: Instruction): Promise<boolean> {
|
||||
var result;
|
||||
|
||||
var outletInstruction = this._getInstruction(nextInstruction);
|
||||
var componentInstruction = outletInstruction.component;
|
||||
|
||||
if (isBlank(this._currentInstruction) ||
|
||||
this._currentInstruction.component != nextInstruction.component) {
|
||||
this._currentInstruction.componentType != componentInstruction.componentType) {
|
||||
result = false;
|
||||
} else if (hasLifecycleHook(hookMod.canReuse, this._currentInstruction.component)) {
|
||||
result = this._componentRef.instance.canReuse(nextInstruction, this._currentInstruction);
|
||||
} else if (hasLifecycleHook(hookMod.canReuse, this._currentInstruction.componentType)) {
|
||||
result = this._componentRef.instance.canReuse(componentInstruction, this._currentInstruction);
|
||||
} else {
|
||||
result = nextInstruction == this._currentInstruction ||
|
||||
StringMapWrapper.equals(nextInstruction.params(), this._currentInstruction.params());
|
||||
result =
|
||||
componentInstruction == this._currentInstruction ||
|
||||
(isPresent(componentInstruction.params) && isPresent(this._currentInstruction.params) &&
|
||||
StringMapWrapper.equals(componentInstruction.params, this._currentInstruction.params));
|
||||
}
|
||||
return PromiseWrapper.resolve(result);
|
||||
return PromiseWrapper.resolve(result).then((result) => {
|
||||
// TODO: this is a hack
|
||||
componentInstruction.reuse = result;
|
||||
return result;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private _reuse(instruction): Promise<any> {
|
||||
private _reuse(instruction: ComponentInstruction): Promise<any> {
|
||||
var previousInstruction = this._currentInstruction;
|
||||
this._currentInstruction = instruction;
|
||||
return PromiseWrapper.resolve(
|
||||
hasLifecycleHook(hookMod.onReuse, this._currentInstruction.component) ?
|
||||
hasLifecycleHook(hookMod.onReuse, this._currentInstruction.componentType) ?
|
||||
this._componentRef.instance.onReuse(instruction, previousInstruction) :
|
||||
true);
|
||||
}
|
||||
@ -122,14 +148,16 @@ export class RouterOutlet {
|
||||
|
||||
|
||||
deactivate(nextInstruction: Instruction): Promise<any> {
|
||||
var outletInstruction = this._getInstruction(nextInstruction);
|
||||
var componentInstruction = isPresent(outletInstruction) ? outletInstruction.component : null;
|
||||
return (isPresent(this.childRouter) ?
|
||||
this.childRouter.deactivate(isPresent(nextInstruction) ? nextInstruction.child :
|
||||
null) :
|
||||
this.childRouter.deactivate(isPresent(outletInstruction) ? outletInstruction.child :
|
||||
null) :
|
||||
PromiseWrapper.resolve(true))
|
||||
.then((_) => {
|
||||
if (isPresent(this._componentRef) && isPresent(this._currentInstruction) &&
|
||||
hasLifecycleHook(hookMod.onDeactivate, this._currentInstruction.component)) {
|
||||
return this._componentRef.instance.onDeactivate(nextInstruction,
|
||||
hasLifecycleHook(hookMod.onDeactivate, this._currentInstruction.componentType)) {
|
||||
return this._componentRef.instance.onDeactivate(componentInstruction,
|
||||
this._currentInstruction);
|
||||
}
|
||||
})
|
||||
|
@ -1,9 +0,0 @@
|
||||
import {RegExpWrapper, StringWrapper} from 'angular2/src/facade/lang';
|
||||
|
||||
var specialCharacters = ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'];
|
||||
|
||||
var escapeRe = RegExpWrapper.create('(\\' + specialCharacters.join('|\\') + ')', 'g');
|
||||
|
||||
export function escapeRegex(string: string): string {
|
||||
return StringWrapper.replaceAllMapped(string, escapeRe, (match) => { return "\\" + match; });
|
||||
}
|
210
modules/angular2/src/router/url_parser.ts
Normal file
210
modules/angular2/src/router/url_parser.ts
Normal file
@ -0,0 +1,210 @@
|
||||
import {List, StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {
|
||||
isPresent,
|
||||
isBlank,
|
||||
BaseException,
|
||||
RegExpWrapper,
|
||||
CONST_EXPR
|
||||
} from 'angular2/src/facade/lang';
|
||||
|
||||
|
||||
export class Url {
|
||||
constructor(public path: string, public child: Url = null,
|
||||
public auxiliary: List<Url> = CONST_EXPR([]),
|
||||
public params: StringMap<string, any> = null) {}
|
||||
|
||||
toString(): string {
|
||||
return this.path + this._matrixParamsToString() + this._auxToString() + this._childString();
|
||||
}
|
||||
|
||||
segmentToString(): string { return this.path + this._matrixParamsToString(); }
|
||||
|
||||
_auxToString(): string {
|
||||
return this.auxiliary.length > 0 ?
|
||||
('(' + this.auxiliary.map(sibling => sibling.toString()).join('//') + ')') :
|
||||
'';
|
||||
}
|
||||
|
||||
private _matrixParamsToString(): string {
|
||||
if (isBlank(this.params)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return ';' + serializeParams(this.params).join(';');
|
||||
}
|
||||
|
||||
_childString(): string { return isPresent(this.child) ? ('/' + this.child.toString()) : ''; }
|
||||
}
|
||||
|
||||
export class RootUrl extends Url {
|
||||
constructor(path: string, child: Url = null, auxiliary: List<Url> = CONST_EXPR([]),
|
||||
params: StringMap<string, any> = null) {
|
||||
super(path, child, auxiliary, params);
|
||||
}
|
||||
|
||||
toString(): string {
|
||||
return this.path + this._auxToString() + this._childString() + this._queryParamsToString();
|
||||
}
|
||||
|
||||
segmentToString(): string { return this.path + this._queryParamsToString(); }
|
||||
|
||||
private _queryParamsToString(): string {
|
||||
if (isBlank(this.params)) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '?' + serializeParams(this.params).join('&');
|
||||
}
|
||||
}
|
||||
|
||||
var SEGMENT_RE = RegExpWrapper.create('^[^\\/\\(\\)\\?;=&]+');
|
||||
function matchUrlSegment(str: string): string {
|
||||
var match = RegExpWrapper.firstMatch(SEGMENT_RE, str);
|
||||
return isPresent(match) ? match[0] : null;
|
||||
}
|
||||
|
||||
export class UrlParser {
|
||||
private remaining: string;
|
||||
|
||||
peekStartsWith(str: string): boolean { return this.remaining.startsWith(str); }
|
||||
|
||||
capture(str: string): void {
|
||||
if (!this.remaining.startsWith(str)) {
|
||||
throw new BaseException(`Expected "${str}".`);
|
||||
}
|
||||
this.remaining = this.remaining.substring(str.length);
|
||||
}
|
||||
|
||||
parse(url: string): Url {
|
||||
this.remaining = url;
|
||||
if (url == '' || url == '/') {
|
||||
return new Url('');
|
||||
}
|
||||
return this.parseRoot();
|
||||
}
|
||||
|
||||
// segment + (aux segments) + (query params)
|
||||
parseRoot(): Url {
|
||||
if (this.peekStartsWith('/')) {
|
||||
this.capture('/');
|
||||
}
|
||||
var path = matchUrlSegment(this.remaining);
|
||||
this.capture(path);
|
||||
|
||||
var aux = [];
|
||||
if (this.peekStartsWith('(')) {
|
||||
aux = this.parseAuxiliaryRoutes();
|
||||
}
|
||||
if (this.peekStartsWith(';')) {
|
||||
// TODO: should these params just be dropped?
|
||||
this.parseMatrixParams();
|
||||
}
|
||||
var child = null;
|
||||
if (this.peekStartsWith('/') && !this.peekStartsWith('//')) {
|
||||
this.capture('/');
|
||||
child = this.parseSegment();
|
||||
}
|
||||
var queryParams = null;
|
||||
if (this.peekStartsWith('?')) {
|
||||
queryParams = this.parseQueryParams();
|
||||
}
|
||||
return new RootUrl(path, child, aux, queryParams);
|
||||
}
|
||||
|
||||
// segment + (matrix params) + (aux segments)
|
||||
parseSegment(): Url {
|
||||
if (this.remaining.length == 0) {
|
||||
return null;
|
||||
}
|
||||
if (this.peekStartsWith('/')) {
|
||||
this.capture('/');
|
||||
}
|
||||
var path = matchUrlSegment(this.remaining);
|
||||
this.capture(path);
|
||||
|
||||
var matrixParams = null;
|
||||
if (this.peekStartsWith(';')) {
|
||||
matrixParams = this.parseMatrixParams();
|
||||
}
|
||||
var aux = [];
|
||||
if (this.peekStartsWith('(')) {
|
||||
aux = this.parseAuxiliaryRoutes();
|
||||
}
|
||||
var child = null;
|
||||
if (this.peekStartsWith('/') && !this.peekStartsWith('//')) {
|
||||
this.capture('/');
|
||||
child = this.parseSegment();
|
||||
}
|
||||
return new Url(path, child, aux, matrixParams);
|
||||
}
|
||||
|
||||
parseQueryParams(): StringMap<string, any> {
|
||||
var params = {};
|
||||
this.capture('?');
|
||||
this.parseParam(params);
|
||||
while (this.remaining.length > 0 && this.peekStartsWith('&')) {
|
||||
this.capture('&');
|
||||
this.parseParam(params);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
parseMatrixParams(): StringMap<string, any> {
|
||||
var params = {};
|
||||
while (this.remaining.length > 0 && this.peekStartsWith(';')) {
|
||||
this.capture(';');
|
||||
this.parseParam(params);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
parseParam(params: StringMap<string, any>): void {
|
||||
var key = matchUrlSegment(this.remaining);
|
||||
if (isBlank(key)) {
|
||||
return;
|
||||
}
|
||||
this.capture(key);
|
||||
var value: any = true;
|
||||
if (this.peekStartsWith('=')) {
|
||||
this.capture('=');
|
||||
var valueMatch = matchUrlSegment(this.remaining);
|
||||
if (isPresent(valueMatch)) {
|
||||
value = valueMatch;
|
||||
this.capture(value);
|
||||
}
|
||||
}
|
||||
|
||||
params[key] = value;
|
||||
}
|
||||
|
||||
parseAuxiliaryRoutes(): List<Url> {
|
||||
var routes = [];
|
||||
this.capture('(');
|
||||
|
||||
while (!this.peekStartsWith(')') && this.remaining.length > 0) {
|
||||
routes.push(this.parseSegment());
|
||||
if (this.peekStartsWith('//')) {
|
||||
this.capture('//');
|
||||
}
|
||||
}
|
||||
this.capture(')');
|
||||
|
||||
return routes;
|
||||
}
|
||||
}
|
||||
|
||||
export var parser = new UrlParser();
|
||||
|
||||
export function serializeParams(paramMap: StringMap<string, any>): List<string> {
|
||||
var params = [];
|
||||
if (isPresent(paramMap)) {
|
||||
StringMapWrapper.forEach(paramMap, (value, key) => {
|
||||
if (value == true) {
|
||||
params.push(key);
|
||||
} else {
|
||||
params.push(key + '=' + value);
|
||||
}
|
||||
});
|
||||
}
|
||||
return params;
|
||||
}
|
Reference in New Issue
Block a user