feat(router): auxiliary routes

Closes #2775
This commit is contained in:
Brian Ford
2015-07-17 13:36:53 -07:00
parent 96e34c1d36
commit ac6227e434
24 changed files with 1482 additions and 986 deletions

View File

@ -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;
}
});
}

View File

@ -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; }
}

View File

@ -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;
}

View File

@ -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);

View File

@ -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(); }
}

View File

@ -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);

View File

@ -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;

View File

@ -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(<{

View File

@ -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}".`);
}

View File

@ -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;
}
}

View File

@ -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;
});

View File

@ -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;
}
}

View File

@ -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);
}
})

View File

@ -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; });
}

View 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;
}