parent
96e34c1d36
commit
ac6227e434
|
@ -19,7 +19,8 @@ export * from './src/router/route_config_decorator';
|
|||
export * from './src/router/route_definition';
|
||||
export {OnActivate, OnDeactivate, OnReuse, CanDeactivate, CanReuse} from './src/router/interfaces';
|
||||
export {CanActivate} from './src/router/lifecycle_annotations';
|
||||
export {Instruction} from './src/router/instruction';
|
||||
export {Instruction, ComponentInstruction} from './src/router/instruction';
|
||||
export {Url} from './src/router/url_parser';
|
||||
|
||||
import {LocationStrategy} from './src/router/location_strategy';
|
||||
import {HTML5LocationStrategy} from './src/router/html5_location_strategy';
|
||||
|
|
|
@ -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; });
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -30,7 +30,13 @@ import {
|
|||
import {RootRouter} from 'angular2/src/router/router';
|
||||
import {Pipeline} from 'angular2/src/router/pipeline';
|
||||
import {Router, RouterOutlet, RouterLink, RouteParams} from 'angular2/router';
|
||||
import {RouteConfig, Route, AsyncRoute, Redirect} from 'angular2/src/router/route_config_decorator';
|
||||
import {
|
||||
RouteConfig,
|
||||
Route,
|
||||
AuxRoute,
|
||||
AsyncRoute,
|
||||
Redirect
|
||||
} from 'angular2/src/router/route_config_decorator';
|
||||
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
|
||||
|
@ -45,10 +51,12 @@ import {
|
|||
CanReuse
|
||||
} from 'angular2/src/router/interfaces';
|
||||
import {CanActivate} from 'angular2/src/router/lifecycle_annotations';
|
||||
import {Instruction} from 'angular2/src/router/instruction';
|
||||
import {ComponentInstruction} from 'angular2/src/router/instruction';
|
||||
import {DirectiveResolver} from 'angular2/src/core/compiler/directive_resolver';
|
||||
|
||||
var cmpInstanceCount, log, eventBus;
|
||||
var cmpInstanceCount;
|
||||
var log: List<string>;
|
||||
var eventBus: EventEmitter;
|
||||
var completer: PromiseCompleter<any>;
|
||||
|
||||
export function main() {
|
||||
|
@ -73,7 +81,7 @@ export function main() {
|
|||
rtr = router;
|
||||
location = loc;
|
||||
cmpInstanceCount = 0;
|
||||
log = '';
|
||||
log = [];
|
||||
eventBus = new EventEmitter();
|
||||
}));
|
||||
|
||||
|
@ -207,7 +215,6 @@ export function main() {
|
|||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should generate link hrefs from a child to its sibling',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
|
@ -247,281 +254,299 @@ export function main() {
|
|||
});
|
||||
}));
|
||||
|
||||
it('should call the onActivate hook', inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/on-activate'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('activate cmp');
|
||||
expect(log).toEqual('activate: null -> /on-activate;');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should wait for a parent component\'s onActivate hook to resolve before calling its child\'s',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => {
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
if (ev.startsWith('parent activate')) {
|
||||
completer.resolve(true);
|
||||
}
|
||||
describe('lifecycle hooks', () => {
|
||||
it('should call the onActivate hook', inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/on-activate'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('activate cmp');
|
||||
expect(log).toEqual(['activate: null -> /on-activate']);
|
||||
async.done();
|
||||
});
|
||||
rtr.navigate('/parent-activate/child-activate')
|
||||
.then((_) => {
|
||||
}));
|
||||
|
||||
it('should wait for a parent component\'s onActivate hook to resolve before calling its child\'s',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => {
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
if (ev.startsWith('parent activate')) {
|
||||
completer.resolve(true);
|
||||
}
|
||||
});
|
||||
rtr.navigate('/parent-activate/child-activate')
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('parent {activate cmp}');
|
||||
expect(log).toEqual([
|
||||
'parent activate: null -> /parent-activate',
|
||||
'activate: null -> /child-activate'
|
||||
]);
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should call the onDeactivate hook', inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/on-deactivate'))
|
||||
.then((_) => rtr.navigate('/a'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('A');
|
||||
expect(log).toEqual(['deactivate: /on-deactivate -> /a']);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should wait for a child component\'s onDeactivate hook to resolve before calling its parent\'s',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/parent-deactivate/child-deactivate'))
|
||||
.then((_) => {
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
if (ev.startsWith('deactivate')) {
|
||||
completer.resolve(true);
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('parent {activate cmp}');
|
||||
expect(log).toEqual(
|
||||
'parent activate: null -> /parent-activate/child-activate;activate: null -> /child-activate;');
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should call the onDeactivate hook', inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/on-deactivate'))
|
||||
.then((_) => rtr.navigate('/a'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('A');
|
||||
expect(log).toEqual('deactivate: /on-deactivate -> /a;');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should wait for a child component\'s onDeactivate hook to resolve before calling its parent\'s',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/parent-deactivate/child-deactivate'))
|
||||
.then((_) => {
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
if (ev.startsWith('deactivate')) {
|
||||
completer.resolve(true);
|
||||
expect(rootTC.nativeElement).toHaveText('parent {deactivate cmp}');
|
||||
}
|
||||
});
|
||||
rtr.navigate('/a').then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('parent {deactivate cmp}');
|
||||
}
|
||||
expect(rootTC.nativeElement).toHaveText('A');
|
||||
expect(log).toEqual([
|
||||
'deactivate: /child-deactivate -> null',
|
||||
'parent deactivate: /parent-deactivate -> /a'
|
||||
]);
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
rtr.navigate('/a').then((_) => {
|
||||
}));
|
||||
|
||||
it('should reuse a component when the canReuse hook returns true',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/on-reuse/1/a'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('A');
|
||||
expect(log).toEqual(
|
||||
'deactivate: /child-deactivate -> null;parent deactivate: /parent-deactivate/child-deactivate -> /a;');
|
||||
expect(log).toEqual([]);
|
||||
expect(rootTC.nativeElement).toHaveText('reuse {A}');
|
||||
expect(cmpInstanceCount).toBe(1);
|
||||
})
|
||||
.then((_) => rtr.navigate('/on-reuse/2/b'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(log).toEqual(['reuse: /on-reuse/1 -> /on-reuse/2']);
|
||||
expect(rootTC.nativeElement).toHaveText('reuse {B}');
|
||||
expect(cmpInstanceCount).toBe(1);
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should reuse a component when the canReuse hook returns false',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/on-reuse/1/a'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(log).toEqual('');
|
||||
expect(rootTC.nativeElement).toHaveText('reuse {A}');
|
||||
expect(cmpInstanceCount).toBe(1);
|
||||
})
|
||||
.then((_) => rtr.navigate('/on-reuse/2/b'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(log).toEqual('reuse: /on-reuse/1/a -> /on-reuse/2/b;');
|
||||
expect(rootTC.nativeElement).toHaveText('reuse {B}');
|
||||
expect(cmpInstanceCount).toBe(1);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
}));
|
||||
|
||||
|
||||
it('should not reuse a component when the canReuse hook returns false',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/never-reuse/1/a'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(log).toEqual('');
|
||||
expect(rootTC.nativeElement).toHaveText('reuse {A}');
|
||||
expect(cmpInstanceCount).toBe(1);
|
||||
})
|
||||
.then((_) => rtr.navigate('/never-reuse/2/b'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(log).toEqual('');
|
||||
expect(rootTC.nativeElement).toHaveText('reuse {B}');
|
||||
expect(cmpInstanceCount).toBe(2);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should navigate when canActivate returns true', inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => {
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
if (ev.startsWith('canActivate')) {
|
||||
completer.resolve(true);
|
||||
}
|
||||
});
|
||||
rtr.navigate('/can-activate/a')
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('canActivate {A}');
|
||||
expect(log).toEqual('canActivate: null -> /can-activate/a;');
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should not navigate when canActivate returns false',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => {
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
if (ev.startsWith('canActivate')) {
|
||||
completer.resolve(false);
|
||||
}
|
||||
});
|
||||
rtr.navigate('/can-activate/a')
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('');
|
||||
expect(log).toEqual('canActivate: null -> /can-activate/a;');
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should navigate away when canDeactivate returns true',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/can-deactivate/a'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('canDeactivate {A}');
|
||||
expect(log).toEqual('');
|
||||
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
if (ev.startsWith('canDeactivate')) {
|
||||
completer.resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
rtr.navigate('/a').then((_) => {
|
||||
it('should not reuse a component when the canReuse hook returns false',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/never-reuse/1/a'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('A');
|
||||
expect(log).toEqual('canDeactivate: /can-deactivate/a -> /a;');
|
||||
expect(log).toEqual([]);
|
||||
expect(rootTC.nativeElement).toHaveText('reuse {A}');
|
||||
expect(cmpInstanceCount).toBe(1);
|
||||
})
|
||||
.then((_) => rtr.navigate('/never-reuse/2/b'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(log).toEqual([]);
|
||||
expect(rootTC.nativeElement).toHaveText('reuse {B}');
|
||||
expect(cmpInstanceCount).toBe(2);
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
}));
|
||||
}));
|
||||
|
||||
it('should not navigate away when canDeactivate returns false',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/can-deactivate/a'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('canDeactivate {A}');
|
||||
expect(log).toEqual('');
|
||||
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
if (ev.startsWith('canDeactivate')) {
|
||||
completer.resolve(false);
|
||||
}
|
||||
it('should navigate when canActivate returns true', inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => {
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
if (ev.startsWith('canActivate')) {
|
||||
completer.resolve(true);
|
||||
}
|
||||
});
|
||||
rtr.navigate('/can-activate/a')
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('canActivate {A}');
|
||||
expect(log).toEqual(['canActivate: null -> /can-activate']);
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
rtr.navigate('/a').then((_) => {
|
||||
it('should not navigate when canActivate returns false',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => {
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
if (ev.startsWith('canActivate')) {
|
||||
completer.resolve(false);
|
||||
}
|
||||
});
|
||||
rtr.navigate('/can-activate/a')
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('');
|
||||
expect(log).toEqual(['canActivate: null -> /can-activate']);
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should navigate away when canDeactivate returns true',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/can-deactivate/a'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('canDeactivate {A}');
|
||||
expect(log).toEqual('canDeactivate: /can-deactivate/a -> /a;');
|
||||
expect(log).toEqual([]);
|
||||
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
if (ev.startsWith('canDeactivate')) {
|
||||
completer.resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
rtr.navigate('/a').then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('A');
|
||||
expect(log).toEqual(['canDeactivate: /can-deactivate -> /a']);
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should not navigate away when canDeactivate returns false',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/can-deactivate/a'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('canDeactivate {A}');
|
||||
expect(log).toEqual([]);
|
||||
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
if (ev.startsWith('canDeactivate')) {
|
||||
completer.resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
rtr.navigate('/a').then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('canDeactivate {A}');
|
||||
expect(log).toEqual(['canDeactivate: /can-deactivate -> /a']);
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should run activation and deactivation hooks in the correct order',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/activation-hooks/child'))
|
||||
.then((_) => {
|
||||
expect(log).toEqual([
|
||||
'canActivate child: null -> /child',
|
||||
'canActivate parent: null -> /activation-hooks',
|
||||
'onActivate parent: null -> /activation-hooks',
|
||||
'onActivate child: null -> /child'
|
||||
]);
|
||||
|
||||
log = [];
|
||||
return rtr.navigate('/a');
|
||||
})
|
||||
.then((_) => {
|
||||
expect(log).toEqual([
|
||||
'canDeactivate parent: /activation-hooks -> /a',
|
||||
'canDeactivate child: /child -> null',
|
||||
'onDeactivate child: /child -> null',
|
||||
'onDeactivate parent: /activation-hooks -> /a'
|
||||
]);
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
}));
|
||||
}));
|
||||
|
||||
it('should only run reuse hooks when reusing', inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/reuse-hooks/1'))
|
||||
.then((_) => {
|
||||
expect(log).toEqual(
|
||||
['canActivate: null -> /reuse-hooks/1', 'onActivate: null -> /reuse-hooks/1']);
|
||||
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
if (ev.startsWith('canReuse')) {
|
||||
completer.resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
it('should run activation and deactivation hooks in the correct order',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/activation-hooks/child'))
|
||||
.then((_) => {
|
||||
expect(log).toEqual('canActivate child: null -> /child;' +
|
||||
'canActivate parent: null -> /activation-hooks/child;' +
|
||||
'onActivate parent: null -> /activation-hooks/child;' +
|
||||
'onActivate child: null -> /child;');
|
||||
|
||||
log = '';
|
||||
return rtr.navigate('/a');
|
||||
})
|
||||
.then((_) => {
|
||||
expect(log).toEqual('canDeactivate parent: /activation-hooks/child -> /a;' +
|
||||
'canDeactivate child: /child -> null;' +
|
||||
'onDeactivate child: /child -> null;' +
|
||||
'onDeactivate parent: /activation-hooks/child -> /a;');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should only run reuse hooks when reusing', inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/reuse-hooks/1'))
|
||||
.then((_) => {
|
||||
expect(log).toEqual('canActivate: null -> /reuse-hooks/1;' +
|
||||
'onActivate: null -> /reuse-hooks/1;');
|
||||
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
if (ev.startsWith('canReuse')) {
|
||||
completer.resolve(true);
|
||||
}
|
||||
log = [];
|
||||
return rtr.navigate('/reuse-hooks/2');
|
||||
})
|
||||
.then((_) => {
|
||||
expect(log).toEqual([
|
||||
'canReuse: /reuse-hooks/1 -> /reuse-hooks/2',
|
||||
'onReuse: /reuse-hooks/1 -> /reuse-hooks/2'
|
||||
]);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
log = '';
|
||||
return rtr.navigate('/reuse-hooks/2');
|
||||
})
|
||||
.then((_) => {
|
||||
expect(log).toEqual('canReuse: /reuse-hooks/1 -> /reuse-hooks/2;' +
|
||||
'onReuse: /reuse-hooks/1 -> /reuse-hooks/2;');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
it('should not run reuse hooks when not reusing', inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/reuse-hooks/1'))
|
||||
.then((_) => {
|
||||
expect(log).toEqual(
|
||||
['canActivate: null -> /reuse-hooks/1', 'onActivate: null -> /reuse-hooks/1']);
|
||||
|
||||
it('should not run reuse hooks when not reusing', inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigate('/reuse-hooks/1'))
|
||||
.then((_) => {
|
||||
expect(log).toEqual('canActivate: null -> /reuse-hooks/1;' +
|
||||
'onActivate: null -> /reuse-hooks/1;');
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
if (ev.startsWith('canReuse')) {
|
||||
completer.resolve(false);
|
||||
}
|
||||
});
|
||||
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
if (ev.startsWith('canReuse')) {
|
||||
completer.resolve(false);
|
||||
}
|
||||
log = [];
|
||||
return rtr.navigate('/reuse-hooks/2');
|
||||
})
|
||||
.then((_) => {
|
||||
expect(log).toEqual([
|
||||
'canReuse: /reuse-hooks/1 -> /reuse-hooks/2',
|
||||
'canActivate: /reuse-hooks/1 -> /reuse-hooks/2',
|
||||
'canDeactivate: /reuse-hooks/1 -> /reuse-hooks/2',
|
||||
'onDeactivate: /reuse-hooks/1 -> /reuse-hooks/2',
|
||||
'onActivate: /reuse-hooks/1 -> /reuse-hooks/2'
|
||||
]);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
log = '';
|
||||
return rtr.navigate('/reuse-hooks/2');
|
||||
})
|
||||
.then((_) => {
|
||||
expect(log).toEqual('canReuse: /reuse-hooks/1 -> /reuse-hooks/2;' +
|
||||
'canActivate: /reuse-hooks/1 -> /reuse-hooks/2;' +
|
||||
'canDeactivate: /reuse-hooks/1 -> /reuse-hooks/2;' +
|
||||
'onDeactivate: /reuse-hooks/1 -> /reuse-hooks/2;' +
|
||||
'onActivate: /reuse-hooks/1 -> /reuse-hooks/2;');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('when clicked', () => {
|
||||
|
||||
|
@ -572,6 +597,19 @@ export function main() {
|
|||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('auxillary routes', () => {
|
||||
it('should recognize a simple case', inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: AuxCmp})]))
|
||||
.then((_) => rtr.navigate('/hello(modal)'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.nativeElement).toHaveText('main {hello} | aux {modal}');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -657,24 +695,26 @@ class MyComp {
|
|||
name;
|
||||
}
|
||||
|
||||
function logHook(name: string, next: Instruction, prev: Instruction) {
|
||||
var message = name + ': ' + (isPresent(prev) ? prev.accumulatedUrl : 'null') + ' -> ' +
|
||||
(isPresent(next) ? next.accumulatedUrl : 'null') + ';';
|
||||
log += message;
|
||||
function logHook(name: string, next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
var message = name + ': ' + (isPresent(prev) ? ('/' + prev.urlPath) : 'null') + ' -> ' +
|
||||
(isPresent(next) ? ('/' + next.urlPath) : 'null');
|
||||
log.push(message);
|
||||
ObservableWrapper.callNext(eventBus, message);
|
||||
}
|
||||
|
||||
@Component({selector: 'activate-cmp'})
|
||||
@View({template: 'activate cmp'})
|
||||
class ActivateCmp implements OnActivate {
|
||||
onActivate(next: Instruction, prev: Instruction) { logHook('activate', next, prev); }
|
||||
onActivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('activate', next, prev);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'parent-activate-cmp'})
|
||||
@View({template: `parent {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
|
||||
@RouteConfig([new Route({path: '/child-activate', component: ActivateCmp})])
|
||||
class ParentActivateCmp implements OnActivate {
|
||||
onActivate(next: Instruction, prev: Instruction): Promise<any> {
|
||||
onActivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<any> {
|
||||
completer = PromiseWrapper.completer();
|
||||
logHook('parent activate', next, prev);
|
||||
return completer.promise;
|
||||
|
@ -684,13 +724,15 @@ class ParentActivateCmp implements OnActivate {
|
|||
@Component({selector: 'deactivate-cmp'})
|
||||
@View({template: 'deactivate cmp'})
|
||||
class DeactivateCmp implements OnDeactivate {
|
||||
onDeactivate(next: Instruction, prev: Instruction) { logHook('deactivate', next, prev); }
|
||||
onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('deactivate', next, prev);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'deactivate-cmp'})
|
||||
@View({template: 'deactivate cmp'})
|
||||
class WaitDeactivateCmp implements OnDeactivate {
|
||||
onDeactivate(next: Instruction, prev: Instruction): Promise<any> {
|
||||
onDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<any> {
|
||||
completer = PromiseWrapper.completer();
|
||||
logHook('deactivate', next, prev);
|
||||
return completer.promise;
|
||||
|
@ -701,7 +743,9 @@ class WaitDeactivateCmp implements OnDeactivate {
|
|||
@View({template: `parent {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
|
||||
@RouteConfig([new Route({path: '/child-deactivate', component: WaitDeactivateCmp})])
|
||||
class ParentDeactivateCmp implements OnDeactivate {
|
||||
onDeactivate(next: Instruction, prev: Instruction) { logHook('parent deactivate', next, prev); }
|
||||
onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('parent deactivate', next, prev);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'reuse-cmp'})
|
||||
|
@ -709,8 +753,8 @@ class ParentDeactivateCmp implements OnDeactivate {
|
|||
@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})])
|
||||
class ReuseCmp implements OnReuse, CanReuse {
|
||||
constructor() { cmpInstanceCount += 1; }
|
||||
canReuse(next: Instruction, prev: Instruction) { return true; }
|
||||
onReuse(next: Instruction, prev: Instruction) { logHook('reuse', next, prev); }
|
||||
canReuse(next: ComponentInstruction, prev: ComponentInstruction) { return true; }
|
||||
onReuse(next: ComponentInstruction, prev: ComponentInstruction) { logHook('reuse', next, prev); }
|
||||
}
|
||||
|
||||
@Component({selector: 'never-reuse-cmp'})
|
||||
|
@ -718,8 +762,8 @@ class ReuseCmp implements OnReuse, CanReuse {
|
|||
@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})])
|
||||
class NeverReuseCmp implements OnReuse, CanReuse {
|
||||
constructor() { cmpInstanceCount += 1; }
|
||||
canReuse(next: Instruction, prev: Instruction) { return false; }
|
||||
onReuse(next: Instruction, prev: Instruction) { logHook('reuse', next, prev); }
|
||||
canReuse(next: ComponentInstruction, prev: ComponentInstruction) { return false; }
|
||||
onReuse(next: ComponentInstruction, prev: ComponentInstruction) { logHook('reuse', next, prev); }
|
||||
}
|
||||
|
||||
@Component({selector: 'can-activate-cmp'})
|
||||
|
@ -727,7 +771,7 @@ class NeverReuseCmp implements OnReuse, CanReuse {
|
|||
@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})])
|
||||
@CanActivate(CanActivateCmp.canActivate)
|
||||
class CanActivateCmp {
|
||||
static canActivate(next: Instruction, prev: Instruction) {
|
||||
static canActivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<boolean> {
|
||||
completer = PromiseWrapper.completer();
|
||||
logHook('canActivate', next, prev);
|
||||
return completer.promise;
|
||||
|
@ -738,7 +782,7 @@ class CanActivateCmp {
|
|||
@View({template: `canDeactivate {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
|
||||
@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})])
|
||||
class CanDeactivateCmp implements CanDeactivate {
|
||||
canDeactivate(next: Instruction, prev: Instruction) {
|
||||
canDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<boolean> {
|
||||
completer = PromiseWrapper.completer();
|
||||
logHook('canDeactivate', next, prev);
|
||||
return completer.promise;
|
||||
|
@ -749,19 +793,23 @@ class CanDeactivateCmp implements CanDeactivate {
|
|||
@View({template: `child`})
|
||||
@CanActivate(AllHooksChildCmp.canActivate)
|
||||
class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate {
|
||||
canDeactivate(next: Instruction, prev: Instruction) {
|
||||
canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('canDeactivate child', next, prev);
|
||||
return true;
|
||||
}
|
||||
|
||||
onDeactivate(next: Instruction, prev: Instruction) { logHook('onDeactivate child', next, prev); }
|
||||
onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('onDeactivate child', next, prev);
|
||||
}
|
||||
|
||||
static canActivate(next: Instruction, prev: Instruction) {
|
||||
static canActivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('canActivate child', next, prev);
|
||||
return true;
|
||||
}
|
||||
|
||||
onActivate(next: Instruction, prev: Instruction) { logHook('onActivate child', next, prev); }
|
||||
onActivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('onActivate child', next, prev);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'all-hooks-parent-cmp'})
|
||||
|
@ -769,46 +817,56 @@ class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate {
|
|||
@RouteConfig([new Route({path: '/child', component: AllHooksChildCmp})])
|
||||
@CanActivate(AllHooksParentCmp.canActivate)
|
||||
class AllHooksParentCmp implements CanDeactivate, OnDeactivate, OnActivate {
|
||||
canDeactivate(next: Instruction, prev: Instruction) {
|
||||
canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('canDeactivate parent', next, prev);
|
||||
return true;
|
||||
}
|
||||
|
||||
onDeactivate(next: Instruction, prev: Instruction) { logHook('onDeactivate parent', next, prev); }
|
||||
onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('onDeactivate parent', next, prev);
|
||||
}
|
||||
|
||||
static canActivate(next: Instruction, prev: Instruction) {
|
||||
static canActivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('canActivate parent', next, prev);
|
||||
return true;
|
||||
}
|
||||
|
||||
onActivate(next: Instruction, prev: Instruction) { logHook('onActivate parent', next, prev); }
|
||||
onActivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('onActivate parent', next, prev);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'reuse-hooks-cmp'})
|
||||
@View({template: 'reuse hooks cmp'})
|
||||
@CanActivate(ReuseHooksCmp.canActivate)
|
||||
class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanDeactivate {
|
||||
canReuse(next: Instruction, prev: Instruction): Promise<any> {
|
||||
canReuse(next: ComponentInstruction, prev: ComponentInstruction): Promise<any> {
|
||||
completer = PromiseWrapper.completer();
|
||||
logHook('canReuse', next, prev);
|
||||
return completer.promise;
|
||||
}
|
||||
|
||||
onReuse(next: Instruction, prev: Instruction) { logHook('onReuse', next, prev); }
|
||||
onReuse(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('onReuse', next, prev);
|
||||
}
|
||||
|
||||
canDeactivate(next: Instruction, prev: Instruction) {
|
||||
canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('canDeactivate', next, prev);
|
||||
return true;
|
||||
}
|
||||
|
||||
onDeactivate(next: Instruction, prev: Instruction) { logHook('onDeactivate', next, prev); }
|
||||
onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('onDeactivate', next, prev);
|
||||
}
|
||||
|
||||
static canActivate(next: Instruction, prev: Instruction) {
|
||||
static canActivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('canActivate', next, prev);
|
||||
return true;
|
||||
}
|
||||
|
||||
onActivate(next: Instruction, prev: Instruction) { logHook('onActivate', next, prev); }
|
||||
onActivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('onActivate', next, prev);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'lifecycle-cmp'})
|
||||
|
@ -828,3 +886,21 @@ class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanD
|
|||
])
|
||||
class LifecycleCmp {
|
||||
}
|
||||
|
||||
@Component({selector: 'modal-cmp'})
|
||||
@View({template: "modal"})
|
||||
class ModalCmp {
|
||||
}
|
||||
|
||||
@Component({selector: 'aux-cmp'})
|
||||
@View({
|
||||
template:
|
||||
`main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`,
|
||||
directives: [RouterOutlet]
|
||||
})
|
||||
@RouteConfig([
|
||||
new Route({path: '/hello', component: HelloCmp}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp})
|
||||
])
|
||||
class AuxCmp {
|
||||
}
|
||||
|
|
|
@ -11,6 +11,7 @@ import {
|
|||
} from 'angular2/test_lib';
|
||||
|
||||
import {PathRecognizer} from 'angular2/src/router/path_recognizer';
|
||||
import {parser, Url, RootUrl} from 'angular2/src/router/url_parser';
|
||||
import {SyncRouteHandler} from 'angular2/src/router/sync_route_handler';
|
||||
|
||||
class DummyClass {
|
||||
|
@ -41,65 +42,60 @@ export function main() {
|
|||
|
||||
describe('querystring params', () => {
|
||||
it('should parse querystring params so long as the recognizer is a root', () => {
|
||||
var rec = new PathRecognizer('/hello/there', mockRouteHandler, true);
|
||||
var params = rec.parseParams('/hello/there?name=igor');
|
||||
expect(params).toEqual({'name': 'igor'});
|
||||
var rec = new PathRecognizer('/hello/there', mockRouteHandler);
|
||||
var url = parser.parse('/hello/there?name=igor');
|
||||
var match = rec.recognize(url);
|
||||
expect(match.instruction.params).toEqual({'name': 'igor'});
|
||||
});
|
||||
|
||||
it('should return a combined map of parameters with the param expected in the URL path',
|
||||
() => {
|
||||
var rec = new PathRecognizer('/hello/:name', mockRouteHandler, true);
|
||||
var params = rec.parseParams('/hello/paul?topic=success');
|
||||
expect(params).toEqual({'name': 'paul', 'topic': 'success'});
|
||||
var rec = new PathRecognizer('/hello/:name', mockRouteHandler);
|
||||
var url = parser.parse('/hello/paul?topic=success');
|
||||
var match = rec.recognize(url);
|
||||
expect(match.instruction.params).toEqual({'name': 'paul', 'topic': 'success'});
|
||||
});
|
||||
});
|
||||
|
||||
describe('matrix params', () => {
|
||||
it('should recognize a trailing matrix value on a path value and assign it to the params return value',
|
||||
() => {
|
||||
var rec = new PathRecognizer('/hello/:id', mockRouteHandler);
|
||||
var params = rec.parseParams('/hello/matias;key=value');
|
||||
|
||||
expect(params['id']).toEqual('matias');
|
||||
expect(params['key']).toEqual('value');
|
||||
});
|
||||
|
||||
it('should recognize and parse multiple matrix params separated by a colon value', () => {
|
||||
var rec = new PathRecognizer('/jello/:sid', mockRouteHandler);
|
||||
var params = rec.parseParams('/jello/man;color=red;height=20');
|
||||
|
||||
expect(params['sid']).toEqual('man');
|
||||
expect(params['color']).toEqual('red');
|
||||
expect(params['height']).toEqual('20');
|
||||
it('should be parsed along with dynamic paths', () => {
|
||||
var rec = new PathRecognizer('/hello/:id', mockRouteHandler);
|
||||
var url = new Url('hello', new Url('matias', null, null, {'key': 'value'}));
|
||||
var match = rec.recognize(url);
|
||||
expect(match.instruction.params).toEqual({'id': 'matias', 'key': 'value'});
|
||||
});
|
||||
|
||||
it('should recognize a matrix param value on a static path value', () => {
|
||||
var rec = new PathRecognizer('/static/man', mockRouteHandler);
|
||||
var params = rec.parseParams('/static/man;name=dave');
|
||||
expect(params['name']).toEqual('dave');
|
||||
it('should be parsed on a static path', () => {
|
||||
var rec = new PathRecognizer('/person', mockRouteHandler);
|
||||
var url = new Url('person', null, null, {'name': 'dave'});
|
||||
var match = rec.recognize(url);
|
||||
expect(match.instruction.params).toEqual({'name': 'dave'});
|
||||
});
|
||||
|
||||
it('should not parse matrix params when a wildcard segment is used', () => {
|
||||
it('should be ignored on a wildcard segment', () => {
|
||||
var rec = new PathRecognizer('/wild/*everything', mockRouteHandler);
|
||||
var params = rec.parseParams('/wild/super;variable=value');
|
||||
expect(params['everything']).toEqual('super;variable=value');
|
||||
var url = parser.parse('/wild/super;variable=value');
|
||||
var match = rec.recognize(url);
|
||||
expect(match.instruction.params).toEqual({'everything': 'super;variable=value'});
|
||||
});
|
||||
|
||||
it('should set matrix param values to true when no value is present within the path string',
|
||||
() => {
|
||||
var rec = new PathRecognizer('/path', mockRouteHandler);
|
||||
var params = rec.parseParams('/path;one;two;three=3');
|
||||
expect(params['one']).toEqual(true);
|
||||
expect(params['two']).toEqual(true);
|
||||
expect(params['three']).toEqual('3');
|
||||
});
|
||||
it('should set matrix param values to true when no value is present', () => {
|
||||
var rec = new PathRecognizer('/path', mockRouteHandler);
|
||||
var url = new Url('path', null, null, {'one': true, 'two': true, 'three': '3'});
|
||||
var match = rec.recognize(url);
|
||||
expect(match.instruction.params).toEqual({'one': true, 'two': true, 'three': '3'});
|
||||
});
|
||||
|
||||
it('should ignore earlier instances of matrix params and only consider the ones at the end of the path',
|
||||
() => {
|
||||
var rec = new PathRecognizer('/one/two/three', mockRouteHandler);
|
||||
var params = rec.parseParams('/one;a=1/two;b=2/three;c=3');
|
||||
expect(params).toEqual({'c': '3'});
|
||||
});
|
||||
it('should be parsed on the final segment of the path', () => {
|
||||
var rec = new PathRecognizer('/one/two/three', mockRouteHandler);
|
||||
|
||||
var three = new Url('three', null, null, {'c': '3'});
|
||||
var two = new Url('two', three, null, {'b': '2'});
|
||||
var one = new Url('one', two, null, {'a': '1'});
|
||||
|
||||
var match = rec.recognize(one);
|
||||
expect(match.instruction.params).toEqual({'c': '3'});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -98,7 +98,31 @@ export function main() {
|
|||
});
|
||||
}));
|
||||
|
||||
// TODO: test apps with wrong configs
|
||||
it('should throw if a config is missing a target',
|
||||
inject(
|
||||
[AsyncTestCompleter],
|
||||
(async) => {
|
||||
bootstrap(WrongConfigCmp, testBindings)
|
||||
.catch((e) => {
|
||||
expect(e.originalException)
|
||||
.toContainError(
|
||||
'Route config should contain exactly one "component", "loader", or "redirectTo" property.');
|
||||
async.done();
|
||||
return null;
|
||||
})}));
|
||||
|
||||
it('should throw if a config has an invalid component type',
|
||||
inject(
|
||||
[AsyncTestCompleter],
|
||||
(async) => {
|
||||
bootstrap(WrongComponentTypeCmp, testBindings)
|
||||
.catch((e) => {
|
||||
expect(e.originalException)
|
||||
.toContainError(
|
||||
'Invalid component type "intentionallyWrongComponentType". Valid types are "constructor" and "loader".');
|
||||
async.done();
|
||||
return null;
|
||||
})}));
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -149,3 +173,17 @@ class ParentCmp {
|
|||
class HierarchyAppCmp {
|
||||
constructor(public router: Router, public location: LocationStrategy) {}
|
||||
}
|
||||
|
||||
@Component({selector: 'app-cmp'})
|
||||
@View({template: `root { <router-outlet></router-outlet> }`, directives: routerDirectives})
|
||||
@RouteConfig([{path: '/hello'}])
|
||||
class WrongConfigCmp {
|
||||
}
|
||||
|
||||
@Component({selector: 'app-cmp'})
|
||||
@View({template: `root { <router-outlet></router-outlet> }`, directives: routerDirectives})
|
||||
@RouteConfig([
|
||||
{path: '/hello', component: {type: 'intentionallyWrongComponentType', constructor: HelloCmp}},
|
||||
])
|
||||
class WrongComponentTypeCmp {
|
||||
}
|
||||
|
|
|
@ -12,9 +12,11 @@ import {
|
|||
|
||||
import {Map, StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {RouteRecognizer, RouteMatch} from 'angular2/src/router/route_recognizer';
|
||||
import {RouteRecognizer} from 'angular2/src/router/route_recognizer';
|
||||
import {ComponentInstruction} from 'angular2/src/router/instruction';
|
||||
|
||||
import {Route, Redirect} from 'angular2/src/router/route_config_decorator';
|
||||
import {parser} from 'angular2/src/router/url_parser';
|
||||
|
||||
export function main() {
|
||||
describe('RouteRecognizer', () => {
|
||||
|
@ -25,31 +27,31 @@ export function main() {
|
|||
|
||||
it('should recognize a static segment', () => {
|
||||
recognizer.config(new Route({path: '/test', component: DummyCmpA}));
|
||||
var solution = recognizer.recognize('/test')[0];
|
||||
var solution = recognize(recognizer, '/test');
|
||||
expect(getComponentType(solution)).toEqual(DummyCmpA);
|
||||
});
|
||||
|
||||
|
||||
it('should recognize a single slash', () => {
|
||||
recognizer.config(new Route({path: '/', component: DummyCmpA}));
|
||||
var solution = recognizer.recognize('/')[0];
|
||||
var solution = recognize(recognizer, '/');
|
||||
expect(getComponentType(solution)).toEqual(DummyCmpA);
|
||||
});
|
||||
|
||||
|
||||
it('should recognize a dynamic segment', () => {
|
||||
recognizer.config(new Route({path: '/user/:name', component: DummyCmpA}));
|
||||
var solution = recognizer.recognize('/user/brian')[0];
|
||||
var solution = recognize(recognizer, '/user/brian');
|
||||
expect(getComponentType(solution)).toEqual(DummyCmpA);
|
||||
expect(solution.params()).toEqual({'name': 'brian'});
|
||||
expect(solution.params).toEqual({'name': 'brian'});
|
||||
});
|
||||
|
||||
|
||||
it('should recognize a star segment', () => {
|
||||
recognizer.config(new Route({path: '/first/*rest', component: DummyCmpA}));
|
||||
var solution = recognizer.recognize('/first/second/third')[0];
|
||||
var solution = recognize(recognizer, '/first/second/third');
|
||||
expect(getComponentType(solution)).toEqual(DummyCmpA);
|
||||
expect(solution.params()).toEqual({'rest': 'second/third'});
|
||||
expect(solution.params).toEqual({'rest': 'second/third'});
|
||||
});
|
||||
|
||||
|
||||
|
@ -70,235 +72,105 @@ export function main() {
|
|||
|
||||
|
||||
it('should recognize redirects', () => {
|
||||
recognizer.config(new Redirect({path: '/a', redirectTo: '/b'}));
|
||||
recognizer.config(new Route({path: '/b', component: DummyCmpA}));
|
||||
var solutions = recognizer.recognize('/a');
|
||||
expect(solutions.length).toBe(1);
|
||||
|
||||
var solution = solutions[0];
|
||||
recognizer.config(new Redirect({path: '/a', redirectTo: 'b'}));
|
||||
var solution = recognize(recognizer, '/a');
|
||||
expect(getComponentType(solution)).toEqual(DummyCmpA);
|
||||
expect(solution.matchedUrl).toEqual('/b');
|
||||
expect(solution.urlPath).toEqual('b');
|
||||
});
|
||||
|
||||
|
||||
it('should not perform root URL redirect on a non-root route', () => {
|
||||
recognizer.config(new Redirect({path: '/', redirectTo: '/foo'}));
|
||||
recognizer.config(new Route({path: '/bar', component: DummyCmpA}));
|
||||
var solutions = recognizer.recognize('/bar');
|
||||
expect(solutions.length).toBe(1);
|
||||
|
||||
var solution = solutions[0];
|
||||
expect(getComponentType(solution)).toEqual(DummyCmpA);
|
||||
expect(solution.matchedUrl).toEqual('/bar');
|
||||
var solution = recognize(recognizer, '/bar');
|
||||
expect(solution.componentType).toEqual(DummyCmpA);
|
||||
expect(solution.urlPath).toEqual('bar');
|
||||
});
|
||||
|
||||
it('should perform a root URL redirect when only a slash or an empty string is being processed',
|
||||
() => {
|
||||
recognizer.config(new Redirect({path: '/', redirectTo: '/matias'}));
|
||||
recognizer.config(new Route({path: '/matias', component: DummyCmpA}));
|
||||
recognizer.config(new Route({path: '/fatias', component: DummyCmpA}));
|
||||
|
||||
var solutions;
|
||||
it('should perform a root URL redirect only for root routes', () => {
|
||||
recognizer.config(new Redirect({path: '/', redirectTo: '/matias'}));
|
||||
recognizer.config(new Route({path: '/matias', component: DummyCmpA}));
|
||||
recognizer.config(new Route({path: '/fatias', component: DummyCmpA}));
|
||||
|
||||
solutions = recognizer.recognize('/');
|
||||
expect(solutions[0].matchedUrl).toBe('/matias');
|
||||
var solution;
|
||||
|
||||
solutions = recognizer.recognize('/fatias');
|
||||
expect(solutions[0].matchedUrl).toBe('/fatias');
|
||||
solution = recognize(recognizer, '/');
|
||||
expect(solution.urlPath).toEqual('matias');
|
||||
|
||||
solution = recognize(recognizer, '/fatias');
|
||||
expect(solution.urlPath).toEqual('fatias');
|
||||
|
||||
solution = recognize(recognizer, '');
|
||||
expect(solution.urlPath).toEqual('matias');
|
||||
});
|
||||
|
||||
solutions = recognizer.recognize('');
|
||||
expect(solutions[0].matchedUrl).toBe('/matias');
|
||||
});
|
||||
|
||||
it('should generate URLs with params', () => {
|
||||
recognizer.config(new Route({path: '/app/user/:name', component: DummyCmpA, as: 'user'}));
|
||||
expect(recognizer.generate('user', {'name': 'misko'})['url']).toEqual('app/user/misko');
|
||||
var instruction = recognizer.generate('user', {'name': 'misko'});
|
||||
expect(instruction.urlPath).toEqual('app/user/misko');
|
||||
});
|
||||
|
||||
|
||||
it('should generate URLs with numeric params', () => {
|
||||
recognizer.config(new Route({path: '/app/page/:number', component: DummyCmpA, as: 'page'}));
|
||||
expect(recognizer.generate('page', {'number': 42})['url']).toEqual('app/page/42');
|
||||
expect(recognizer.generate('page', {'number': 42}).urlPath).toEqual('app/page/42');
|
||||
});
|
||||
|
||||
|
||||
it('should throw in the absence of required params URLs', () => {
|
||||
recognizer.config(new Route({path: 'app/user/:name', component: DummyCmpA, as: 'user'}));
|
||||
expect(() => recognizer.generate('user', {})['url'])
|
||||
expect(() => recognizer.generate('user', {}))
|
||||
.toThrowError('Route generator for \'name\' was not included in parameters passed.');
|
||||
});
|
||||
|
||||
describe('querystring params', () => {
|
||||
it('should recognize querystring parameters within the URL path', () => {
|
||||
var recognizer = new RouteRecognizer(true);
|
||||
recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, as: 'user'}));
|
||||
|
||||
var solution = recognizer.recognize('/profile/matsko?comments=all')[0];
|
||||
var params = solution.params();
|
||||
expect(params['name']).toEqual('matsko');
|
||||
expect(params['comments']).toEqual('all');
|
||||
describe('params', () => {
|
||||
it('should recognize parameters within the URL path', () => {
|
||||
recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, as: 'user'}));
|
||||
var solution = recognize(recognizer, '/profile/matsko?comments=all');
|
||||
expect(solution.params).toEqual({'name': 'matsko', 'comments': 'all'});
|
||||
});
|
||||
|
||||
|
||||
it('should generate and populate the given static-based route with querystring params',
|
||||
() => {
|
||||
var recognizer = new RouteRecognizer(true);
|
||||
recognizer.config(
|
||||
new Route({path: 'forum/featured', component: DummyCmpA, as: 'forum-page'}));
|
||||
|
||||
var params = StringMapWrapper.create();
|
||||
params['start'] = 10;
|
||||
params['end'] = 100;
|
||||
var params = {'start': 10, 'end': 100};
|
||||
|
||||
var result = recognizer.generate('forum-page', params);
|
||||
expect(result['url']).toEqual('forum/featured?start=10&end=100');
|
||||
expect(result.urlPath).toEqual('forum/featured');
|
||||
expect(result.urlParams).toEqual(['start=10', 'end=100']);
|
||||
});
|
||||
|
||||
it('should place a higher priority on actual route params incase the same params are defined in the querystring',
|
||||
() => {
|
||||
var recognizer = new RouteRecognizer(true);
|
||||
recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, as: 'user'}));
|
||||
|
||||
var solution = recognizer.recognize('/profile/yegor?name=igor')[0];
|
||||
var params = solution.params();
|
||||
expect(params['name']).toEqual('yegor');
|
||||
});
|
||||
|
||||
it('should strip out any occurences of matrix params when querystring params are allowed',
|
||||
() => {
|
||||
var recognizer = new RouteRecognizer(true);
|
||||
recognizer.config(new Route({path: '/home', component: DummyCmpA, as: 'user'}));
|
||||
|
||||
var solution = recognizer.recognize('/home;showAll=true;limit=100?showAll=false')[0];
|
||||
var params = solution.params();
|
||||
|
||||
expect(params['showAll']).toEqual('false');
|
||||
expect(params['limit']).toBeFalsy();
|
||||
});
|
||||
|
||||
it('should strip out any occurences of matrix params as input data', () => {
|
||||
var recognizer = new RouteRecognizer(true);
|
||||
recognizer.config(new Route({path: '/home/:subject', component: DummyCmpA, as: 'user'}));
|
||||
|
||||
var solution = recognizer.recognize('/home/zero;one=1?two=2')[0];
|
||||
var params = solution.params();
|
||||
|
||||
expect(params['subject']).toEqual('zero');
|
||||
expect(params['one']).toBeFalsy();
|
||||
expect(params['two']).toEqual('2');
|
||||
});
|
||||
});
|
||||
|
||||
describe('matrix params', () => {
|
||||
it('should recognize matrix parameters within the URL path', () => {
|
||||
var recognizer = new RouteRecognizer();
|
||||
it('should prefer positional params over query params', () => {
|
||||
recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, as: 'user'}));
|
||||
|
||||
var solution = recognizer.recognize('/profile/matsko;comments=all')[0];
|
||||
var params = solution.params();
|
||||
expect(params['name']).toEqual('matsko');
|
||||
expect(params['comments']).toEqual('all');
|
||||
var solution = recognize(recognizer, '/profile/yegor?name=igor');
|
||||
expect(solution.params).toEqual({'name': 'yegor'});
|
||||
});
|
||||
|
||||
it('should recognize multiple matrix params and set parameters that contain no value to true',
|
||||
() => {
|
||||
var recognizer = new RouteRecognizer();
|
||||
recognizer.config(new Route({path: '/profile/hello', component: DummyCmpA, as: 'user'}));
|
||||
|
||||
var solution =
|
||||
recognizer.recognize('/profile/hello;modal;showAll=true;hideAll=false')[0];
|
||||
var params = solution.params();
|
||||
|
||||
expect(params['modal']).toEqual(true);
|
||||
expect(params['showAll']).toEqual('true');
|
||||
expect(params['hideAll']).toEqual('false');
|
||||
});
|
||||
|
||||
it('should only consider the matrix parameters at the end of the path handler', () => {
|
||||
var recognizer = new RouteRecognizer();
|
||||
recognizer.config(new Route({path: '/profile/hi/:name', component: DummyCmpA, as: 'user'}));
|
||||
|
||||
var solution = recognizer.recognize('/profile;a=1/hi;b=2;c=3/william;d=4')[0];
|
||||
var params = solution.params();
|
||||
|
||||
expect(params).toEqual({'name': 'william', 'd': '4'});
|
||||
});
|
||||
|
||||
it('should generate and populate the given static-based route with matrix params', () => {
|
||||
var recognizer = new RouteRecognizer();
|
||||
recognizer.config(
|
||||
new Route({path: 'forum/featured', component: DummyCmpA, as: 'forum-page'}));
|
||||
|
||||
var params = StringMapWrapper.create();
|
||||
params['start'] = 10;
|
||||
params['end'] = 100;
|
||||
|
||||
var result = recognizer.generate('forum-page', params);
|
||||
expect(result['url']).toEqual('forum/featured;start=10;end=100');
|
||||
});
|
||||
|
||||
it('should generate and populate the given dynamic-based route with matrix params', () => {
|
||||
var recognizer = new RouteRecognizer();
|
||||
recognizer.config(
|
||||
new Route({path: 'forum/:topic', component: DummyCmpA, as: 'forum-page'}));
|
||||
|
||||
var params = StringMapWrapper.create();
|
||||
params['topic'] = 'crazy';
|
||||
params['total-posts'] = 100;
|
||||
params['moreDetail'] = null;
|
||||
|
||||
var result = recognizer.generate('forum-page', params);
|
||||
expect(result['url']).toEqual('forum/crazy;total-posts=100;moreDetail');
|
||||
});
|
||||
|
||||
it('should not apply any matrix params if a dynamic route segment takes up the slot when a path is generated',
|
||||
() => {
|
||||
var recognizer = new RouteRecognizer();
|
||||
recognizer.config(
|
||||
new Route({path: 'hello/:name', component: DummyCmpA, as: 'profile-page'}));
|
||||
|
||||
var params = StringMapWrapper.create();
|
||||
params['name'] = 'matsko';
|
||||
|
||||
var result = recognizer.generate('profile-page', params);
|
||||
expect(result['url']).toEqual('hello/matsko');
|
||||
});
|
||||
|
||||
it('should place a higher priority on actual route params incase the same params are defined in the matrix params string',
|
||||
() => {
|
||||
var recognizer = new RouteRecognizer();
|
||||
recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, as: 'user'}));
|
||||
|
||||
var solution = recognizer.recognize('/profile/yegor;name=igor')[0];
|
||||
var params = solution.params();
|
||||
expect(params['name']).toEqual('yegor');
|
||||
});
|
||||
|
||||
it('should strip out any occurences of querystring params when matrix params are allowed',
|
||||
() => {
|
||||
var recognizer = new RouteRecognizer();
|
||||
recognizer.config(new Route({path: '/home', component: DummyCmpA, as: 'user'}));
|
||||
|
||||
var solution = recognizer.recognize('/home;limit=100?limit=1000&showAll=true')[0];
|
||||
var params = solution.params();
|
||||
|
||||
expect(params['showAll']).toBeFalsy();
|
||||
expect(params['limit']).toEqual('100');
|
||||
});
|
||||
|
||||
it('should strip out any occurences of matrix params as input data', () => {
|
||||
var recognizer = new RouteRecognizer();
|
||||
it('should ignore matrix params for the top-level component', () => {
|
||||
recognizer.config(new Route({path: '/home/:subject', component: DummyCmpA, as: 'user'}));
|
||||
|
||||
var solution = recognizer.recognize('/home/zero;one=1?two=2')[0];
|
||||
var params = solution.params();
|
||||
|
||||
expect(params['subject']).toEqual('zero');
|
||||
expect(params['one']).toEqual('1');
|
||||
expect(params['two']).toBeFalsy();
|
||||
var solution = recognize(recognizer, '/home;sort=asc/zero;one=1?two=2');
|
||||
expect(solution.params).toEqual({'subject': 'zero', 'two': '2'});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function getComponentType(routeMatch: RouteMatch): any {
|
||||
return routeMatch.recognizer.handler.componentType;
|
||||
function recognize(recognizer: RouteRecognizer, url: string): ComponentInstruction {
|
||||
return recognizer.recognize(parser.parse(url))[0].instruction;
|
||||
}
|
||||
|
||||
function getComponentType(routeMatch: ComponentInstruction): any {
|
||||
return routeMatch.componentType;
|
||||
}
|
||||
|
||||
class DummyCmpA {}
|
||||
|
|
|
@ -14,6 +14,7 @@ import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
|||
|
||||
import {RouteRegistry} from 'angular2/src/router/route_registry';
|
||||
import {RouteConfig, Route, AsyncRoute} from 'angular2/src/router/route_config_decorator';
|
||||
import {stringifyInstruction} from 'angular2/src/router/instruction';
|
||||
|
||||
export function main() {
|
||||
describe('RouteRegistry', () => {
|
||||
|
@ -27,7 +28,7 @@ export function main() {
|
|||
|
||||
registry.recognize('/test', RootHostCmp)
|
||||
.then((instruction) => {
|
||||
expect(instruction.component).toBe(DummyCmpB);
|
||||
expect(instruction.component.componentType).toBe(DummyCmpB);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
@ -36,8 +37,10 @@ export function main() {
|
|||
registry.config(RootHostCmp,
|
||||
new Route({path: '/first/...', component: DummyParentCmp, as: 'firstCmp'}));
|
||||
|
||||
expect(registry.generate(['firstCmp', 'secondCmp'], RootHostCmp)).toEqual('first/second');
|
||||
expect(registry.generate(['secondCmp'], DummyParentCmp)).toEqual('second');
|
||||
expect(stringifyInstruction(registry.generate(['firstCmp', 'secondCmp'], RootHostCmp)))
|
||||
.toEqual('first/second');
|
||||
expect(stringifyInstruction(registry.generate(['secondCmp'], DummyParentCmp)))
|
||||
.toEqual('second');
|
||||
});
|
||||
|
||||
it('should generate URLs with params', () => {
|
||||
|
@ -45,8 +48,8 @@ export function main() {
|
|||
RootHostCmp,
|
||||
new Route({path: '/first/:param/...', component: DummyParentParamCmp, as: 'firstCmp'}));
|
||||
|
||||
var url =
|
||||
registry.generate(['firstCmp', {param: 'one'}, 'secondCmp', {param: 'two'}], RootHostCmp);
|
||||
var url = stringifyInstruction(registry.generate(
|
||||
['firstCmp', {param: 'one'}, 'secondCmp', {param: 'two'}], RootHostCmp));
|
||||
expect(url).toEqual('first/one/second/two');
|
||||
});
|
||||
|
||||
|
@ -61,7 +64,8 @@ export function main() {
|
|||
|
||||
registry.recognize('/first/second', RootHostCmp)
|
||||
.then((_) => {
|
||||
expect(registry.generate(['firstCmp', 'secondCmp'], RootHostCmp))
|
||||
expect(
|
||||
stringifyInstruction(registry.generate(['firstCmp', 'secondCmp'], RootHostCmp)))
|
||||
.toEqual('first/second');
|
||||
async.done();
|
||||
});
|
||||
|
@ -73,14 +77,13 @@ export function main() {
|
|||
.toThrowError('Component "RootHostCmp" has no route config.');
|
||||
});
|
||||
|
||||
|
||||
it('should prefer static segments to dynamic', inject([AsyncTestCompleter], (async) => {
|
||||
registry.config(RootHostCmp, new Route({path: '/:site', component: DummyCmpB}));
|
||||
registry.config(RootHostCmp, new Route({path: '/home', component: DummyCmpA}));
|
||||
|
||||
registry.recognize('/home', RootHostCmp)
|
||||
.then((instruction) => {
|
||||
expect(instruction.component).toBe(DummyCmpA);
|
||||
expect(instruction.component.componentType).toBe(DummyCmpA);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
@ -91,7 +94,7 @@ export function main() {
|
|||
|
||||
registry.recognize('/home', RootHostCmp)
|
||||
.then((instruction) => {
|
||||
expect(instruction.component).toBe(DummyCmpA);
|
||||
expect(instruction.component.componentType).toBe(DummyCmpA);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
@ -102,7 +105,7 @@ export function main() {
|
|||
|
||||
registry.recognize('/some/path', RootHostCmp)
|
||||
.then((instruction) => {
|
||||
expect(instruction.component).toBe(DummyCmpA);
|
||||
expect(instruction.component.componentType).toBe(DummyCmpA);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
@ -113,7 +116,7 @@ export function main() {
|
|||
|
||||
registry.recognize('/first/second', RootHostCmp)
|
||||
.then((instruction) => {
|
||||
expect(instruction.component).toBe(DummyCmpA);
|
||||
expect(instruction.component.componentType).toBe(DummyCmpA);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
@ -127,7 +130,7 @@ export function main() {
|
|||
|
||||
registry.recognize('/first/second/third', RootHostCmp)
|
||||
.then((instruction) => {
|
||||
expect(instruction.component).toBe(DummyCmpB);
|
||||
expect(instruction.component.componentType).toBe(DummyCmpB);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
@ -137,8 +140,8 @@ export function main() {
|
|||
|
||||
registry.recognize('/first/second', RootHostCmp)
|
||||
.then((instruction) => {
|
||||
expect(instruction.component).toBe(DummyParentCmp);
|
||||
expect(instruction.child.component).toBe(DummyCmpB);
|
||||
expect(instruction.component.componentType).toBe(DummyParentCmp);
|
||||
expect(instruction.child.component.componentType).toBe(DummyCmpB);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
@ -149,8 +152,8 @@ export function main() {
|
|||
|
||||
registry.recognize('/first/second', RootHostCmp)
|
||||
.then((instruction) => {
|
||||
expect(instruction.component).toBe(DummyAsyncCmp);
|
||||
expect(instruction.child.component).toBe(DummyCmpB);
|
||||
expect(instruction.component.componentType).toBe(DummyAsyncCmp);
|
||||
expect(instruction.child.component.componentType).toBe(DummyCmpB);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
@ -162,28 +165,12 @@ export function main() {
|
|||
|
||||
registry.recognize('/first/second', RootHostCmp)
|
||||
.then((instruction) => {
|
||||
expect(instruction.component).toBe(DummyParentCmp);
|
||||
expect(instruction.child.component).toBe(DummyCmpB);
|
||||
expect(instruction.component.componentType).toBe(DummyParentCmp);
|
||||
expect(instruction.child.component.componentType).toBe(DummyCmpB);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
// TODO: not sure what to do with these tests
|
||||
// it('should throw when a config does not have a component or redirectTo property', () => {
|
||||
// expect(() => registry.config(rootHostComponent, {'path': '/some/path'}))
|
||||
// .toThrowError(
|
||||
// 'Route config should contain exactly one \'component\', or \'redirectTo\'
|
||||
// property');
|
||||
//});
|
||||
//
|
||||
// it('should throw when a config has an invalid component type', () => {
|
||||
// expect(() => registry.config(
|
||||
// rootHostComponent,
|
||||
// {'path': '/some/path', 'component': {'type':
|
||||
// 'intentionallyWrongComponentType'}}))
|
||||
// .toThrowError('Invalid component type \'intentionallyWrongComponentType\'');
|
||||
//});
|
||||
|
||||
it('should throw when a parent config is missing the `...` suffix any of its children add routes',
|
||||
() => {
|
||||
expect(() =>
|
||||
|
@ -198,6 +185,40 @@ export function main() {
|
|||
.toThrowError('Unexpected "..." before the end of the path for "home/.../fun/".');
|
||||
});
|
||||
|
||||
it('should match matrix params on child components and query params on the root component',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
registry.config(RootHostCmp, new Route({path: '/first/...', component: DummyParentCmp}));
|
||||
|
||||
registry.recognize('/first/second;filter=odd?comments=all', RootHostCmp)
|
||||
.then((instruction) => {
|
||||
expect(instruction.component.componentType).toBe(DummyParentCmp);
|
||||
expect(instruction.component.params).toEqual({'comments': 'all'});
|
||||
|
||||
expect(instruction.child.component.componentType).toBe(DummyCmpB);
|
||||
expect(instruction.child.component.params).toEqual({'filter': 'odd'});
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should generate URLs with matrix and query params', () => {
|
||||
registry.config(
|
||||
RootHostCmp,
|
||||
new Route({path: '/first/:param/...', component: DummyParentParamCmp, as: 'firstCmp'}));
|
||||
|
||||
var url = stringifyInstruction(registry.generate(
|
||||
[
|
||||
'firstCmp',
|
||||
{param: 'one', query: 'cats'},
|
||||
'secondCmp',
|
||||
{
|
||||
param: 'two',
|
||||
sort: 'asc',
|
||||
}
|
||||
],
|
||||
RootHostCmp));
|
||||
expect(url).toEqual('first/one/second/two;sort=asc?query=cats');
|
||||
});
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -23,14 +23,13 @@ import {IMPLEMENTS} from 'angular2/src/facade/lang';
|
|||
import {bind, Component, View} from 'angular2/angular2';
|
||||
|
||||
import {Location, Router, RouterLink} from 'angular2/router';
|
||||
import {Instruction, ComponentInstruction} from 'angular2/src/router/instruction';
|
||||
|
||||
import {
|
||||
DOM
|
||||
} from 'angular2/src/dom/dom_adapter'
|
||||
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||
|
||||
var dummyInstruction = new Instruction(new ComponentInstruction('detail', [], null), null, {});
|
||||
|
||||
export function
|
||||
main() {
|
||||
export function main() {
|
||||
describe('router-link directive', function() {
|
||||
|
||||
beforeEachBindings(
|
||||
|
@ -59,7 +58,7 @@ import {
|
|||
testComponent.detectChanges();
|
||||
// TODO: shouldn't this be just 'click' rather than '^click'?
|
||||
testComponent.query(By.css('a')).triggerEventHandler('^click', {});
|
||||
expect(router.spy("navigate")).toHaveBeenCalledWith('/detail');
|
||||
expect(router.spy('navigateInstruction')).toHaveBeenCalledWith(dummyInstruction);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
@ -100,7 +99,7 @@ class DummyRouter extends SpyObject {
|
|||
|
||||
function makeDummyRouter() {
|
||||
var dr = new DummyRouter();
|
||||
dr.spy('generate').andCallFake((routeParams) => routeParams.join('='));
|
||||
dr.spy('navigate');
|
||||
dr.spy('generate').andCallFake((routeParams) => dummyInstruction);
|
||||
dr.spy('navigateInstruction');
|
||||
return dr;
|
||||
}
|
||||
|
|
|
@ -20,6 +20,7 @@ import {Pipeline} from 'angular2/src/router/pipeline';
|
|||
import {RouterOutlet} from 'angular2/src/router/router_outlet';
|
||||
import {SpyLocation} from 'angular2/src/mock/location_mock';
|
||||
import {Location} from 'angular2/src/router/location';
|
||||
import {stringifyInstruction} from 'angular2/src/router/instruction';
|
||||
|
||||
import {RouteRegistry} from 'angular2/src/router/route_registry';
|
||||
import {RouteConfig, Route} from 'angular2/src/router/route_config_decorator';
|
||||
|
@ -125,52 +126,54 @@ export function main() {
|
|||
it('should generate URLs from the root component when the path starts with /', () => {
|
||||
router.config([new Route({path: '/first/...', component: DummyParentComp, as: 'firstCmp'})]);
|
||||
|
||||
expect(router.generate(['/firstCmp', 'secondCmp'])).toEqual('/first/second');
|
||||
expect(router.generate(['/firstCmp', 'secondCmp'])).toEqual('/first/second');
|
||||
expect(router.generate(['/firstCmp/secondCmp'])).toEqual('/first/second');
|
||||
var instruction = router.generate(['/firstCmp', 'secondCmp']);
|
||||
expect(stringifyInstruction(instruction)).toEqual('first/second');
|
||||
|
||||
instruction = router.generate(['/firstCmp/secondCmp']);
|
||||
expect(stringifyInstruction(instruction)).toEqual('first/second');
|
||||
});
|
||||
|
||||
describe('querstring params', () => {
|
||||
it('should only apply querystring params if the given URL is on the root router and is terminal',
|
||||
() => {
|
||||
router.config([
|
||||
new Route({path: '/hi/how/are/you', component: DummyComponent, as: 'greeting-url'})
|
||||
]);
|
||||
describe('query string params', () => {
|
||||
it('should use query string params for the root route', () => {
|
||||
router.config(
|
||||
[new Route({path: '/hi/how/are/you', component: DummyComponent, as: 'greeting-url'})]);
|
||||
|
||||
var path = router.generate(['/greeting-url', {'name': 'brad'}]);
|
||||
expect(path).toEqual('/hi/how/are/you?name=brad');
|
||||
});
|
||||
var instruction = router.generate(['/greeting-url', {'name': 'brad'}]);
|
||||
var path = stringifyInstruction(instruction);
|
||||
expect(path).toEqual('hi/how/are/you?name=brad');
|
||||
});
|
||||
|
||||
it('should use parameters that are not apart of the route definition as querystring params',
|
||||
it('should serialize parameters that are not part of the route definition as query string params',
|
||||
() => {
|
||||
router.config(
|
||||
[new Route({path: '/one/two/:three', component: DummyComponent, as: 'number-url'})]);
|
||||
|
||||
var path = router.generate(['/number-url', {'three': 'three', 'four': 'four'}]);
|
||||
expect(path).toEqual('/one/two/three?four=four');
|
||||
var instruction = router.generate(['/number-url', {'three': 'three', 'four': 'four'}]);
|
||||
var path = stringifyInstruction(instruction);
|
||||
expect(path).toEqual('one/two/three?four=four');
|
||||
});
|
||||
});
|
||||
|
||||
describe('matrix params', () => {
|
||||
it('should apply inline matrix params for each router path within the generated URL', () => {
|
||||
it('should generate matrix params for each non-root component', () => {
|
||||
router.config(
|
||||
[new Route({path: '/first/...', component: DummyParentComp, as: 'firstCmp'})]);
|
||||
|
||||
var path =
|
||||
var instruction =
|
||||
router.generate(['/firstCmp', {'key': 'value'}, 'secondCmp', {'project': 'angular'}]);
|
||||
expect(path).toEqual('/first;key=value/second;project=angular');
|
||||
var path = stringifyInstruction(instruction);
|
||||
expect(path).toEqual('first/second;project=angular?key=value');
|
||||
});
|
||||
|
||||
it('should apply inline matrix params for each router path within the generated URL and also include named params',
|
||||
() => {
|
||||
router.config([
|
||||
new Route({path: '/first/:token/...', component: DummyParentComp, as: 'firstCmp'})
|
||||
]);
|
||||
it('should work with named params', () => {
|
||||
router.config(
|
||||
[new Route({path: '/first/:token/...', component: DummyParentComp, as: 'firstCmp'})]);
|
||||
|
||||
var path =
|
||||
router.generate(['/firstCmp', {'token': 'min'}, 'secondCmp', {'author': 'max'}]);
|
||||
expect(path).toEqual('/first/min/second;author=max');
|
||||
});
|
||||
var instruction =
|
||||
router.generate(['/firstCmp', {'token': 'min'}, 'secondCmp', {'author': 'max'}]);
|
||||
var path = stringifyInstruction(instruction);
|
||||
expect(path).toEqual('first/min/second;author=max');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
import {
|
||||
AsyncTestCompleter,
|
||||
describe,
|
||||
it,
|
||||
iit,
|
||||
ddescribe,
|
||||
expect,
|
||||
inject,
|
||||
beforeEach,
|
||||
SpyObject
|
||||
} from 'angular2/test_lib';
|
||||
|
||||
import {UrlParser, Url} from 'angular2/src/router/url_parser';
|
||||
|
||||
|
||||
export function main() {
|
||||
describe('ParsedUrl', () => {
|
||||
var urlParser;
|
||||
|
||||
beforeEach(() => { urlParser = new UrlParser(); });
|
||||
|
||||
it('should work in a simple case', () => {
|
||||
var url = urlParser.parse('hello/there');
|
||||
expect(url.toString()).toEqual('hello/there');
|
||||
});
|
||||
|
||||
it('should remove the leading slash', () => {
|
||||
var url = urlParser.parse('/hello/there');
|
||||
expect(url.toString()).toEqual('hello/there');
|
||||
});
|
||||
|
||||
it('should work with a single aux route', () => {
|
||||
var url = urlParser.parse('hello/there(a)');
|
||||
expect(url.toString()).toEqual('hello/there(a)');
|
||||
});
|
||||
|
||||
it('should work with multiple aux routes', () => {
|
||||
var url = urlParser.parse('hello/there(a//b)');
|
||||
expect(url.toString()).toEqual('hello/there(a//b)');
|
||||
});
|
||||
|
||||
it('should work with children after an aux route', () => {
|
||||
var url = urlParser.parse('hello/there(a//b)/c/d');
|
||||
expect(url.toString()).toEqual('hello/there(a//b)/c/d');
|
||||
});
|
||||
|
||||
it('should work when aux routes have children', () => {
|
||||
var url = urlParser.parse('hello(aa/bb//bb/cc)');
|
||||
expect(url.toString()).toEqual('hello(aa/bb//bb/cc)');
|
||||
});
|
||||
|
||||
it('should parse an aux route with an aux route', () => {
|
||||
var url = urlParser.parse('hello(aa(bb))');
|
||||
expect(url.toString()).toEqual('hello(aa(bb))');
|
||||
});
|
||||
|
||||
it('should simplify an empty aux route definition', () => {
|
||||
var url = urlParser.parse('hello()/there');
|
||||
expect(url.toString()).toEqual('hello/there');
|
||||
});
|
||||
|
||||
it('should parse a key-value matrix param', () => {
|
||||
var url = urlParser.parse('hello/friend;name=bob');
|
||||
expect(url.toString()).toEqual('hello/friend;name=bob');
|
||||
});
|
||||
|
||||
it('should parse multiple key-value matrix params', () => {
|
||||
var url = urlParser.parse('hello/there;greeting=hi;whats=up');
|
||||
expect(url.toString()).toEqual('hello/there;greeting=hi;whats=up');
|
||||
});
|
||||
|
||||
it('should ignore matrix params on the first segment', () => {
|
||||
var url = urlParser.parse('profile;a=1/hi');
|
||||
expect(url.toString()).toEqual('profile/hi');
|
||||
});
|
||||
|
||||
it('should parse a key-only matrix param', () => {
|
||||
var url = urlParser.parse('hello/there;hi');
|
||||
expect(url.toString()).toEqual('hello/there;hi');
|
||||
});
|
||||
|
||||
it('should parse a key-value query param', () => {
|
||||
var url = urlParser.parse('hello/friend?name=bob');
|
||||
expect(url.toString()).toEqual('hello/friend?name=bob');
|
||||
});
|
||||
|
||||
it('should parse multiple key-value query params', () => {
|
||||
var url = urlParser.parse('hello/there?greeting=hi&whats=up');
|
||||
expect(url.params).toEqual({'greeting': 'hi', 'whats': 'up'});
|
||||
expect(url.toString()).toEqual('hello/there?greeting=hi&whats=up');
|
||||
});
|
||||
|
||||
it('should parse a key-only matrix param', () => {
|
||||
var url = urlParser.parse('hello/there?hi');
|
||||
expect(url.toString()).toEqual('hello/there?hi');
|
||||
});
|
||||
|
||||
it('should parse a route with matrix and query params', () => {
|
||||
var url = urlParser.parse('hello/there;sort=asc;unfiltered?hi&friend=true');
|
||||
expect(url.toString()).toEqual('hello/there;sort=asc;unfiltered?hi&friend=true');
|
||||
});
|
||||
|
||||
it('should parse a route with matrix params and aux routes', () => {
|
||||
var url = urlParser.parse('hello/there;sort=asc(modal)');
|
||||
expect(url.toString()).toEqual('hello/there;sort=asc(modal)');
|
||||
});
|
||||
|
||||
it('should parse an aux route with matrix params', () => {
|
||||
var url = urlParser.parse('hello/there(modal;sort=asc)');
|
||||
expect(url.toString()).toEqual('hello/there(modal;sort=asc)');
|
||||
});
|
||||
|
||||
it('should parse a route with matrix params, aux routes, and query params', () => {
|
||||
var url = urlParser.parse('hello/there;sort=asc(modal)?friend=true');
|
||||
expect(url.toString()).toEqual('hello/there;sort=asc(modal)?friend=true');
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Reference in New Issue