parent
96e34c1d36
commit
ac6227e434
@ -19,7 +19,8 @@ export * from './src/router/route_config_decorator';
|
|||||||
export * from './src/router/route_definition';
|
export * from './src/router/route_definition';
|
||||||
export {OnActivate, OnDeactivate, OnReuse, CanDeactivate, CanReuse} from './src/router/interfaces';
|
export {OnActivate, OnDeactivate, OnReuse, CanDeactivate, CanReuse} from './src/router/interfaces';
|
||||||
export {CanActivate} from './src/router/lifecycle_annotations';
|
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 {LocationStrategy} from './src/router/location_strategy';
|
||||||
import {HTML5LocationStrategy} from './src/router/html5_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,
|
List,
|
||||||
ListWrapper
|
ListWrapper
|
||||||
} from 'angular2/src/facade/collection';
|
} 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 {PathRecognizer} from './path_recognizer';
|
||||||
|
import {Url} from './url_parser';
|
||||||
|
|
||||||
export class RouteParams {
|
export class RouteParams {
|
||||||
constructor(public params: StringMap<string, string>) {}
|
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 {
|
export class Instruction {
|
||||||
// "capturedUrl" is the part of the URL captured by this instruction
|
constructor(public component: ComponentInstruction, public child: Instruction,
|
||||||
// "accumulatedUrl" is the part of the URL captured by this instruction and all children
|
public auxInstruction: StringMap<string, Instruction>) {}
|
||||||
accumulatedUrl: string;
|
|
||||||
reuse: boolean = false;
|
|
||||||
specificity: number;
|
|
||||||
|
|
||||||
constructor(public component: any, public capturedUrl: string,
|
replaceChild(child: Instruction): Instruction {
|
||||||
private _recognizer: PathRecognizer, public child: Instruction = null,
|
return new Instruction(this.component, child, this.auxInstruction);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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';
|
import {global} from 'angular2/src/facade/lang';
|
||||||
|
|
||||||
// This is here only so that after TS transpilation the file is not empty.
|
// 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]
|
* Defines route lifecycle method [onActivate]
|
||||||
*/
|
*/
|
||||||
export interface OnActivate {
|
export interface OnActivate {
|
||||||
onActivate(nextInstruction: Instruction, prevInstruction: Instruction): any;
|
onActivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines route lifecycle method [onReuse]
|
* Defines route lifecycle method [onReuse]
|
||||||
*/
|
*/
|
||||||
export interface OnReuse {
|
export interface OnReuse {
|
||||||
onReuse(nextInstruction: Instruction, prevInstruction: Instruction): any;
|
onReuse(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines route lifecycle method [onDeactivate]
|
* Defines route lifecycle method [onDeactivate]
|
||||||
*/
|
*/
|
||||||
export interface OnDeactivate {
|
export interface OnDeactivate {
|
||||||
onDeactivate(nextInstruction: Instruction, prevInstruction: Instruction): any;
|
onDeactivate(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines route lifecycle method [canReuse]
|
* Defines route lifecycle method [canReuse]
|
||||||
*/
|
*/
|
||||||
export interface CanReuse {
|
export interface CanReuse {
|
||||||
canReuse(nextInstruction: Instruction, prevInstruction: Instruction): any;
|
canReuse(nextInstruction: ComponentInstruction, prevInstruction: ComponentInstruction): any;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Defines route lifecycle method [canDeactivate]
|
* Defines route lifecycle method [canDeactivate]
|
||||||
*/
|
*/
|
||||||
export interface 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 {makeDecorator} from 'angular2/src/util/decorators';
|
||||||
import {CanActivate as CanActivateAnnotation} from './lifecycle_annotations_impl';
|
import {CanActivate as CanActivateAnnotation} from './lifecycle_annotations_impl';
|
||||||
import {Promise} from 'angular2/src/facade/async';
|
import {Promise} from 'angular2/src/facade/async';
|
||||||
import {Instruction} from 'angular2/src/router/instruction';
|
import {ComponentInstruction} from 'angular2/src/router/instruction';
|
||||||
|
|
||||||
export {
|
export {
|
||||||
canReuse,
|
canReuse,
|
||||||
@ -17,5 +17,5 @@ export {
|
|||||||
} from './lifecycle_annotations_impl';
|
} from './lifecycle_annotations_impl';
|
||||||
|
|
||||||
export var CanActivate:
|
export var CanActivate:
|
||||||
(hook: (next: Instruction, prev: Instruction) => Promise<boolean>| boolean) => ClassDecorator =
|
(hook: (next: ComponentInstruction, prev: ComponentInstruction) => Promise<boolean>| boolean) => ClassDecorator =
|
||||||
makeDecorator(CanActivateAnnotation);
|
makeDecorator(CanActivateAnnotation);
|
||||||
|
@ -7,7 +7,6 @@ import {
|
|||||||
isBlank,
|
isBlank,
|
||||||
BaseException
|
BaseException
|
||||||
} from 'angular2/src/facade/lang';
|
} from 'angular2/src/facade/lang';
|
||||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
|
||||||
import {
|
import {
|
||||||
Map,
|
Map,
|
||||||
MapWrapper,
|
MapWrapper,
|
||||||
@ -17,21 +16,14 @@ import {
|
|||||||
ListWrapper
|
ListWrapper
|
||||||
} from 'angular2/src/facade/collection';
|
} from 'angular2/src/facade/collection';
|
||||||
import {IMPLEMENTS} from 'angular2/src/facade/lang';
|
import {IMPLEMENTS} from 'angular2/src/facade/lang';
|
||||||
import {parseAndAssignParamString} from 'angular2/src/router/helpers';
|
|
||||||
import {escapeRegex} from './url';
|
|
||||||
import {RouteHandler} from './route_handler';
|
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:
|
export class TouchMap {
|
||||||
// https://github.com/angular/ts2dart/issues/173
|
map: StringMap<string, string> = {};
|
||||||
export class Segment {
|
keys: StringMap<string, boolean> = {};
|
||||||
name: string;
|
|
||||||
regex: string;
|
|
||||||
generate(params: TouchMap): string { return ''; }
|
|
||||||
}
|
|
||||||
|
|
||||||
class TouchMap {
|
|
||||||
map: StringMap<string, string> = StringMapWrapper.create();
|
|
||||||
keys: StringMap<string, boolean> = StringMapWrapper.create();
|
|
||||||
|
|
||||||
constructor(map: StringMap<string, any>) {
|
constructor(map: StringMap<string, any>) {
|
||||||
if (isPresent(map)) {
|
if (isPresent(map)) {
|
||||||
@ -63,31 +55,28 @@ function normalizeString(obj: any): string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class ContinuationSegment extends Segment {}
|
export interface Segment {
|
||||||
|
name: string;
|
||||||
class StaticSegment extends Segment {
|
generate(params: TouchMap): string;
|
||||||
regex: string;
|
match(path: string): boolean;
|
||||||
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; }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@IMPLEMENTS(Segment)
|
class ContinuationSegment implements Segment {
|
||||||
class DynamicSegment {
|
name: string = '';
|
||||||
regex: 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) {}
|
constructor(public name: string) {}
|
||||||
|
match(path: string): boolean { return true; }
|
||||||
generate(params: TouchMap): string {
|
generate(params: TouchMap): string {
|
||||||
if (!StringMapWrapper.contains(params.map, this.name)) {
|
if (!StringMapWrapper.contains(params.map, this.name)) {
|
||||||
throw new BaseException(
|
throw new BaseException(
|
||||||
@ -98,11 +87,9 @@ class DynamicSegment {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
class StarSegment {
|
class StarSegment implements Segment {
|
||||||
regex: string = "(.+)";
|
|
||||||
|
|
||||||
constructor(public name: string) {}
|
constructor(public name: string) {}
|
||||||
|
match(path: string): boolean { return true; }
|
||||||
generate(params: TouchMap): string { return normalizeString(params.get(this.name)); }
|
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}".`);
|
throw new BaseException(`Unexpected "..." before the end of the path for "${route}".`);
|
||||||
}
|
}
|
||||||
results.push(new ContinuationSegment());
|
results.push(new ContinuationSegment());
|
||||||
} else if (segment.length > 0) {
|
} else {
|
||||||
results.push(new StaticSegment(segment));
|
results.push(new StaticSegment(segment));
|
||||||
specificity += 100 * (100 - i);
|
specificity += 100 * (100 - i);
|
||||||
}
|
}
|
||||||
@ -161,6 +148,23 @@ function parsePathString(route: string): StringMap<string, any> {
|
|||||||
return result;
|
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> {
|
function splitBySlash(url: string): List<string> {
|
||||||
return url.split('/');
|
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'
|
// represents something like '/foo/:bar'
|
||||||
export class PathRecognizer {
|
export class PathRecognizer {
|
||||||
segments: List<Segment>;
|
private _segments: List<Segment>;
|
||||||
regex: RegExp;
|
|
||||||
specificity: number;
|
specificity: number;
|
||||||
terminal: boolean = true;
|
terminal: boolean = true;
|
||||||
|
hash: string;
|
||||||
|
|
||||||
static matrixRegex: RegExp = RegExpWrapper.create('^(.*\/[^\/]+?)(;[^\/]+)?\/?$');
|
// TODO: cache component instruction instances by params and by ParsedUrl instance
|
||||||
static queryRegex: RegExp = RegExpWrapper.create('^(.*\/[^\/]+?)(\\?[^\/]+)?$');
|
|
||||||
|
|
||||||
constructor(public path: string, public handler: RouteHandler, public isRoot: boolean = false) {
|
constructor(public path: string, public handler: RouteHandler) {
|
||||||
assertPath(path);
|
assertPath(path);
|
||||||
var parsed = parsePathString(path);
|
var parsed = parsePathString(path);
|
||||||
var specificity = parsed['specificity'];
|
|
||||||
var segments = parsed['segments'];
|
|
||||||
var regexString = '^';
|
|
||||||
|
|
||||||
ListWrapper.forEach(segments, (segment) => {
|
this._segments = parsed['segments'];
|
||||||
if (segment instanceof ContinuationSegment) {
|
this.specificity = parsed['specificity'];
|
||||||
this.terminal = false;
|
this.hash = pathDslHash(this._segments);
|
||||||
} else {
|
|
||||||
regexString += '/' + segment.regex;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (this.terminal) {
|
var lastSegment = this._segments[this._segments.length - 1];
|
||||||
regexString += '$';
|
this.terminal = !(lastSegment instanceof ContinuationSegment);
|
||||||
}
|
|
||||||
|
|
||||||
this.regex = RegExpWrapper.create(regexString);
|
|
||||||
this.segments = segments;
|
|
||||||
this.specificity = specificity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
recognize(beginningSegment: Url): PathMatch {
|
||||||
if (!containsStarSegment) {
|
var nextSegment = beginningSegment;
|
||||||
var matches = RegExpWrapper.firstMatch(
|
var currentSegment: Url;
|
||||||
useQueryString ? PathRecognizer.queryRegex : PathRecognizer.matrixRegex, url);
|
var positionalParams = {};
|
||||||
if (isPresent(matches)) {
|
var captured = [];
|
||||||
url = matches[1];
|
|
||||||
paramsString = matches[2];
|
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) {
|
if (segment instanceof ContinuationSegment) {
|
||||||
continue;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
var match = RegExpWrapper.firstMatch(RegExpWrapper.create('/' + segment.regex), urlPart);
|
captured.push(currentSegment.path);
|
||||||
urlPart = StringWrapper.substring(urlPart, match[0].length);
|
|
||||||
if (segment.name.length > 0) {
|
// the star segment consumes all of the remaining URL, including matrix params
|
||||||
params[segment.name] = match[1];
|
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) {
|
if (this.terminal && isPresent(nextSegment)) {
|
||||||
var expectedStartingValue = useQueryString ? '?' : ';';
|
return null;
|
||||||
if (paramsString[0] == expectedStartingValue) {
|
|
||||||
parseAndAssignParamString(expectedStartingValue, paramsString, params);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 paramTokens = new TouchMap(params);
|
||||||
var applyLeadingSlash = false;
|
|
||||||
var useQueryString = this.isRoot && this.terminal;
|
|
||||||
|
|
||||||
var url = '';
|
var path = [];
|
||||||
for (var i = 0; i < this.segments.length; i++) {
|
|
||||||
let segment = this.segments[i];
|
|
||||||
let s = segment.generate(paramTokens);
|
|
||||||
applyLeadingSlash = applyLeadingSlash || (segment instanceof ContinuationSegment);
|
|
||||||
|
|
||||||
if (s.length > 0) {
|
for (var i = 0; i < this._segments.length; i++) {
|
||||||
url += (i > 0 ? '/' : '') + s;
|
let segment = this._segments[i];
|
||||||
|
if (!(segment instanceof ContinuationSegment)) {
|
||||||
|
path.push(segment.generate(paramTokens));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
var urlPath = path.join('/');
|
||||||
|
|
||||||
var unusedParams = paramTokens.getUnused();
|
var nonPositionalParams = paramTokens.getUnused();
|
||||||
if (!StringMapWrapper.isEmpty(unusedParams)) {
|
var urlParams = serializeParams(nonPositionalParams);
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (applyLeadingSlash) {
|
return new ComponentInstruction(urlPath, urlParams, this, params);
|
||||||
url += '/';
|
|
||||||
}
|
|
||||||
|
|
||||||
return url;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {makeDecorator} from 'angular2/src/util/decorators';
|
||||||
import {List} from 'angular2/src/facade/collection';
|
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 =
|
export var RouteConfig: (configs: List<RouteDefinition>) => ClassDecorator =
|
||||||
makeDecorator(RouteConfigAnnotation);
|
makeDecorator(RouteConfigAnnotation);
|
||||||
|
@ -8,7 +8,7 @@ export {RouteDefinition} from './route_definition';
|
|||||||
*
|
*
|
||||||
* Supported keys:
|
* Supported keys:
|
||||||
* - `path` (required)
|
* - `path` (required)
|
||||||
* - `component`, `redirectTo` (requires exactly one of these)
|
* - `component`, `loader`, `redirectTo` (requires exactly one of these)
|
||||||
* - `as` (optional)
|
* - `as` (optional)
|
||||||
*/
|
*/
|
||||||
@CONST()
|
@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()
|
@CONST()
|
||||||
export class AsyncRoute implements RouteDefinition {
|
export class AsyncRoute implements RouteDefinition {
|
||||||
path: string;
|
path: string;
|
||||||
@ -51,6 +66,8 @@ export class Redirect implements RouteDefinition {
|
|||||||
path: string;
|
path: string;
|
||||||
redirectTo: string;
|
redirectTo: string;
|
||||||
as: string = null;
|
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}) {
|
constructor({path, redirectTo}: {path: string, redirectTo: string}) {
|
||||||
this.path = path;
|
this.path = path;
|
||||||
this.redirectTo = redirectTo;
|
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 {ComponentDefinition} from './route_definition';
|
||||||
import {Type, BaseException} from 'angular2/src/facade/lang';
|
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
|
* Given a JS Object that represents... returns a corresponding Route, AsyncRoute, or Redirect
|
||||||
*/
|
*/
|
||||||
export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
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;
|
return <RouteDefinition>config;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((!config.component) == (!config.redirectTo)) {
|
if ((!config.component) == (!config.redirectTo)) {
|
||||||
throw new BaseException(
|
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 (config.component) {
|
||||||
if (typeof config.component == 'object') {
|
if (typeof config.component == 'object') {
|
||||||
@ -28,7 +29,7 @@ export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
|||||||
{path: config.path, loader: componentDefinitionObject.loader, as: config.as});
|
{path: config.path, loader: componentDefinitionObject.loader, as: config.as});
|
||||||
} else {
|
} else {
|
||||||
throw new BaseException(
|
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(<{
|
return new Route(<{
|
||||||
|
@ -6,7 +6,8 @@ import {
|
|||||||
isPresent,
|
isPresent,
|
||||||
isType,
|
isType,
|
||||||
isStringMap,
|
isStringMap,
|
||||||
BaseException
|
BaseException,
|
||||||
|
Type
|
||||||
} from 'angular2/src/facade/lang';
|
} from 'angular2/src/facade/lang';
|
||||||
import {
|
import {
|
||||||
Map,
|
Map,
|
||||||
@ -17,12 +18,13 @@ import {
|
|||||||
StringMapWrapper
|
StringMapWrapper
|
||||||
} from 'angular2/src/facade/collection';
|
} from 'angular2/src/facade/collection';
|
||||||
|
|
||||||
import {PathRecognizer} from './path_recognizer';
|
import {PathRecognizer, PathMatch} from './path_recognizer';
|
||||||
import {RouteHandler} from './route_handler';
|
import {Route, AsyncRoute, AuxRoute, Redirect, RouteDefinition} from './route_config_impl';
|
||||||
import {Route, AsyncRoute, Redirect, RouteDefinition} from './route_config_impl';
|
|
||||||
import {AsyncRouteHandler} from './async_route_handler';
|
import {AsyncRouteHandler} from './async_route_handler';
|
||||||
import {SyncRouteHandler} from './sync_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.
|
* `RouteRecognizer` is responsible for recognizing routes for a single component.
|
||||||
@ -31,30 +33,45 @@ import {parseAndAssignParamString} from 'angular2/src/router/helpers';
|
|||||||
*/
|
*/
|
||||||
export class RouteRecognizer {
|
export class RouteRecognizer {
|
||||||
names: Map<string, PathRecognizer> = new Map();
|
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 {
|
config(config: RouteDefinition): boolean {
|
||||||
var handler;
|
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) {
|
if (config instanceof Redirect) {
|
||||||
let path = config.path == '/' ? '' : config.path;
|
this.redirects.push(new Redirector(config.path, config.redirectTo));
|
||||||
this.redirects.set(path, config.redirectTo);
|
|
||||||
return true;
|
return true;
|
||||||
} else if (config instanceof Route) {
|
}
|
||||||
|
|
||||||
|
if (config instanceof Route) {
|
||||||
handler = new SyncRouteHandler(config.component);
|
handler = new SyncRouteHandler(config.component);
|
||||||
} else if (config instanceof AsyncRoute) {
|
} else if (config instanceof AsyncRoute) {
|
||||||
handler = new AsyncRouteHandler(config.loader);
|
handler = new AsyncRouteHandler(config.loader);
|
||||||
}
|
}
|
||||||
var recognizer = new PathRecognizer(config.path, handler, this.isRoot);
|
var recognizer = new PathRecognizer(config.path, handler);
|
||||||
MapWrapper.forEach(this.matchers, (matcher, _) => {
|
|
||||||
if (recognizer.regex.toString() == matcher.regex.toString()) {
|
this.matchers.forEach((matcher) => {
|
||||||
|
if (recognizer.hash == matcher.hash) {
|
||||||
throw new BaseException(
|
throw new BaseException(
|
||||||
`Configuration '${config.path}' conflicts with existing route '${matcher.path}'`);
|
`Configuration '${config.path}' conflicts with existing route '${matcher.path}'`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
this.matchers.set(recognizer.regex, recognizer);
|
|
||||||
|
this.matchers.push(recognizer);
|
||||||
if (isPresent(config.as)) {
|
if (isPresent(config.as)) {
|
||||||
this.names.set(config.as, recognizer);
|
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.
|
* 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 = [];
|
var solutions = [];
|
||||||
if (url.length > 0 && url[url.length - 1] == '/') {
|
|
||||||
url = url.substring(0, url.length - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
MapWrapper.forEach(this.redirects, (target, path) => {
|
urlParse = this._redirect(urlParse);
|
||||||
// "/" redirect case
|
|
||||||
if (path == '/' || path == '') {
|
|
||||||
if (path == url) {
|
|
||||||
url = target;
|
|
||||||
}
|
|
||||||
} else if (url.startsWith(path)) {
|
|
||||||
url = target + url.substring(path.length);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
var queryParams = StringMapWrapper.create();
|
this.matchers.forEach((pathRecognizer: PathRecognizer) => {
|
||||||
var queryString = '';
|
var pathMatch = pathRecognizer.recognize(urlParse);
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
MapWrapper.forEach(this.matchers, (pathRecognizer, regex) => {
|
if (isPresent(pathMatch)) {
|
||||||
var match;
|
solutions.push(pathMatch);
|
||||||
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));
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return solutions;
|
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); }
|
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);
|
var pathRecognizer: PathRecognizer = this.names.get(name);
|
||||||
if (isBlank(pathRecognizer)) {
|
if (isBlank(pathRecognizer)) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
var url = pathRecognizer.generate(params);
|
return pathRecognizer.generate(params);
|
||||||
return {url, 'nextComponent': pathRecognizer.handler.componentType};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export class RouteMatch {
|
export class Redirector {
|
||||||
private _params: StringMap<string, any>;
|
segments: List<string> = [];
|
||||||
private _paramsParsed: boolean = false;
|
toSegments: List<string> = [];
|
||||||
|
|
||||||
constructor(public recognizer: PathRecognizer, public matchedUrl: string,
|
constructor(path: string, redirectTo: string) {
|
||||||
public unmatchedUrl: string, p: StringMap<string, any> = null) {
|
if (path.startsWith('/')) {
|
||||||
this._params = isPresent(p) ? p : StringMapWrapper.create();
|
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) {
|
* Returns `null` or a `ParsedUrl` representing the new path to match
|
||||||
this._paramsParsed = true;
|
*/
|
||||||
StringMapWrapper.forEach(this.recognizer.parseParams(this.matchedUrl),
|
redirect(urlParse: Url): Url {
|
||||||
(value, key) => { StringMapWrapper.set(this._params, key, value); });
|
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 {PathMatch} from './path_recognizer';
|
||||||
import {Instruction} from './instruction';
|
import {RouteRecognizer} from './route_recognizer';
|
||||||
|
import {Instruction, ComponentInstruction, PrimaryInstruction} from './instruction';
|
||||||
import {
|
import {
|
||||||
List,
|
List,
|
||||||
ListWrapper,
|
ListWrapper,
|
||||||
@ -24,6 +25,9 @@ import {RouteConfig, AsyncRoute, Route, Redirect, RouteDefinition} from './route
|
|||||||
import {reflector} from 'angular2/src/reflection/reflection';
|
import {reflector} from 'angular2/src/reflection/reflection';
|
||||||
import {Injectable} from 'angular2/di';
|
import {Injectable} from 'angular2/di';
|
||||||
import {normalizeRouteConfig} from './route_config_nomalizer';
|
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.
|
* 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
|
* 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);
|
config = normalizeRouteConfig(config);
|
||||||
|
|
||||||
var recognizer: RouteRecognizer = this._rules.get(parentComponent);
|
var recognizer: RouteRecognizer = this._rules.get(parentComponent);
|
||||||
|
|
||||||
if (isBlank(recognizer)) {
|
if (isBlank(recognizer)) {
|
||||||
recognizer = new RouteRecognizer(isRootLevelRoute);
|
recognizer = new RouteRecognizer();
|
||||||
this._rules.set(parentComponent, recognizer);
|
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
|
* 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)) {
|
if (!isType(component)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -77,8 +81,7 @@ export class RouteRegistry {
|
|||||||
var annotation = annotations[i];
|
var annotation = annotations[i];
|
||||||
|
|
||||||
if (annotation instanceof RouteConfig) {
|
if (annotation instanceof RouteConfig) {
|
||||||
ListWrapper.forEach(annotation.configs,
|
ListWrapper.forEach(annotation.configs, (config) => this.config(component, config));
|
||||||
(config) => this.config(component, config, isRootComponent));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -90,63 +93,100 @@ export class RouteRegistry {
|
|||||||
* the application into the state specified by the url
|
* the application into the state specified by the url
|
||||||
*/
|
*/
|
||||||
recognize(url: string, parentComponent: any): Promise<Instruction> {
|
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);
|
var componentRecognizer = this._rules.get(parentComponent);
|
||||||
if (isBlank(componentRecognizer)) {
|
if (isBlank(componentRecognizer)) {
|
||||||
return PromiseWrapper.resolve(null);
|
return PromiseWrapper.resolve(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matches some beginning part of the given URL
|
// Matches some beginning part of the given URL
|
||||||
var possibleMatches = componentRecognizer.recognize(url);
|
var possibleMatches = componentRecognizer.recognize(parsedUrl);
|
||||||
|
|
||||||
var matchPromises =
|
var matchPromises =
|
||||||
ListWrapper.map(possibleMatches, (candidate) => this._completeRouteMatch(candidate));
|
ListWrapper.map(possibleMatches, (candidate) => this._completePrimaryRouteMatch(candidate));
|
||||||
|
|
||||||
return PromiseWrapper.all(matchPromises)
|
return PromiseWrapper.all(matchPromises).then(mostSpecific);
|
||||||
.then((solutions: List<Instruction>) => {
|
|
||||||
// remove nulls
|
|
||||||
var fullSolutions = ListWrapper.filter(solutions, (solution) => isPresent(solution));
|
|
||||||
|
|
||||||
if (fullSolutions.length > 0) {
|
|
||||||
return mostSpecific(fullSolutions);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _completePrimaryRouteMatch(partialMatch: PathMatch): Promise<PrimaryInstruction> {
|
||||||
_completeRouteMatch(partialMatch: RouteMatch): Promise<Instruction> {
|
var instruction = partialMatch.instruction;
|
||||||
var recognizer = partialMatch.recognizer;
|
return instruction.resolveComponentType().then((componentType) => {
|
||||||
var handler = recognizer.handler;
|
|
||||||
return handler.resolveComponentType().then((componentType) => {
|
|
||||||
this.configFromComponent(componentType);
|
this.configFromComponent(componentType);
|
||||||
|
|
||||||
if (partialMatch.unmatchedUrl.length == 0) {
|
if (isBlank(partialMatch.remaining)) {
|
||||||
if (recognizer.terminal) {
|
if (instruction.terminal) {
|
||||||
return new Instruction(componentType, partialMatch.matchedUrl, recognizer, null,
|
return new PrimaryInstruction(instruction, null, partialMatch.remainingAux);
|
||||||
partialMatch.params());
|
|
||||||
} else {
|
} else {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.recognize(partialMatch.unmatchedUrl, componentType)
|
return this._recognizePrimaryRoute(partialMatch.remaining, componentType)
|
||||||
.then(childInstruction => {
|
.then((childInstruction) => {
|
||||||
if (isBlank(childInstruction)) {
|
if (isBlank(childInstruction)) {
|
||||||
return null;
|
return null;
|
||||||
} else {
|
} else {
|
||||||
return new Instruction(componentType, partialMatch.matchedUrl, recognizer,
|
return new PrimaryInstruction(instruction, childInstruction,
|
||||||
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 }]`
|
* 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`.
|
* generates a url with a leading slash relative to the provided `parentComponent`.
|
||||||
*/
|
*/
|
||||||
generate(linkParams: List<any>, parentComponent: any): string {
|
generate(linkParams: List<any>, parentComponent: any): Instruction {
|
||||||
let url = '';
|
let segments = [];
|
||||||
let componentCursor = parentComponent;
|
let componentCursor = parentComponent;
|
||||||
|
|
||||||
for (let i = 0; i < linkParams.length; i += 1) {
|
for (let i = 0; i < linkParams.length; i += 1) {
|
||||||
let segment = linkParams[i];
|
let segment = linkParams[i];
|
||||||
if (isBlank(componentCursor)) {
|
if (isBlank(componentCursor)) {
|
||||||
@ -172,15 +212,22 @@ export class RouteRegistry {
|
|||||||
`Component "${getTypeNameForDebugging(componentCursor)}" has no route config.`);
|
`Component "${getTypeNameForDebugging(componentCursor)}" has no route config.`);
|
||||||
}
|
}
|
||||||
var response = componentRecognizer.generate(segment, params);
|
var response = componentRecognizer.generate(segment, params);
|
||||||
|
|
||||||
if (isBlank(response)) {
|
if (isBlank(response)) {
|
||||||
throw new BaseException(
|
throw new BaseException(
|
||||||
`Component "${getTypeNameForDebugging(componentCursor)}" has no route named "${segment}".`);
|
`Component "${getTypeNameForDebugging(componentCursor)}" has no route named "${segment}".`);
|
||||||
}
|
}
|
||||||
url += response['url'];
|
segments.push(response);
|
||||||
componentCursor = response['nextComponent'];
|
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
|
* 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];
|
var mostSpecificSolution = instructions[0];
|
||||||
for (var solutionIndex = 1; solutionIndex < instructions.length; solutionIndex++) {
|
for (var solutionIndex = 1; solutionIndex < instructions.length; solutionIndex++) {
|
||||||
var solution = instructions[solutionIndex];
|
var solution: PrimaryInstruction = instructions[solutionIndex];
|
||||||
if (solution.specificity > mostSpecificSolution.specificity) {
|
if (isBlank(solution)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (solution.component.specificity > mostSpecificSolution.component.specificity) {
|
||||||
mostSpecificSolution = solution;
|
mostSpecificSolution = solution;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -12,7 +12,7 @@ import {
|
|||||||
|
|
||||||
import {RouteRegistry} from './route_registry';
|
import {RouteRegistry} from './route_registry';
|
||||||
import {Pipeline} from './pipeline';
|
import {Pipeline} from './pipeline';
|
||||||
import {Instruction} from './instruction';
|
import {ComponentInstruction, Instruction, stringifyInstruction} from './instruction';
|
||||||
import {RouterOutlet} from './router_outlet';
|
import {RouterOutlet} from './router_outlet';
|
||||||
import {Location} from './location';
|
import {Location} from './location';
|
||||||
import {getCanActivateHook} from './route_lifecycle_reflector';
|
import {getCanActivateHook} from './route_lifecycle_reflector';
|
||||||
@ -45,10 +45,9 @@ export class Router {
|
|||||||
private _currentInstruction: Instruction = null;
|
private _currentInstruction: Instruction = null;
|
||||||
private _currentNavigation: Promise<any> = _resolveToTrue;
|
private _currentNavigation: Promise<any> = _resolveToTrue;
|
||||||
private _outlet: RouterOutlet = null;
|
private _outlet: RouterOutlet = null;
|
||||||
|
private _auxOutlets: Map<string, RouterOutlet> = new Map();
|
||||||
private _subject: EventEmitter = new EventEmitter();
|
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,
|
constructor(public registry: RouteRegistry, public _pipeline: Pipeline, public parent: Router,
|
||||||
public hostComponent: any) {}
|
public hostComponent: any) {}
|
||||||
|
|
||||||
@ -65,8 +64,11 @@ export class Router {
|
|||||||
* you're writing a reusable component.
|
* you're writing a reusable component.
|
||||||
*/
|
*/
|
||||||
registerOutlet(outlet: RouterOutlet): Promise<boolean> {
|
registerOutlet(outlet: RouterOutlet): Promise<boolean> {
|
||||||
// TODO: sibling routes
|
if (isPresent(outlet.name)) {
|
||||||
this._outlet = outlet;
|
this._auxOutlets.set(outlet.name, outlet);
|
||||||
|
} else {
|
||||||
|
this._outlet = outlet;
|
||||||
|
}
|
||||||
if (isPresent(this._currentInstruction)) {
|
if (isPresent(this._currentInstruction)) {
|
||||||
return outlet.commit(this._currentInstruction);
|
return outlet.commit(this._currentInstruction);
|
||||||
}
|
}
|
||||||
@ -87,9 +89,8 @@ export class Router {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
config(definitions: List<RouteDefinition>): Promise<any> {
|
config(definitions: List<RouteDefinition>): Promise<any> {
|
||||||
definitions.forEach((routeDefinition) => {
|
definitions.forEach(
|
||||||
this.registry.config(this.hostComponent, routeDefinition, this instanceof RootRouter);
|
(routeDefinition) => { this.registry.config(this.hostComponent, routeDefinition); });
|
||||||
});
|
|
||||||
return this.renavigate();
|
return this.renavigate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -104,31 +105,51 @@ export class Router {
|
|||||||
return this._currentNavigation = this._currentNavigation.then((_) => {
|
return this._currentNavigation = this._currentNavigation.then((_) => {
|
||||||
this.lastNavigationAttempt = url;
|
this.lastNavigationAttempt = url;
|
||||||
this._startNavigating();
|
this._startNavigating();
|
||||||
return this._afterPromiseFinishNavigating(this.recognize(url).then((matchedInstruction) => {
|
return this._afterPromiseFinishNavigating(this.recognize(url).then((instruction) => {
|
||||||
if (isBlank(matchedInstruction)) {
|
if (isBlank(instruction)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return this._reuse(matchedInstruction)
|
return this._navigate(instruction, _skipLocationChange);
|
||||||
.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;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 _emitNavigationFinish(url): void { ObservableWrapper.callNext(this._subject, url); }
|
||||||
|
|
||||||
private _afterPromiseFinishNavigating(promise: Promise<any>): Promise<any> {
|
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)) {
|
if (isBlank(this._outlet)) {
|
||||||
return _resolveToFalse;
|
return _resolveToFalse;
|
||||||
}
|
}
|
||||||
return this._outlet.canReuse(instruction)
|
return this._outlet.canReuse(instruction)
|
||||||
.then((result) => {
|
.then((result) => {
|
||||||
instruction.reuse = result;
|
|
||||||
if (isPresent(this._outlet.childRouter) && isPresent(instruction.child)) {
|
if (isPresent(this._outlet.childRouter) && isPresent(instruction.child)) {
|
||||||
return this._outlet.childRouter._reuse(instruction.child);
|
return this._outlet.childRouter._reuse(instruction.child);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _canActivate(instruction: Instruction): Promise<boolean> {
|
private _canActivate(nextInstruction: Instruction): Promise<boolean> {
|
||||||
return canActivateOne(instruction, this._currentInstruction);
|
return canActivateOne(nextInstruction, this._currentInstruction);
|
||||||
}
|
}
|
||||||
|
|
||||||
private _canDeactivate(instruction: Instruction): Promise<boolean> {
|
private _canDeactivate(instruction: Instruction): Promise<boolean> {
|
||||||
@ -160,11 +180,12 @@ export class Router {
|
|||||||
return _resolveToTrue;
|
return _resolveToTrue;
|
||||||
}
|
}
|
||||||
var next: Promise<boolean>;
|
var next: Promise<boolean>;
|
||||||
if (isPresent(instruction) && instruction.reuse) {
|
if (isPresent(instruction) && instruction.component.reuse) {
|
||||||
next = _resolveToTrue;
|
next = _resolveToTrue;
|
||||||
} else {
|
} else {
|
||||||
next = this._outlet.canDeactivate(instruction);
|
next = this._outlet.canDeactivate(instruction);
|
||||||
}
|
}
|
||||||
|
// TODO: aux route lifecycle hooks
|
||||||
return next.then((result) => {
|
return next.then((result) => {
|
||||||
if (result == false) {
|
if (result == false) {
|
||||||
return false;
|
return false;
|
||||||
@ -182,10 +203,14 @@ export class Router {
|
|||||||
*/
|
*/
|
||||||
commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> {
|
commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> {
|
||||||
this._currentInstruction = instruction;
|
this._currentInstruction = instruction;
|
||||||
|
var next = _resolveToTrue;
|
||||||
if (isPresent(this._outlet)) {
|
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
|
* Generate a URL from a component name and optional map of parameters. The URL is relative to the
|
||||||
* app's base href.
|
* app's base href.
|
||||||
*/
|
*/
|
||||||
generate(linkParams: List<any>): string {
|
generate(linkParams: List<any>): Instruction {
|
||||||
let normalizedLinkParams = splitAndFlattenLinkParams(linkParams);
|
let normalizedLinkParams = splitAndFlattenLinkParams(linkParams);
|
||||||
|
|
||||||
var first = ListWrapper.first(normalizedLinkParams);
|
var first = ListWrapper.first(normalizedLinkParams);
|
||||||
@ -275,11 +300,22 @@ export class Router {
|
|||||||
throw new BaseException(msg);
|
throw new BaseException(msg);
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = '';
|
// TODO: structural cloning and whatnot
|
||||||
if (isPresent(router.parent) && isPresent(router.parent._currentInstruction)) {
|
|
||||||
url = router.parent._currentInstruction.capturedUrl;
|
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);
|
super(registry, pipeline, null, hostComponent);
|
||||||
this._location = location;
|
this._location = location;
|
||||||
this._location.subscribe((change) => this.navigate(change['url'], isPresent(change['pop'])));
|
this._location.subscribe((change) => this.navigate(change['url'], isPresent(change['pop'])));
|
||||||
this.registry.configFromComponent(hostComponent, true);
|
|
||||||
|
this.registry.configFromComponent(hostComponent);
|
||||||
this.navigate(location.path());
|
this.navigate(location.path());
|
||||||
}
|
}
|
||||||
|
|
||||||
commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> {
|
commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> {
|
||||||
|
var emitUrl = stringifyInstruction(instruction);
|
||||||
|
if (emitUrl.length > 0) {
|
||||||
|
emitUrl = '/' + emitUrl;
|
||||||
|
}
|
||||||
var promise = super.commit(instruction);
|
var promise = super.commit(instruction);
|
||||||
if (!_skipLocationChange) {
|
if (!_skipLocationChange) {
|
||||||
promise = promise.then((_) => { this._location.go(instruction.accumulatedUrl); });
|
promise = promise.then((_) => { this._location.go(emitUrl); });
|
||||||
}
|
}
|
||||||
return promise;
|
return promise;
|
||||||
}
|
}
|
||||||
@ -315,6 +356,12 @@ class ChildRouter extends Router {
|
|||||||
// Delegate navigation to the root router
|
// Delegate navigation to the root router
|
||||||
return this.parent.navigate(url, _skipLocationChange);
|
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;
|
var next = _resolveToTrue;
|
||||||
if (isPresent(nextInstruction.child)) {
|
if (isPresent(nextInstruction.child)) {
|
||||||
next = canActivateOne(nextInstruction.child,
|
next = canActivateOne(nextInstruction.child,
|
||||||
isPresent(currentInstruction) ? currentInstruction.child : null);
|
isPresent(prevInstruction) ? prevInstruction.child : null);
|
||||||
}
|
}
|
||||||
return next.then((res) => {
|
return next.then((result) => {
|
||||||
if (res == false) {
|
if (result == false) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (nextInstruction.reuse) {
|
if (nextInstruction.component.reuse) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
var hook = getCanActivateHook(nextInstruction.component);
|
var hook = getCanActivateHook(nextInstruction.component.componentType);
|
||||||
if (isPresent(hook)) {
|
if (isPresent(hook)) {
|
||||||
return hook(nextInstruction, currentInstruction);
|
return hook(nextInstruction.component,
|
||||||
|
isPresent(prevInstruction) ? prevInstruction.component : null);
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
@ -3,6 +3,7 @@ import {List, StringMap, StringMapWrapper} from 'angular2/src/facade/collection'
|
|||||||
|
|
||||||
import {Router} from './router';
|
import {Router} from './router';
|
||||||
import {Location} from './location';
|
import {Location} from './location';
|
||||||
|
import {Instruction, stringifyInstruction} from './instruction';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The RouterLink directive lets you link to specific parts of your app.
|
* 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.
|
// the url displayed on the anchor element.
|
||||||
visibleHref: string;
|
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) {}
|
constructor(private _router: Router, private _location: Location) {}
|
||||||
|
|
||||||
set routeParams(changes: List<any>) {
|
set routeParams(changes: List<any>) {
|
||||||
this._routeParams = changes;
|
this._routeParams = changes;
|
||||||
this._navigationHref = this._router.generate(this._routeParams);
|
this._navigationInstruction = this._router.generate(this._routeParams);
|
||||||
this.visibleHref = this._location.normalizeAbsolutely(this._navigationHref);
|
|
||||||
|
// TODO: is this the right spot for this?
|
||||||
|
var navigationHref = '/' + stringifyInstruction(this._navigationInstruction);
|
||||||
|
this.visibleHref = this._location.normalizeAbsolutely(navigationHref);
|
||||||
}
|
}
|
||||||
|
|
||||||
onClick(): boolean {
|
onClick(): boolean {
|
||||||
this._router.navigate(this._navigationHref);
|
this._router.navigateInstruction(this._navigationInstruction);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -7,7 +7,7 @@ import {DynamicComponentLoader, ComponentRef, ElementRef} from 'angular2/core';
|
|||||||
import {Injector, bind, Dependency, undefinedValue} from 'angular2/di';
|
import {Injector, bind, Dependency, undefinedValue} from 'angular2/di';
|
||||||
|
|
||||||
import * as routerMod from './router';
|
import * as routerMod from './router';
|
||||||
import {Instruction, RouteParams} from './instruction';
|
import {Instruction, ComponentInstruction, RouteParams} from './instruction';
|
||||||
import * as hookMod from './lifecycle_annotations';
|
import * as hookMod from './lifecycle_annotations';
|
||||||
import {hasLifecycleHook} from './route_lifecycle_reflector';
|
import {hasLifecycleHook} from './route_lifecycle_reflector';
|
||||||
|
|
||||||
@ -23,16 +23,16 @@ import {hasLifecycleHook} from './route_lifecycle_reflector';
|
|||||||
@Directive({selector: 'router-outlet'})
|
@Directive({selector: 'router-outlet'})
|
||||||
export class RouterOutlet {
|
export class RouterOutlet {
|
||||||
childRouter: routerMod.Router = null;
|
childRouter: routerMod.Router = null;
|
||||||
|
name: string = null;
|
||||||
|
|
||||||
private _componentRef: ComponentRef = null;
|
private _componentRef: ComponentRef = null;
|
||||||
private _currentInstruction: Instruction = null;
|
private _currentInstruction: ComponentInstruction = null;
|
||||||
|
|
||||||
constructor(private _elementRef: ElementRef, private _loader: DynamicComponentLoader,
|
constructor(private _elementRef: ElementRef, private _loader: DynamicComponentLoader,
|
||||||
private _parentRouter: routerMod.Router, @Attribute('name') nameAttr: string) {
|
private _parentRouter: routerMod.Router, @Attribute('name') nameAttr: string) {
|
||||||
// TODO: reintroduce with new // sibling routes
|
if (isPresent(nameAttr)) {
|
||||||
// if (isBlank(nameAttr)) {
|
this.name = nameAttr;
|
||||||
// nameAttr = 'default';
|
}
|
||||||
//}
|
|
||||||
this._parentRouter.registerOutlet(this);
|
this._parentRouter.registerOutlet(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,15 +40,28 @@ export class RouterOutlet {
|
|||||||
* Given an instruction, update the contents of this outlet.
|
* Given an instruction, update the contents of this outlet.
|
||||||
*/
|
*/
|
||||||
commit(instruction: Instruction): Promise<any> {
|
commit(instruction: Instruction): Promise<any> {
|
||||||
|
instruction = this._getInstruction(instruction);
|
||||||
|
var componentInstruction = instruction.component;
|
||||||
|
if (isBlank(componentInstruction)) {
|
||||||
|
return PromiseWrapper.resolve(true);
|
||||||
|
}
|
||||||
var next;
|
var next;
|
||||||
if (instruction.reuse) {
|
if (componentInstruction.reuse) {
|
||||||
next = this._reuse(instruction);
|
next = this._reuse(componentInstruction);
|
||||||
} else {
|
} else {
|
||||||
next = this.deactivate(instruction).then((_) => this._activate(instruction));
|
next = this.deactivate(instruction).then((_) => this._activate(componentInstruction));
|
||||||
}
|
}
|
||||||
return next.then((_) => this._commitChild(instruction));
|
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> {
|
private _commitChild(instruction: Instruction): Promise<any> {
|
||||||
if (isPresent(this.childRouter)) {
|
if (isPresent(this.childRouter)) {
|
||||||
return this.childRouter.commit(instruction.child);
|
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;
|
var previousInstruction = this._currentInstruction;
|
||||||
this._currentInstruction = instruction;
|
this._currentInstruction = instruction;
|
||||||
this.childRouter = this._parentRouter.childRouter(instruction.component);
|
var componentType = instruction.componentType;
|
||||||
|
this.childRouter = this._parentRouter.childRouter(componentType);
|
||||||
|
|
||||||
var bindings = Injector.resolve([
|
var bindings = Injector.resolve([
|
||||||
bind(RouteParams)
|
bind(RouteParams)
|
||||||
.toValue(new RouteParams(instruction.params())),
|
.toValue(new RouteParams(instruction.params)),
|
||||||
bind(routerMod.Router).toValue(this.childRouter)
|
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) => {
|
.then((componentRef) => {
|
||||||
this._componentRef = componentRef;
|
this._componentRef = componentRef;
|
||||||
if (hasLifecycleHook(hookMod.onActivate, instruction.component)) {
|
if (hasLifecycleHook(hookMod.onActivate, componentType)) {
|
||||||
return this._componentRef.instance.onActivate(instruction, previousInstruction);
|
return this._componentRef.instance.onActivate(instruction, previousInstruction);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -84,9 +98,11 @@ export class RouterOutlet {
|
|||||||
if (isBlank(this._currentInstruction)) {
|
if (isBlank(this._currentInstruction)) {
|
||||||
return PromiseWrapper.resolve(true);
|
return PromiseWrapper.resolve(true);
|
||||||
}
|
}
|
||||||
if (hasLifecycleHook(hookMod.canDeactivate, this._currentInstruction.component)) {
|
var outletInstruction = this._getInstruction(nextInstruction);
|
||||||
return PromiseWrapper.resolve(
|
if (hasLifecycleHook(hookMod.canDeactivate, this._currentInstruction.componentType)) {
|
||||||
this._componentRef.instance.canDeactivate(nextInstruction, this._currentInstruction));
|
return PromiseWrapper.resolve(this._componentRef.instance.canDeactivate(
|
||||||
|
isPresent(outletInstruction) ? outletInstruction.component : null,
|
||||||
|
this._currentInstruction));
|
||||||
}
|
}
|
||||||
return PromiseWrapper.resolve(true);
|
return PromiseWrapper.resolve(true);
|
||||||
}
|
}
|
||||||
@ -97,24 +113,34 @@ export class RouterOutlet {
|
|||||||
*/
|
*/
|
||||||
canReuse(nextInstruction: Instruction): Promise<boolean> {
|
canReuse(nextInstruction: Instruction): Promise<boolean> {
|
||||||
var result;
|
var result;
|
||||||
|
|
||||||
|
var outletInstruction = this._getInstruction(nextInstruction);
|
||||||
|
var componentInstruction = outletInstruction.component;
|
||||||
|
|
||||||
if (isBlank(this._currentInstruction) ||
|
if (isBlank(this._currentInstruction) ||
|
||||||
this._currentInstruction.component != nextInstruction.component) {
|
this._currentInstruction.componentType != componentInstruction.componentType) {
|
||||||
result = false;
|
result = false;
|
||||||
} else if (hasLifecycleHook(hookMod.canReuse, this._currentInstruction.component)) {
|
} else if (hasLifecycleHook(hookMod.canReuse, this._currentInstruction.componentType)) {
|
||||||
result = this._componentRef.instance.canReuse(nextInstruction, this._currentInstruction);
|
result = this._componentRef.instance.canReuse(componentInstruction, this._currentInstruction);
|
||||||
} else {
|
} else {
|
||||||
result = nextInstruction == this._currentInstruction ||
|
result =
|
||||||
StringMapWrapper.equals(nextInstruction.params(), this._currentInstruction.params());
|
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;
|
var previousInstruction = this._currentInstruction;
|
||||||
this._currentInstruction = instruction;
|
this._currentInstruction = instruction;
|
||||||
return PromiseWrapper.resolve(
|
return PromiseWrapper.resolve(
|
||||||
hasLifecycleHook(hookMod.onReuse, this._currentInstruction.component) ?
|
hasLifecycleHook(hookMod.onReuse, this._currentInstruction.componentType) ?
|
||||||
this._componentRef.instance.onReuse(instruction, previousInstruction) :
|
this._componentRef.instance.onReuse(instruction, previousInstruction) :
|
||||||
true);
|
true);
|
||||||
}
|
}
|
||||||
@ -122,14 +148,16 @@ export class RouterOutlet {
|
|||||||
|
|
||||||
|
|
||||||
deactivate(nextInstruction: Instruction): Promise<any> {
|
deactivate(nextInstruction: Instruction): Promise<any> {
|
||||||
|
var outletInstruction = this._getInstruction(nextInstruction);
|
||||||
|
var componentInstruction = isPresent(outletInstruction) ? outletInstruction.component : null;
|
||||||
return (isPresent(this.childRouter) ?
|
return (isPresent(this.childRouter) ?
|
||||||
this.childRouter.deactivate(isPresent(nextInstruction) ? nextInstruction.child :
|
this.childRouter.deactivate(isPresent(outletInstruction) ? outletInstruction.child :
|
||||||
null) :
|
null) :
|
||||||
PromiseWrapper.resolve(true))
|
PromiseWrapper.resolve(true))
|
||||||
.then((_) => {
|
.then((_) => {
|
||||||
if (isPresent(this._componentRef) && isPresent(this._currentInstruction) &&
|
if (isPresent(this._componentRef) && isPresent(this._currentInstruction) &&
|
||||||
hasLifecycleHook(hookMod.onDeactivate, this._currentInstruction.component)) {
|
hasLifecycleHook(hookMod.onDeactivate, this._currentInstruction.componentType)) {
|
||||||
return this._componentRef.instance.onDeactivate(nextInstruction,
|
return this._componentRef.instance.onDeactivate(componentInstruction,
|
||||||
this._currentInstruction);
|
this._currentInstruction);
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import {RegExpWrapper, StringWrapper} from 'angular2/src/facade/lang';
|
|
||||||
|
|
||||||
var specialCharacters = ['/', '.', '*', '+', '?', '|', '(', ')', '[', ']', '{', '}', '\\'];
|
|
||||||
|
|
||||||
var escapeRe = RegExpWrapper.create('(\\' + specialCharacters.join('|\\') + ')', 'g');
|
|
||||||
|
|
||||||
export function escapeRegex(string: string): string {
|
|
||||||
return StringWrapper.replaceAllMapped(string, escapeRe, (match) => { return "\\" + match; });
|
|
||||||
}
|
|
210
modules/angular2/src/router/url_parser.ts
Normal file
210
modules/angular2/src/router/url_parser.ts
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
import {List, StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||||
|
import {
|
||||||
|
isPresent,
|
||||||
|
isBlank,
|
||||||
|
BaseException,
|
||||||
|
RegExpWrapper,
|
||||||
|
CONST_EXPR
|
||||||
|
} from 'angular2/src/facade/lang';
|
||||||
|
|
||||||
|
|
||||||
|
export class Url {
|
||||||
|
constructor(public path: string, public child: Url = null,
|
||||||
|
public auxiliary: List<Url> = CONST_EXPR([]),
|
||||||
|
public params: StringMap<string, any> = null) {}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return this.path + this._matrixParamsToString() + this._auxToString() + this._childString();
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentToString(): string { return this.path + this._matrixParamsToString(); }
|
||||||
|
|
||||||
|
_auxToString(): string {
|
||||||
|
return this.auxiliary.length > 0 ?
|
||||||
|
('(' + this.auxiliary.map(sibling => sibling.toString()).join('//') + ')') :
|
||||||
|
'';
|
||||||
|
}
|
||||||
|
|
||||||
|
private _matrixParamsToString(): string {
|
||||||
|
if (isBlank(this.params)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return ';' + serializeParams(this.params).join(';');
|
||||||
|
}
|
||||||
|
|
||||||
|
_childString(): string { return isPresent(this.child) ? ('/' + this.child.toString()) : ''; }
|
||||||
|
}
|
||||||
|
|
||||||
|
export class RootUrl extends Url {
|
||||||
|
constructor(path: string, child: Url = null, auxiliary: List<Url> = CONST_EXPR([]),
|
||||||
|
params: StringMap<string, any> = null) {
|
||||||
|
super(path, child, auxiliary, params);
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
return this.path + this._auxToString() + this._childString() + this._queryParamsToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
segmentToString(): string { return this.path + this._queryParamsToString(); }
|
||||||
|
|
||||||
|
private _queryParamsToString(): string {
|
||||||
|
if (isBlank(this.params)) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return '?' + serializeParams(this.params).join('&');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var SEGMENT_RE = RegExpWrapper.create('^[^\\/\\(\\)\\?;=&]+');
|
||||||
|
function matchUrlSegment(str: string): string {
|
||||||
|
var match = RegExpWrapper.firstMatch(SEGMENT_RE, str);
|
||||||
|
return isPresent(match) ? match[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class UrlParser {
|
||||||
|
private remaining: string;
|
||||||
|
|
||||||
|
peekStartsWith(str: string): boolean { return this.remaining.startsWith(str); }
|
||||||
|
|
||||||
|
capture(str: string): void {
|
||||||
|
if (!this.remaining.startsWith(str)) {
|
||||||
|
throw new BaseException(`Expected "${str}".`);
|
||||||
|
}
|
||||||
|
this.remaining = this.remaining.substring(str.length);
|
||||||
|
}
|
||||||
|
|
||||||
|
parse(url: string): Url {
|
||||||
|
this.remaining = url;
|
||||||
|
if (url == '' || url == '/') {
|
||||||
|
return new Url('');
|
||||||
|
}
|
||||||
|
return this.parseRoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
// segment + (aux segments) + (query params)
|
||||||
|
parseRoot(): Url {
|
||||||
|
if (this.peekStartsWith('/')) {
|
||||||
|
this.capture('/');
|
||||||
|
}
|
||||||
|
var path = matchUrlSegment(this.remaining);
|
||||||
|
this.capture(path);
|
||||||
|
|
||||||
|
var aux = [];
|
||||||
|
if (this.peekStartsWith('(')) {
|
||||||
|
aux = this.parseAuxiliaryRoutes();
|
||||||
|
}
|
||||||
|
if (this.peekStartsWith(';')) {
|
||||||
|
// TODO: should these params just be dropped?
|
||||||
|
this.parseMatrixParams();
|
||||||
|
}
|
||||||
|
var child = null;
|
||||||
|
if (this.peekStartsWith('/') && !this.peekStartsWith('//')) {
|
||||||
|
this.capture('/');
|
||||||
|
child = this.parseSegment();
|
||||||
|
}
|
||||||
|
var queryParams = null;
|
||||||
|
if (this.peekStartsWith('?')) {
|
||||||
|
queryParams = this.parseQueryParams();
|
||||||
|
}
|
||||||
|
return new RootUrl(path, child, aux, queryParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
// segment + (matrix params) + (aux segments)
|
||||||
|
parseSegment(): Url {
|
||||||
|
if (this.remaining.length == 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (this.peekStartsWith('/')) {
|
||||||
|
this.capture('/');
|
||||||
|
}
|
||||||
|
var path = matchUrlSegment(this.remaining);
|
||||||
|
this.capture(path);
|
||||||
|
|
||||||
|
var matrixParams = null;
|
||||||
|
if (this.peekStartsWith(';')) {
|
||||||
|
matrixParams = this.parseMatrixParams();
|
||||||
|
}
|
||||||
|
var aux = [];
|
||||||
|
if (this.peekStartsWith('(')) {
|
||||||
|
aux = this.parseAuxiliaryRoutes();
|
||||||
|
}
|
||||||
|
var child = null;
|
||||||
|
if (this.peekStartsWith('/') && !this.peekStartsWith('//')) {
|
||||||
|
this.capture('/');
|
||||||
|
child = this.parseSegment();
|
||||||
|
}
|
||||||
|
return new Url(path, child, aux, matrixParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
parseQueryParams(): StringMap<string, any> {
|
||||||
|
var params = {};
|
||||||
|
this.capture('?');
|
||||||
|
this.parseParam(params);
|
||||||
|
while (this.remaining.length > 0 && this.peekStartsWith('&')) {
|
||||||
|
this.capture('&');
|
||||||
|
this.parseParam(params);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseMatrixParams(): StringMap<string, any> {
|
||||||
|
var params = {};
|
||||||
|
while (this.remaining.length > 0 && this.peekStartsWith(';')) {
|
||||||
|
this.capture(';');
|
||||||
|
this.parseParam(params);
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseParam(params: StringMap<string, any>): void {
|
||||||
|
var key = matchUrlSegment(this.remaining);
|
||||||
|
if (isBlank(key)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.capture(key);
|
||||||
|
var value: any = true;
|
||||||
|
if (this.peekStartsWith('=')) {
|
||||||
|
this.capture('=');
|
||||||
|
var valueMatch = matchUrlSegment(this.remaining);
|
||||||
|
if (isPresent(valueMatch)) {
|
||||||
|
value = valueMatch;
|
||||||
|
this.capture(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
params[key] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
parseAuxiliaryRoutes(): List<Url> {
|
||||||
|
var routes = [];
|
||||||
|
this.capture('(');
|
||||||
|
|
||||||
|
while (!this.peekStartsWith(')') && this.remaining.length > 0) {
|
||||||
|
routes.push(this.parseSegment());
|
||||||
|
if (this.peekStartsWith('//')) {
|
||||||
|
this.capture('//');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.capture(')');
|
||||||
|
|
||||||
|
return routes;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export var parser = new UrlParser();
|
||||||
|
|
||||||
|
export function serializeParams(paramMap: StringMap<string, any>): List<string> {
|
||||||
|
var params = [];
|
||||||
|
if (isPresent(paramMap)) {
|
||||||
|
StringMapWrapper.forEach(paramMap, (value, key) => {
|
||||||
|
if (value == true) {
|
||||||
|
params.push(key);
|
||||||
|
} else {
|
||||||
|
params.push(key + '=' + value);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return params;
|
||||||
|
}
|
@ -30,7 +30,13 @@ import {
|
|||||||
import {RootRouter} from 'angular2/src/router/router';
|
import {RootRouter} from 'angular2/src/router/router';
|
||||||
import {Pipeline} from 'angular2/src/router/pipeline';
|
import {Pipeline} from 'angular2/src/router/pipeline';
|
||||||
import {Router, RouterOutlet, RouterLink, RouteParams} from 'angular2/router';
|
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';
|
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||||
|
|
||||||
@ -45,10 +51,12 @@ import {
|
|||||||
CanReuse
|
CanReuse
|
||||||
} from 'angular2/src/router/interfaces';
|
} from 'angular2/src/router/interfaces';
|
||||||
import {CanActivate} from 'angular2/src/router/lifecycle_annotations';
|
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';
|
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>;
|
var completer: PromiseCompleter<any>;
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
@ -73,7 +81,7 @@ export function main() {
|
|||||||
rtr = router;
|
rtr = router;
|
||||||
location = loc;
|
location = loc;
|
||||||
cmpInstanceCount = 0;
|
cmpInstanceCount = 0;
|
||||||
log = '';
|
log = [];
|
||||||
eventBus = new EventEmitter();
|
eventBus = new EventEmitter();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -207,7 +215,6 @@ export function main() {
|
|||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
|
||||||
it('should generate link hrefs from a child to its sibling',
|
it('should generate link hrefs from a child to its sibling',
|
||||||
inject([AsyncTestCompleter], (async) => {
|
inject([AsyncTestCompleter], (async) => {
|
||||||
compile()
|
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',
|
describe('lifecycle hooks', () => {
|
||||||
inject([AsyncTestCompleter], (async) => {
|
it('should call the onActivate hook', inject([AsyncTestCompleter], (async) => {
|
||||||
compile()
|
compile()
|
||||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||||
.then((_) => {
|
.then((_) => rtr.navigate('/on-activate'))
|
||||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
.then((_) => {
|
||||||
if (ev.startsWith('parent activate')) {
|
rootTC.detectChanges();
|
||||||
completer.resolve(true);
|
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();
|
rootTC.detectChanges();
|
||||||
expect(rootTC.nativeElement).toHaveText('parent {activate cmp}');
|
expect(rootTC.nativeElement).toHaveText('parent {deactivate cmp}');
|
||||||
expect(log).toEqual(
|
}
|
||||||
'parent activate: null -> /parent-activate/child-activate;activate: null -> /child-activate;');
|
});
|
||||||
async.done();
|
rtr.navigate('/a').then((_) => {
|
||||||
});
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
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();
|
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();
|
rootTC.detectChanges();
|
||||||
expect(rootTC.nativeElement).toHaveText('A');
|
expect(log).toEqual([]);
|
||||||
expect(log).toEqual(
|
expect(rootTC.nativeElement).toHaveText('reuse {A}');
|
||||||
'deactivate: /child-deactivate -> null;parent deactivate: /parent-deactivate/child-deactivate -> /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();
|
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',
|
it('should not reuse a component when the canReuse hook returns false',
|
||||||
inject([AsyncTestCompleter], (async) => {
|
inject([AsyncTestCompleter], (async) => {
|
||||||
compile()
|
compile()
|
||||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||||
.then((_) => rtr.navigate('/never-reuse/1/a'))
|
.then((_) => rtr.navigate('/never-reuse/1/a'))
|
||||||
.then((_) => {
|
.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((_) => {
|
|
||||||
rootTC.detectChanges();
|
rootTC.detectChanges();
|
||||||
expect(rootTC.nativeElement).toHaveText('A');
|
expect(log).toEqual([]);
|
||||||
expect(log).toEqual('canDeactivate: /can-deactivate/a -> /a;');
|
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();
|
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) => {
|
it('should navigate when canActivate returns true', inject([AsyncTestCompleter], (async) => {
|
||||||
if (ev.startsWith('canDeactivate')) {
|
compile()
|
||||||
completer.resolve(false);
|
.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();
|
rootTC.detectChanges();
|
||||||
expect(rootTC.nativeElement).toHaveText('canDeactivate {A}');
|
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();
|
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',
|
log = [];
|
||||||
inject([AsyncTestCompleter], (async) => {
|
return rtr.navigate('/reuse-hooks/2');
|
||||||
compile()
|
})
|
||||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
.then((_) => {
|
||||||
.then((_) => rtr.navigate('/activation-hooks/child'))
|
expect(log).toEqual([
|
||||||
.then((_) => {
|
'canReuse: /reuse-hooks/1 -> /reuse-hooks/2',
|
||||||
expect(log).toEqual('canActivate child: null -> /child;' +
|
'onReuse: /reuse-hooks/1 -> /reuse-hooks/2'
|
||||||
'canActivate parent: null -> /activation-hooks/child;' +
|
]);
|
||||||
'onActivate parent: null -> /activation-hooks/child;' +
|
async.done();
|
||||||
'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 = '';
|
it('should not run reuse hooks when not reusing', inject([AsyncTestCompleter], (async) => {
|
||||||
return rtr.navigate('/reuse-hooks/2');
|
compile()
|
||||||
})
|
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||||
.then((_) => {
|
.then((_) => rtr.navigate('/reuse-hooks/1'))
|
||||||
expect(log).toEqual('canReuse: /reuse-hooks/1 -> /reuse-hooks/2;' +
|
.then((_) => {
|
||||||
'onReuse: /reuse-hooks/1 -> /reuse-hooks/2;');
|
expect(log).toEqual(
|
||||||
async.done();
|
['canActivate: null -> /reuse-hooks/1', 'onActivate: null -> /reuse-hooks/1']);
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should not run reuse hooks when not reusing', inject([AsyncTestCompleter], (async) => {
|
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||||
compile()
|
if (ev.startsWith('canReuse')) {
|
||||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
completer.resolve(false);
|
||||||
.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) => {
|
log = [];
|
||||||
if (ev.startsWith('canReuse')) {
|
return rtr.navigate('/reuse-hooks/2');
|
||||||
completer.resolve(false);
|
})
|
||||||
}
|
.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', () => {
|
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;
|
name;
|
||||||
}
|
}
|
||||||
|
|
||||||
function logHook(name: string, next: Instruction, prev: Instruction) {
|
function logHook(name: string, next: ComponentInstruction, prev: ComponentInstruction) {
|
||||||
var message = name + ': ' + (isPresent(prev) ? prev.accumulatedUrl : 'null') + ' -> ' +
|
var message = name + ': ' + (isPresent(prev) ? ('/' + prev.urlPath) : 'null') + ' -> ' +
|
||||||
(isPresent(next) ? next.accumulatedUrl : 'null') + ';';
|
(isPresent(next) ? ('/' + next.urlPath) : 'null');
|
||||||
log += message;
|
log.push(message);
|
||||||
ObservableWrapper.callNext(eventBus, message);
|
ObservableWrapper.callNext(eventBus, message);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({selector: 'activate-cmp'})
|
@Component({selector: 'activate-cmp'})
|
||||||
@View({template: 'activate cmp'})
|
@View({template: 'activate cmp'})
|
||||||
class ActivateCmp implements OnActivate {
|
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'})
|
@Component({selector: 'parent-activate-cmp'})
|
||||||
@View({template: `parent {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
|
@View({template: `parent {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
|
||||||
@RouteConfig([new Route({path: '/child-activate', component: ActivateCmp})])
|
@RouteConfig([new Route({path: '/child-activate', component: ActivateCmp})])
|
||||||
class ParentActivateCmp implements OnActivate {
|
class ParentActivateCmp implements OnActivate {
|
||||||
onActivate(next: Instruction, prev: Instruction): Promise<any> {
|
onActivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<any> {
|
||||||
completer = PromiseWrapper.completer();
|
completer = PromiseWrapper.completer();
|
||||||
logHook('parent activate', next, prev);
|
logHook('parent activate', next, prev);
|
||||||
return completer.promise;
|
return completer.promise;
|
||||||
@ -684,13 +724,15 @@ class ParentActivateCmp implements OnActivate {
|
|||||||
@Component({selector: 'deactivate-cmp'})
|
@Component({selector: 'deactivate-cmp'})
|
||||||
@View({template: 'deactivate cmp'})
|
@View({template: 'deactivate cmp'})
|
||||||
class DeactivateCmp implements OnDeactivate {
|
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'})
|
@Component({selector: 'deactivate-cmp'})
|
||||||
@View({template: 'deactivate cmp'})
|
@View({template: 'deactivate cmp'})
|
||||||
class WaitDeactivateCmp implements OnDeactivate {
|
class WaitDeactivateCmp implements OnDeactivate {
|
||||||
onDeactivate(next: Instruction, prev: Instruction): Promise<any> {
|
onDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<any> {
|
||||||
completer = PromiseWrapper.completer();
|
completer = PromiseWrapper.completer();
|
||||||
logHook('deactivate', next, prev);
|
logHook('deactivate', next, prev);
|
||||||
return completer.promise;
|
return completer.promise;
|
||||||
@ -701,7 +743,9 @@ class WaitDeactivateCmp implements OnDeactivate {
|
|||||||
@View({template: `parent {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
|
@View({template: `parent {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
|
||||||
@RouteConfig([new Route({path: '/child-deactivate', component: WaitDeactivateCmp})])
|
@RouteConfig([new Route({path: '/child-deactivate', component: WaitDeactivateCmp})])
|
||||||
class ParentDeactivateCmp implements OnDeactivate {
|
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'})
|
@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})])
|
@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})])
|
||||||
class ReuseCmp implements OnReuse, CanReuse {
|
class ReuseCmp implements OnReuse, CanReuse {
|
||||||
constructor() { cmpInstanceCount += 1; }
|
constructor() { cmpInstanceCount += 1; }
|
||||||
canReuse(next: Instruction, prev: Instruction) { return true; }
|
canReuse(next: ComponentInstruction, prev: ComponentInstruction) { return true; }
|
||||||
onReuse(next: Instruction, prev: Instruction) { logHook('reuse', next, prev); }
|
onReuse(next: ComponentInstruction, prev: ComponentInstruction) { logHook('reuse', next, prev); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({selector: 'never-reuse-cmp'})
|
@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})])
|
@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})])
|
||||||
class NeverReuseCmp implements OnReuse, CanReuse {
|
class NeverReuseCmp implements OnReuse, CanReuse {
|
||||||
constructor() { cmpInstanceCount += 1; }
|
constructor() { cmpInstanceCount += 1; }
|
||||||
canReuse(next: Instruction, prev: Instruction) { return false; }
|
canReuse(next: ComponentInstruction, prev: ComponentInstruction) { return false; }
|
||||||
onReuse(next: Instruction, prev: Instruction) { logHook('reuse', next, prev); }
|
onReuse(next: ComponentInstruction, prev: ComponentInstruction) { logHook('reuse', next, prev); }
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({selector: 'can-activate-cmp'})
|
@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})])
|
@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})])
|
||||||
@CanActivate(CanActivateCmp.canActivate)
|
@CanActivate(CanActivateCmp.canActivate)
|
||||||
class CanActivateCmp {
|
class CanActivateCmp {
|
||||||
static canActivate(next: Instruction, prev: Instruction) {
|
static canActivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<boolean> {
|
||||||
completer = PromiseWrapper.completer();
|
completer = PromiseWrapper.completer();
|
||||||
logHook('canActivate', next, prev);
|
logHook('canActivate', next, prev);
|
||||||
return completer.promise;
|
return completer.promise;
|
||||||
@ -738,7 +782,7 @@ class CanActivateCmp {
|
|||||||
@View({template: `canDeactivate {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
|
@View({template: `canDeactivate {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
|
||||||
@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})])
|
@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})])
|
||||||
class CanDeactivateCmp implements CanDeactivate {
|
class CanDeactivateCmp implements CanDeactivate {
|
||||||
canDeactivate(next: Instruction, prev: Instruction) {
|
canDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<boolean> {
|
||||||
completer = PromiseWrapper.completer();
|
completer = PromiseWrapper.completer();
|
||||||
logHook('canDeactivate', next, prev);
|
logHook('canDeactivate', next, prev);
|
||||||
return completer.promise;
|
return completer.promise;
|
||||||
@ -749,19 +793,23 @@ class CanDeactivateCmp implements CanDeactivate {
|
|||||||
@View({template: `child`})
|
@View({template: `child`})
|
||||||
@CanActivate(AllHooksChildCmp.canActivate)
|
@CanActivate(AllHooksChildCmp.canActivate)
|
||||||
class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate {
|
class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate {
|
||||||
canDeactivate(next: Instruction, prev: Instruction) {
|
canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||||
logHook('canDeactivate child', next, prev);
|
logHook('canDeactivate child', next, prev);
|
||||||
return true;
|
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);
|
logHook('canActivate child', next, prev);
|
||||||
return true;
|
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'})
|
@Component({selector: 'all-hooks-parent-cmp'})
|
||||||
@ -769,46 +817,56 @@ class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate {
|
|||||||
@RouteConfig([new Route({path: '/child', component: AllHooksChildCmp})])
|
@RouteConfig([new Route({path: '/child', component: AllHooksChildCmp})])
|
||||||
@CanActivate(AllHooksParentCmp.canActivate)
|
@CanActivate(AllHooksParentCmp.canActivate)
|
||||||
class AllHooksParentCmp implements CanDeactivate, OnDeactivate, OnActivate {
|
class AllHooksParentCmp implements CanDeactivate, OnDeactivate, OnActivate {
|
||||||
canDeactivate(next: Instruction, prev: Instruction) {
|
canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||||
logHook('canDeactivate parent', next, prev);
|
logHook('canDeactivate parent', next, prev);
|
||||||
return true;
|
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);
|
logHook('canActivate parent', next, prev);
|
||||||
return true;
|
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'})
|
@Component({selector: 'reuse-hooks-cmp'})
|
||||||
@View({template: 'reuse hooks cmp'})
|
@View({template: 'reuse hooks cmp'})
|
||||||
@CanActivate(ReuseHooksCmp.canActivate)
|
@CanActivate(ReuseHooksCmp.canActivate)
|
||||||
class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanDeactivate {
|
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();
|
completer = PromiseWrapper.completer();
|
||||||
logHook('canReuse', next, prev);
|
logHook('canReuse', next, prev);
|
||||||
return completer.promise;
|
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);
|
logHook('canDeactivate', next, prev);
|
||||||
return true;
|
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);
|
logHook('canActivate', next, prev);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
onActivate(next: Instruction, prev: Instruction) { logHook('onActivate', next, prev); }
|
onActivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||||
|
logHook('onActivate', next, prev);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Component({selector: 'lifecycle-cmp'})
|
@Component({selector: 'lifecycle-cmp'})
|
||||||
@ -828,3 +886,21 @@ class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanD
|
|||||||
])
|
])
|
||||||
class LifecycleCmp {
|
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';
|
} from 'angular2/test_lib';
|
||||||
|
|
||||||
import {PathRecognizer} from 'angular2/src/router/path_recognizer';
|
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';
|
import {SyncRouteHandler} from 'angular2/src/router/sync_route_handler';
|
||||||
|
|
||||||
class DummyClass {
|
class DummyClass {
|
||||||
@ -41,65 +42,60 @@ export function main() {
|
|||||||
|
|
||||||
describe('querystring params', () => {
|
describe('querystring params', () => {
|
||||||
it('should parse querystring params so long as the recognizer is a root', () => {
|
it('should parse querystring params so long as the recognizer is a root', () => {
|
||||||
var rec = new PathRecognizer('/hello/there', mockRouteHandler, true);
|
var rec = new PathRecognizer('/hello/there', mockRouteHandler);
|
||||||
var params = rec.parseParams('/hello/there?name=igor');
|
var url = parser.parse('/hello/there?name=igor');
|
||||||
expect(params).toEqual({'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',
|
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 rec = new PathRecognizer('/hello/:name', mockRouteHandler);
|
||||||
var params = rec.parseParams('/hello/paul?topic=success');
|
var url = parser.parse('/hello/paul?topic=success');
|
||||||
expect(params).toEqual({'name': 'paul', 'topic': 'success'});
|
var match = rec.recognize(url);
|
||||||
|
expect(match.instruction.params).toEqual({'name': 'paul', 'topic': 'success'});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('matrix params', () => {
|
describe('matrix params', () => {
|
||||||
it('should recognize a trailing matrix value on a path value and assign it to the params return value',
|
it('should be parsed along with dynamic paths', () => {
|
||||||
() => {
|
var rec = new PathRecognizer('/hello/:id', mockRouteHandler);
|
||||||
var rec = new PathRecognizer('/hello/:id', mockRouteHandler);
|
var url = new Url('hello', new Url('matias', null, null, {'key': 'value'}));
|
||||||
var params = rec.parseParams('/hello/matias;key=value');
|
var match = rec.recognize(url);
|
||||||
|
expect(match.instruction.params).toEqual({'id': '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 recognize a matrix param value on a static path value', () => {
|
it('should be parsed on a static path', () => {
|
||||||
var rec = new PathRecognizer('/static/man', mockRouteHandler);
|
var rec = new PathRecognizer('/person', mockRouteHandler);
|
||||||
var params = rec.parseParams('/static/man;name=dave');
|
var url = new Url('person', null, null, {'name': 'dave'});
|
||||||
expect(params['name']).toEqual('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 rec = new PathRecognizer('/wild/*everything', mockRouteHandler);
|
||||||
var params = rec.parseParams('/wild/super;variable=value');
|
var url = parser.parse('/wild/super;variable=value');
|
||||||
expect(params['everything']).toEqual('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',
|
it('should set matrix param values to true when no value is present', () => {
|
||||||
() => {
|
var rec = new PathRecognizer('/path', mockRouteHandler);
|
||||||
var rec = new PathRecognizer('/path', mockRouteHandler);
|
var url = new Url('path', null, null, {'one': true, 'two': true, 'three': '3'});
|
||||||
var params = rec.parseParams('/path;one;two;three=3');
|
var match = rec.recognize(url);
|
||||||
expect(params['one']).toEqual(true);
|
expect(match.instruction.params).toEqual({'one': true, 'two': true, 'three': '3'});
|
||||||
expect(params['two']).toEqual(true);
|
});
|
||||||
expect(params['three']).toEqual('3');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should ignore earlier instances of matrix params and only consider the ones at the end of the path',
|
it('should be parsed on the final segment of the path', () => {
|
||||||
() => {
|
var rec = new PathRecognizer('/one/two/three', mockRouteHandler);
|
||||||
var rec = new PathRecognizer('/one/two/three', mockRouteHandler);
|
|
||||||
var params = rec.parseParams('/one;a=1/two;b=2/three;c=3');
|
var three = new Url('three', null, null, {'c': '3'});
|
||||||
expect(params).toEqual({'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 {
|
class HierarchyAppCmp {
|
||||||
constructor(public router: Router, public location: LocationStrategy) {}
|
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 {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 {Route, Redirect} from 'angular2/src/router/route_config_decorator';
|
||||||
|
import {parser} from 'angular2/src/router/url_parser';
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('RouteRecognizer', () => {
|
describe('RouteRecognizer', () => {
|
||||||
@ -25,31 +27,31 @@ export function main() {
|
|||||||
|
|
||||||
it('should recognize a static segment', () => {
|
it('should recognize a static segment', () => {
|
||||||
recognizer.config(new Route({path: '/test', component: DummyCmpA}));
|
recognizer.config(new Route({path: '/test', component: DummyCmpA}));
|
||||||
var solution = recognizer.recognize('/test')[0];
|
var solution = recognize(recognizer, '/test');
|
||||||
expect(getComponentType(solution)).toEqual(DummyCmpA);
|
expect(getComponentType(solution)).toEqual(DummyCmpA);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should recognize a single slash', () => {
|
it('should recognize a single slash', () => {
|
||||||
recognizer.config(new Route({path: '/', component: DummyCmpA}));
|
recognizer.config(new Route({path: '/', component: DummyCmpA}));
|
||||||
var solution = recognizer.recognize('/')[0];
|
var solution = recognize(recognizer, '/');
|
||||||
expect(getComponentType(solution)).toEqual(DummyCmpA);
|
expect(getComponentType(solution)).toEqual(DummyCmpA);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should recognize a dynamic segment', () => {
|
it('should recognize a dynamic segment', () => {
|
||||||
recognizer.config(new Route({path: '/user/:name', component: DummyCmpA}));
|
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(getComponentType(solution)).toEqual(DummyCmpA);
|
||||||
expect(solution.params()).toEqual({'name': 'brian'});
|
expect(solution.params).toEqual({'name': 'brian'});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should recognize a star segment', () => {
|
it('should recognize a star segment', () => {
|
||||||
recognizer.config(new Route({path: '/first/*rest', component: DummyCmpA}));
|
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(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', () => {
|
it('should recognize redirects', () => {
|
||||||
recognizer.config(new Redirect({path: '/a', redirectTo: '/b'}));
|
|
||||||
recognizer.config(new Route({path: '/b', component: DummyCmpA}));
|
recognizer.config(new Route({path: '/b', component: DummyCmpA}));
|
||||||
var solutions = recognizer.recognize('/a');
|
recognizer.config(new Redirect({path: '/a', redirectTo: 'b'}));
|
||||||
expect(solutions.length).toBe(1);
|
var solution = recognize(recognizer, '/a');
|
||||||
|
|
||||||
var solution = solutions[0];
|
|
||||||
expect(getComponentType(solution)).toEqual(DummyCmpA);
|
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', () => {
|
it('should not perform root URL redirect on a non-root route', () => {
|
||||||
recognizer.config(new Redirect({path: '/', redirectTo: '/foo'}));
|
recognizer.config(new Redirect({path: '/', redirectTo: '/foo'}));
|
||||||
recognizer.config(new Route({path: '/bar', component: DummyCmpA}));
|
recognizer.config(new Route({path: '/bar', component: DummyCmpA}));
|
||||||
var solutions = recognizer.recognize('/bar');
|
var solution = recognize(recognizer, '/bar');
|
||||||
expect(solutions.length).toBe(1);
|
expect(solution.componentType).toEqual(DummyCmpA);
|
||||||
|
expect(solution.urlPath).toEqual('bar');
|
||||||
var solution = solutions[0];
|
|
||||||
expect(getComponentType(solution)).toEqual(DummyCmpA);
|
|
||||||
expect(solution.matchedUrl).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('/');
|
var solution;
|
||||||
expect(solutions[0].matchedUrl).toBe('/matias');
|
|
||||||
|
|
||||||
solutions = recognizer.recognize('/fatias');
|
solution = recognize(recognizer, '/');
|
||||||
expect(solutions[0].matchedUrl).toBe('/fatias');
|
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', () => {
|
it('should generate URLs with params', () => {
|
||||||
recognizer.config(new Route({path: '/app/user/:name', component: DummyCmpA, as: 'user'}));
|
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', () => {
|
it('should generate URLs with numeric params', () => {
|
||||||
recognizer.config(new Route({path: '/app/page/:number', component: DummyCmpA, as: 'page'}));
|
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', () => {
|
it('should throw in the absence of required params URLs', () => {
|
||||||
recognizer.config(new Route({path: 'app/user/:name', component: DummyCmpA, as: 'user'}));
|
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.');
|
.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];
|
describe('params', () => {
|
||||||
var params = solution.params();
|
it('should recognize parameters within the URL path', () => {
|
||||||
expect(params['name']).toEqual('matsko');
|
recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, as: 'user'}));
|
||||||
expect(params['comments']).toEqual('all');
|
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',
|
it('should generate and populate the given static-based route with querystring params',
|
||||||
() => {
|
() => {
|
||||||
var recognizer = new RouteRecognizer(true);
|
|
||||||
recognizer.config(
|
recognizer.config(
|
||||||
new Route({path: 'forum/featured', component: DummyCmpA, as: 'forum-page'}));
|
new Route({path: 'forum/featured', component: DummyCmpA, as: 'forum-page'}));
|
||||||
|
|
||||||
var params = StringMapWrapper.create();
|
var params = {'start': 10, 'end': 100};
|
||||||
params['start'] = 10;
|
|
||||||
params['end'] = 100;
|
|
||||||
|
|
||||||
var result = recognizer.generate('forum-page', params);
|
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];
|
it('should prefer positional params over query params', () => {
|
||||||
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();
|
|
||||||
recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, as: 'user'}));
|
recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, as: 'user'}));
|
||||||
|
|
||||||
var solution = recognizer.recognize('/profile/matsko;comments=all')[0];
|
var solution = recognize(recognizer, '/profile/yegor?name=igor');
|
||||||
var params = solution.params();
|
expect(solution.params).toEqual({'name': 'yegor'});
|
||||||
expect(params['name']).toEqual('matsko');
|
|
||||||
expect(params['comments']).toEqual('all');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
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 =
|
it('should ignore matrix params for the top-level component', () => {
|
||||||
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();
|
|
||||||
recognizer.config(new Route({path: '/home/:subject', component: DummyCmpA, as: 'user'}));
|
recognizer.config(new Route({path: '/home/:subject', component: DummyCmpA, as: 'user'}));
|
||||||
|
var solution = recognize(recognizer, '/home;sort=asc/zero;one=1?two=2');
|
||||||
var solution = recognizer.recognize('/home/zero;one=1?two=2')[0];
|
expect(solution.params).toEqual({'subject': 'zero', 'two': '2'});
|
||||||
var params = solution.params();
|
|
||||||
|
|
||||||
expect(params['subject']).toEqual('zero');
|
|
||||||
expect(params['one']).toEqual('1');
|
|
||||||
expect(params['two']).toBeFalsy();
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getComponentType(routeMatch: RouteMatch): any {
|
function recognize(recognizer: RouteRecognizer, url: string): ComponentInstruction {
|
||||||
return routeMatch.recognizer.handler.componentType;
|
return recognizer.recognize(parser.parse(url))[0].instruction;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getComponentType(routeMatch: ComponentInstruction): any {
|
||||||
|
return routeMatch.componentType;
|
||||||
}
|
}
|
||||||
|
|
||||||
class DummyCmpA {}
|
class DummyCmpA {}
|
||||||
|
@ -14,6 +14,7 @@ import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
|||||||
|
|
||||||
import {RouteRegistry} from 'angular2/src/router/route_registry';
|
import {RouteRegistry} from 'angular2/src/router/route_registry';
|
||||||
import {RouteConfig, Route, AsyncRoute} from 'angular2/src/router/route_config_decorator';
|
import {RouteConfig, Route, AsyncRoute} from 'angular2/src/router/route_config_decorator';
|
||||||
|
import {stringifyInstruction} from 'angular2/src/router/instruction';
|
||||||
|
|
||||||
export function main() {
|
export function main() {
|
||||||
describe('RouteRegistry', () => {
|
describe('RouteRegistry', () => {
|
||||||
@ -27,7 +28,7 @@ export function main() {
|
|||||||
|
|
||||||
registry.recognize('/test', RootHostCmp)
|
registry.recognize('/test', RootHostCmp)
|
||||||
.then((instruction) => {
|
.then((instruction) => {
|
||||||
expect(instruction.component).toBe(DummyCmpB);
|
expect(instruction.component.componentType).toBe(DummyCmpB);
|
||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
@ -36,8 +37,10 @@ export function main() {
|
|||||||
registry.config(RootHostCmp,
|
registry.config(RootHostCmp,
|
||||||
new Route({path: '/first/...', component: DummyParentCmp, as: 'firstCmp'}));
|
new Route({path: '/first/...', component: DummyParentCmp, as: 'firstCmp'}));
|
||||||
|
|
||||||
expect(registry.generate(['firstCmp', 'secondCmp'], RootHostCmp)).toEqual('first/second');
|
expect(stringifyInstruction(registry.generate(['firstCmp', 'secondCmp'], RootHostCmp)))
|
||||||
expect(registry.generate(['secondCmp'], DummyParentCmp)).toEqual('second');
|
.toEqual('first/second');
|
||||||
|
expect(stringifyInstruction(registry.generate(['secondCmp'], DummyParentCmp)))
|
||||||
|
.toEqual('second');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate URLs with params', () => {
|
it('should generate URLs with params', () => {
|
||||||
@ -45,8 +48,8 @@ export function main() {
|
|||||||
RootHostCmp,
|
RootHostCmp,
|
||||||
new Route({path: '/first/:param/...', component: DummyParentParamCmp, as: 'firstCmp'}));
|
new Route({path: '/first/:param/...', component: DummyParentParamCmp, as: 'firstCmp'}));
|
||||||
|
|
||||||
var url =
|
var url = stringifyInstruction(registry.generate(
|
||||||
registry.generate(['firstCmp', {param: 'one'}, 'secondCmp', {param: 'two'}], RootHostCmp);
|
['firstCmp', {param: 'one'}, 'secondCmp', {param: 'two'}], RootHostCmp));
|
||||||
expect(url).toEqual('first/one/second/two');
|
expect(url).toEqual('first/one/second/two');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -61,7 +64,8 @@ export function main() {
|
|||||||
|
|
||||||
registry.recognize('/first/second', RootHostCmp)
|
registry.recognize('/first/second', RootHostCmp)
|
||||||
.then((_) => {
|
.then((_) => {
|
||||||
expect(registry.generate(['firstCmp', 'secondCmp'], RootHostCmp))
|
expect(
|
||||||
|
stringifyInstruction(registry.generate(['firstCmp', 'secondCmp'], RootHostCmp)))
|
||||||
.toEqual('first/second');
|
.toEqual('first/second');
|
||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
@ -73,14 +77,13 @@ export function main() {
|
|||||||
.toThrowError('Component "RootHostCmp" has no route config.');
|
.toThrowError('Component "RootHostCmp" has no route config.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
it('should prefer static segments to dynamic', inject([AsyncTestCompleter], (async) => {
|
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: '/:site', component: DummyCmpB}));
|
||||||
registry.config(RootHostCmp, new Route({path: '/home', component: DummyCmpA}));
|
registry.config(RootHostCmp, new Route({path: '/home', component: DummyCmpA}));
|
||||||
|
|
||||||
registry.recognize('/home', RootHostCmp)
|
registry.recognize('/home', RootHostCmp)
|
||||||
.then((instruction) => {
|
.then((instruction) => {
|
||||||
expect(instruction.component).toBe(DummyCmpA);
|
expect(instruction.component.componentType).toBe(DummyCmpA);
|
||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
@ -91,7 +94,7 @@ export function main() {
|
|||||||
|
|
||||||
registry.recognize('/home', RootHostCmp)
|
registry.recognize('/home', RootHostCmp)
|
||||||
.then((instruction) => {
|
.then((instruction) => {
|
||||||
expect(instruction.component).toBe(DummyCmpA);
|
expect(instruction.component.componentType).toBe(DummyCmpA);
|
||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
@ -102,7 +105,7 @@ export function main() {
|
|||||||
|
|
||||||
registry.recognize('/some/path', RootHostCmp)
|
registry.recognize('/some/path', RootHostCmp)
|
||||||
.then((instruction) => {
|
.then((instruction) => {
|
||||||
expect(instruction.component).toBe(DummyCmpA);
|
expect(instruction.component.componentType).toBe(DummyCmpA);
|
||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
@ -113,7 +116,7 @@ export function main() {
|
|||||||
|
|
||||||
registry.recognize('/first/second', RootHostCmp)
|
registry.recognize('/first/second', RootHostCmp)
|
||||||
.then((instruction) => {
|
.then((instruction) => {
|
||||||
expect(instruction.component).toBe(DummyCmpA);
|
expect(instruction.component.componentType).toBe(DummyCmpA);
|
||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
@ -127,7 +130,7 @@ export function main() {
|
|||||||
|
|
||||||
registry.recognize('/first/second/third', RootHostCmp)
|
registry.recognize('/first/second/third', RootHostCmp)
|
||||||
.then((instruction) => {
|
.then((instruction) => {
|
||||||
expect(instruction.component).toBe(DummyCmpB);
|
expect(instruction.component.componentType).toBe(DummyCmpB);
|
||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
@ -137,8 +140,8 @@ export function main() {
|
|||||||
|
|
||||||
registry.recognize('/first/second', RootHostCmp)
|
registry.recognize('/first/second', RootHostCmp)
|
||||||
.then((instruction) => {
|
.then((instruction) => {
|
||||||
expect(instruction.component).toBe(DummyParentCmp);
|
expect(instruction.component.componentType).toBe(DummyParentCmp);
|
||||||
expect(instruction.child.component).toBe(DummyCmpB);
|
expect(instruction.child.component.componentType).toBe(DummyCmpB);
|
||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
@ -149,8 +152,8 @@ export function main() {
|
|||||||
|
|
||||||
registry.recognize('/first/second', RootHostCmp)
|
registry.recognize('/first/second', RootHostCmp)
|
||||||
.then((instruction) => {
|
.then((instruction) => {
|
||||||
expect(instruction.component).toBe(DummyAsyncCmp);
|
expect(instruction.component.componentType).toBe(DummyAsyncCmp);
|
||||||
expect(instruction.child.component).toBe(DummyCmpB);
|
expect(instruction.child.component.componentType).toBe(DummyCmpB);
|
||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
@ -162,28 +165,12 @@ export function main() {
|
|||||||
|
|
||||||
registry.recognize('/first/second', RootHostCmp)
|
registry.recognize('/first/second', RootHostCmp)
|
||||||
.then((instruction) => {
|
.then((instruction) => {
|
||||||
expect(instruction.component).toBe(DummyParentCmp);
|
expect(instruction.component.componentType).toBe(DummyParentCmp);
|
||||||
expect(instruction.child.component).toBe(DummyCmpB);
|
expect(instruction.child.component.componentType).toBe(DummyCmpB);
|
||||||
async.done();
|
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',
|
it('should throw when a parent config is missing the `...` suffix any of its children add routes',
|
||||||
() => {
|
() => {
|
||||||
expect(() =>
|
expect(() =>
|
||||||
@ -198,6 +185,40 @@ export function main() {
|
|||||||
.toThrowError('Unexpected "..." before the end of the path for "home/.../fun/".');
|
.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 {bind, Component, View} from 'angular2/angular2';
|
||||||
|
|
||||||
import {Location, Router, RouterLink} from 'angular2/router';
|
import {Location, Router, RouterLink} from 'angular2/router';
|
||||||
|
import {Instruction, ComponentInstruction} from 'angular2/src/router/instruction';
|
||||||
|
|
||||||
import {
|
import {DOM} from 'angular2/src/dom/dom_adapter';
|
||||||
DOM
|
|
||||||
} from 'angular2/src/dom/dom_adapter'
|
|
||||||
|
|
||||||
|
var dummyInstruction = new Instruction(new ComponentInstruction('detail', [], null), null, {});
|
||||||
|
|
||||||
export function
|
export function main() {
|
||||||
main() {
|
|
||||||
describe('router-link directive', function() {
|
describe('router-link directive', function() {
|
||||||
|
|
||||||
beforeEachBindings(
|
beforeEachBindings(
|
||||||
@ -59,7 +58,7 @@ import {
|
|||||||
testComponent.detectChanges();
|
testComponent.detectChanges();
|
||||||
// TODO: shouldn't this be just 'click' rather than '^click'?
|
// TODO: shouldn't this be just 'click' rather than '^click'?
|
||||||
testComponent.query(By.css('a')).triggerEventHandler('^click', {});
|
testComponent.query(By.css('a')).triggerEventHandler('^click', {});
|
||||||
expect(router.spy("navigate")).toHaveBeenCalledWith('/detail');
|
expect(router.spy('navigateInstruction')).toHaveBeenCalledWith(dummyInstruction);
|
||||||
async.done();
|
async.done();
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
@ -100,7 +99,7 @@ class DummyRouter extends SpyObject {
|
|||||||
|
|
||||||
function makeDummyRouter() {
|
function makeDummyRouter() {
|
||||||
var dr = new DummyRouter();
|
var dr = new DummyRouter();
|
||||||
dr.spy('generate').andCallFake((routeParams) => routeParams.join('='));
|
dr.spy('generate').andCallFake((routeParams) => dummyInstruction);
|
||||||
dr.spy('navigate');
|
dr.spy('navigateInstruction');
|
||||||
return dr;
|
return dr;
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import {Pipeline} from 'angular2/src/router/pipeline';
|
|||||||
import {RouterOutlet} from 'angular2/src/router/router_outlet';
|
import {RouterOutlet} from 'angular2/src/router/router_outlet';
|
||||||
import {SpyLocation} from 'angular2/src/mock/location_mock';
|
import {SpyLocation} from 'angular2/src/mock/location_mock';
|
||||||
import {Location} from 'angular2/src/router/location';
|
import {Location} from 'angular2/src/router/location';
|
||||||
|
import {stringifyInstruction} from 'angular2/src/router/instruction';
|
||||||
|
|
||||||
import {RouteRegistry} from 'angular2/src/router/route_registry';
|
import {RouteRegistry} from 'angular2/src/router/route_registry';
|
||||||
import {RouteConfig, Route} from 'angular2/src/router/route_config_decorator';
|
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 /', () => {
|
it('should generate URLs from the root component when the path starts with /', () => {
|
||||||
router.config([new Route({path: '/first/...', component: DummyParentComp, as: 'firstCmp'})]);
|
router.config([new Route({path: '/first/...', component: DummyParentComp, as: 'firstCmp'})]);
|
||||||
|
|
||||||
expect(router.generate(['/firstCmp', 'secondCmp'])).toEqual('/first/second');
|
var instruction = router.generate(['/firstCmp', 'secondCmp']);
|
||||||
expect(router.generate(['/firstCmp', 'secondCmp'])).toEqual('/first/second');
|
expect(stringifyInstruction(instruction)).toEqual('first/second');
|
||||||
expect(router.generate(['/firstCmp/secondCmp'])).toEqual('/first/second');
|
|
||||||
|
instruction = router.generate(['/firstCmp/secondCmp']);
|
||||||
|
expect(stringifyInstruction(instruction)).toEqual('first/second');
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('querstring params', () => {
|
describe('query string params', () => {
|
||||||
it('should only apply querystring params if the given URL is on the root router and is terminal',
|
it('should use query string params for the root route', () => {
|
||||||
() => {
|
router.config(
|
||||||
router.config([
|
[new Route({path: '/hi/how/are/you', component: DummyComponent, as: 'greeting-url'})]);
|
||||||
new Route({path: '/hi/how/are/you', component: DummyComponent, as: 'greeting-url'})
|
|
||||||
]);
|
|
||||||
|
|
||||||
var path = router.generate(['/greeting-url', {'name': 'brad'}]);
|
var instruction = router.generate(['/greeting-url', {'name': 'brad'}]);
|
||||||
expect(path).toEqual('/hi/how/are/you?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(
|
router.config(
|
||||||
[new Route({path: '/one/two/:three', component: DummyComponent, as: 'number-url'})]);
|
[new Route({path: '/one/two/:three', component: DummyComponent, as: 'number-url'})]);
|
||||||
|
|
||||||
var path = router.generate(['/number-url', {'three': 'three', 'four': 'four'}]);
|
var instruction = router.generate(['/number-url', {'three': 'three', 'four': 'four'}]);
|
||||||
expect(path).toEqual('/one/two/three?four=four');
|
var path = stringifyInstruction(instruction);
|
||||||
|
expect(path).toEqual('one/two/three?four=four');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('matrix params', () => {
|
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(
|
router.config(
|
||||||
[new Route({path: '/first/...', component: DummyParentComp, as: 'firstCmp'})]);
|
[new Route({path: '/first/...', component: DummyParentComp, as: 'firstCmp'})]);
|
||||||
|
|
||||||
var path =
|
var instruction =
|
||||||
router.generate(['/firstCmp', {'key': 'value'}, 'secondCmp', {'project': 'angular'}]);
|
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',
|
it('should work with named params', () => {
|
||||||
() => {
|
router.config(
|
||||||
router.config([
|
[new Route({path: '/first/:token/...', component: DummyParentComp, as: 'firstCmp'})]);
|
||||||
new Route({path: '/first/:token/...', component: DummyParentComp, as: 'firstCmp'})
|
|
||||||
]);
|
|
||||||
|
|
||||||
var path =
|
var instruction =
|
||||||
router.generate(['/firstCmp', {'token': 'min'}, 'secondCmp', {'author': 'max'}]);
|
router.generate(['/firstCmp', {'token': 'min'}, 'secondCmp', {'author': 'max'}]);
|
||||||
expect(path).toEqual('/first/min/second;author=max');
|
var path = stringifyInstruction(instruction);
|
||||||
});
|
expect(path).toEqual('first/min/second;author=max');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
118
modules/angular2/test/router/url_parser_spec.ts
Normal file
118
modules/angular2/test/router/url_parser_spec.ts
Normal file
@ -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…
x
Reference in New Issue
Block a user