Revert "refactor(router): improve recognition and generation pipeline"

This reverts commit cf7292fcb1.

This commit triggered an existing race condition in Google code. More work is needed on the Router to fix this condition before this refactor can land.
This commit is contained in:
Alex Rickabaugh 2015-11-23 16:26:47 -08:00
parent ba64b5ea5a
commit c5294c77d9
41 changed files with 1117 additions and 2970 deletions

View File

@ -6,13 +6,12 @@ var ts = require('typescript');
var files = [
'lifecycle_annotations_impl.ts',
'url_parser.ts',
'route_recognizer.ts',
'path_recognizer.ts',
'route_config_impl.ts',
'async_route_handler.ts',
'sync_route_handler.ts',
'component_recognizer.ts',
'route_recognizer.ts',
'instruction.ts',
'path_recognizer.ts',
'route_config_nomalizer.ts',
'route_lifecycle_reflector.ts',
'route_registry.ts',

View File

@ -173,10 +173,6 @@ var StringMapWrapper = {
var List = Array;
var ListWrapper = {
clear: function (l) {
l.length = 0;
},
create: function () {
return [];
},

View File

@ -31,9 +31,7 @@ function routerFactory($q, $location, $$directiveIntrospector, $browser, $rootSc
// property in a route config
exports.assertComponentExists = function () {};
angular.stringifyInstruction = function (instruction) {
return instruction.toRootUrl();
};
angular.stringifyInstruction = exports.stringifyInstruction;
var RouteRegistry = exports.RouteRegistry;
var RootRouter = exports.RootRouter;

View File

@ -110,7 +110,7 @@
routeMap[path] = routeCopy;
if (route.redirectTo) {
routeDefinition.redirectTo = [routeMap[route.redirectTo].name];
routeDefinition.redirectTo = route.redirectTo;
} else {
if (routeCopy.controller && !routeCopy.controllerAs) {
console.warn('Route for "' + path + '" should use "controllerAs".');
@ -123,7 +123,7 @@
}
routeDefinition.component = directiveName;
routeDefinition.name = route.name || upperCase(directiveName);
routeDefinition.as = upperCase(directiveName);
var directiveController = routeCopy.controller;

View File

@ -113,7 +113,8 @@ describe('navigation', function () {
});
it('should work with recursive nested outlets', function () {
// TODO: fix this
xit('should work with recursive nested outlets', function () {
registerComponent('recurCmp', {
template: '<div>recur { <div ng-outlet></div> }</div>',
$routeConfig: [
@ -151,8 +152,8 @@ describe('navigation', function () {
compile('<div ng-outlet></div>');
$router.config([
{ path: '/', redirectTo: ['/User'] },
{ path: '/user', component: 'userCmp', name: 'User' }
{ path: '/', redirectTo: '/user' },
{ path: '/user', component: 'userCmp' }
]);
$router.navigateByUrl('/');
@ -166,15 +167,16 @@ describe('navigation', function () {
registerComponent('childRouter', {
template: '<div>inner { <div ng-outlet></div> }</div>',
$routeConfig: [
{ path: '/new-child', component: 'oneCmp', name: 'NewChild'},
{ path: '/new-child-two', component: 'twoCmp', name: 'NewChildTwo'}
{ path: '/old-child', redirectTo: '/new-child' },
{ path: '/new-child', component: 'oneCmp'},
{ path: '/old-child-two', redirectTo: '/new-child-two' },
{ path: '/new-child-two', component: 'twoCmp'}
]
});
$router.config([
{ path: '/old-parent/old-child', redirectTo: ['/NewParent', 'NewChild'] },
{ path: '/old-parent/old-child-two', redirectTo: ['/NewParent', 'NewChildTwo'] },
{ path: '/new-parent/...', component: 'childRouter', name: 'NewParent' }
{ path: '/old-parent', redirectTo: '/new-parent' },
{ path: '/new-parent/...', component: 'childRouter' }
]);
compile('<div ng-outlet></div>');

View File

@ -139,12 +139,11 @@ describe('ngRoute shim', function () {
it('should adapt routes with redirects', inject(function ($location) {
$routeProvider
.when('/home', {
template: 'welcome home!',
name: 'Home'
})
.when('/', {
redirectTo: '/home'
})
.when('/home', {
template: 'welcome home!'
});
$rootScope.$digest();

View File

@ -1,19 +1,13 @@
import {RouteHandler} from './route_handler';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {isPresent, Type} from 'angular2/src/facade/lang';
import {RouteHandler} from './route_handler';
import {RouteData, BLANK_ROUTE_DATA} from './instruction';
export class AsyncRouteHandler implements RouteHandler {
/** @internal */
_resolvedComponent: Promise<any> = null;
componentType: Type;
public data: RouteData;
constructor(private _loader: Function, data: {[key: string]: any} = null) {
this.data = isPresent(data) ? new RouteData(data) : BLANK_ROUTE_DATA;
}
constructor(private _loader: Function, public data?: {[key: string]: any}) {}
resolveComponentType(): Promise<any> {
if (isPresent(this._resolvedComponent)) {

View File

@ -1,157 +0,0 @@
import {isBlank, isPresent} from 'angular2/src/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
import {Map, MapWrapper, ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {
AbstractRecognizer,
RouteRecognizer,
RedirectRecognizer,
RouteMatch
} from './route_recognizer';
import {Route, AsyncRoute, AuxRoute, Redirect, RouteDefinition} from './route_config_impl';
import {AsyncRouteHandler} from './async_route_handler';
import {SyncRouteHandler} from './sync_route_handler';
import {Url} from './url_parser';
import {ComponentInstruction} from './instruction';
/**
* `ComponentRecognizer` is responsible for recognizing routes for a single component.
* It is consumed by `RouteRegistry`, which knows how to recognize an entire hierarchy of
* components.
*/
export class ComponentRecognizer {
names = new Map<string, RouteRecognizer>();
// map from name to recognizer
auxNames = new Map<string, RouteRecognizer>();
// map from starting path to recognizer
auxRoutes = new Map<string, RouteRecognizer>();
// TODO: optimize this into a trie
matchers: AbstractRecognizer[] = [];
defaultRoute: RouteRecognizer = null;
/**
* returns whether or not the config is terminal
*/
config(config: RouteDefinition): boolean {
var handler;
if (isPresent(config.name) && config.name[0].toUpperCase() != config.name[0]) {
var suggestedName = config.name[0].toUpperCase() + config.name.substring(1);
throw new BaseException(
`Route "${config.path}" with name "${config.name}" does not begin with an uppercase letter. Route names should be CamelCase like "${suggestedName}".`);
}
if (config instanceof AuxRoute) {
handler = new SyncRouteHandler(config.component, config.data);
let path = config.path.startsWith('/') ? config.path.substring(1) : config.path;
var recognizer = new RouteRecognizer(config.path, handler);
this.auxRoutes.set(path, recognizer);
if (isPresent(config.name)) {
this.auxNames.set(config.name, recognizer);
}
return recognizer.terminal;
}
var useAsDefault = false;
if (config instanceof Redirect) {
let redirector = new RedirectRecognizer(config.path, config.redirectTo);
this._assertNoHashCollision(redirector.hash, config.path);
this.matchers.push(redirector);
return true;
}
if (config instanceof Route) {
handler = new SyncRouteHandler(config.component, config.data);
useAsDefault = isPresent(config.useAsDefault) && config.useAsDefault;
} else if (config instanceof AsyncRoute) {
handler = new AsyncRouteHandler(config.loader, config.data);
useAsDefault = isPresent(config.useAsDefault) && config.useAsDefault;
}
var recognizer = new RouteRecognizer(config.path, handler);
this._assertNoHashCollision(recognizer.hash, config.path);
if (useAsDefault) {
if (isPresent(this.defaultRoute)) {
throw new BaseException(`Only one route can be default`);
}
this.defaultRoute = recognizer;
}
this.matchers.push(recognizer);
if (isPresent(config.name)) {
this.names.set(config.name, recognizer);
}
return recognizer.terminal;
}
private _assertNoHashCollision(hash: string, path) {
this.matchers.forEach((matcher) => {
if (hash == matcher.hash) {
throw new BaseException(
`Configuration '${path}' conflicts with existing route '${matcher.path}'`);
}
});
}
/**
* Given a URL, returns a list of `RouteMatch`es, which are partial recognitions for some route.
*/
recognize(urlParse: Url): Promise<RouteMatch>[] {
var solutions = [];
this.matchers.forEach((routeRecognizer: AbstractRecognizer) => {
var pathMatch = routeRecognizer.recognize(urlParse);
if (isPresent(pathMatch)) {
solutions.push(pathMatch);
}
});
return solutions;
}
recognizeAuxiliary(urlParse: Url): Promise<RouteMatch>[] {
var routeRecognizer: RouteRecognizer = this.auxRoutes.get(urlParse.path);
if (isPresent(routeRecognizer)) {
return [routeRecognizer.recognize(urlParse)];
}
return [PromiseWrapper.resolve(null)];
}
hasRoute(name: string): boolean { return this.names.has(name); }
componentLoaded(name: string): boolean {
return this.hasRoute(name) && isPresent(this.names.get(name).handler.componentType);
}
loadComponent(name: string): Promise<any> {
return this.names.get(name).handler.resolveComponentType();
}
generate(name: string, params: any): ComponentInstruction {
var pathRecognizer: RouteRecognizer = this.names.get(name);
if (isBlank(pathRecognizer)) {
return null;
}
return pathRecognizer.generate(params);
}
generateAuxiliary(name: string, params: any): ComponentInstruction {
var pathRecognizer: RouteRecognizer = this.auxNames.get(name);
if (isBlank(pathRecognizer)) {
return null;
}
return pathRecognizer.generate(params);
}
}

View File

@ -1,7 +1,10 @@
import {Map, MapWrapper, StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
import {unimplemented} from 'angular2/src/facade/exceptions';
import {isPresent, isBlank, normalizeBlank, Type, CONST_EXPR} from 'angular2/src/facade/lang';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {Promise} from 'angular2/src/facade/async';
import {PathRecognizer} from './path_recognizer';
import {Url} from './url_parser';
/**
* `RouteParams` is an immutable map of parameters for the given route
@ -74,7 +77,7 @@ export class RouteData {
get(key: string): any { return normalizeBlank(StringMapWrapper.get(this.data, key)); }
}
export var BLANK_ROUTE_DATA = new RouteData();
var BLANK_ROUTE_DATA = new RouteData();
/**
* `Instruction` is a tree of {@link ComponentInstruction}s with all the information needed
@ -103,184 +106,74 @@ export var BLANK_ROUTE_DATA = new RouteData();
* bootstrap(AppCmp, ROUTER_PROVIDERS);
* ```
*/
export abstract class Instruction {
public component: ComponentInstruction;
public child: Instruction;
public auxInstruction: {[key: string]: Instruction} = {};
get urlPath(): string { return this.component.urlPath; }
get urlParams(): string[] { return this.component.urlParams; }
get specificity(): number {
var total = 0;
if (isPresent(this.component)) {
total += this.component.specificity;
}
if (isPresent(this.child)) {
total += this.child.specificity;
}
return total;
}
abstract resolveComponent(): Promise<ComponentInstruction>;
/**
* converts the instruction into a URL string
*/
toRootUrl(): string { return this.toUrlPath() + this.toUrlQuery(); }
/** @internal */
_toNonRootUrl(): string {
return this._stringifyPathMatrixAuxPrefixed() +
(isPresent(this.child) ? this.child._toNonRootUrl() : '');
}
toUrlQuery(): string { return this.urlParams.length > 0 ? ('?' + this.urlParams.join('&')) : ''; }
export class Instruction {
constructor(public component: ComponentInstruction, public child: Instruction,
public auxInstruction: {[key: string]: Instruction}) {}
/**
* Returns a new instruction that shares the state of the existing instruction, but with
* the given child {@link Instruction} replacing the existing child.
*/
replaceChild(child: Instruction): Instruction {
return new ResolvedInstruction(this.component, child, this.auxInstruction);
return new Instruction(this.component, child, this.auxInstruction);
}
}
/**
* If the final URL for the instruction is ``
*/
toUrlPath(): string {
return this.urlPath + this._stringifyAux() +
(isPresent(this.child) ? this.child._toNonRootUrl() : '');
/**
* 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: Url[]) {}
}
export function stringifyInstruction(instruction: Instruction): string {
return stringifyInstructionPath(instruction) + stringifyInstructionQuery(instruction);
}
export function stringifyInstructionPath(instruction: Instruction): string {
return instruction.component.urlPath + stringifyAux(instruction) +
stringifyPrimaryPrefixed(instruction.child);
}
export function stringifyInstructionQuery(instruction: Instruction): string {
return instruction.component.urlParams.length > 0 ?
('?' + instruction.component.urlParams.join('&')) :
'';
}
function stringifyPrimaryPrefixed(instruction: Instruction): string {
var primary = stringifyPrimary(instruction);
if (primary.length > 0) {
primary = '/' + primary;
}
return primary;
}
// default instructions override these
toLinkUrl(): string {
return this.urlPath + this._stringifyAux() +
(isPresent(this.child) ? this.child._toLinkUrl() : '');
}
// this is the non-root version (called recursively)
/** @internal */
_toLinkUrl(): string {
return this._stringifyPathMatrixAuxPrefixed() +
(isPresent(this.child) ? this.child._toLinkUrl() : '');
}
/** @internal */
_stringifyPathMatrixAuxPrefixed(): string {
var primary = this._stringifyPathMatrixAux();
if (primary.length > 0) {
primary = '/' + primary;
}
return primary;
}
/** @internal */
_stringifyMatrixParams(): string {
return this.urlParams.length > 0 ? (';' + this.component.urlParams.join(';')) : '';
}
/** @internal */
_stringifyPathMatrixAux(): string {
if (isBlank(this.component)) {
return '';
}
return this.urlPath + this._stringifyMatrixParams() + this._stringifyAux();
}
/** @internal */
_stringifyAux(): string {
var routes = [];
StringMapWrapper.forEach(this.auxInstruction, (auxInstruction, _) => {
routes.push(auxInstruction._stringifyPathMatrixAux());
});
if (routes.length > 0) {
return '(' + routes.join('//') + ')';
}
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) +
stringifyPrimaryPrefixed(instruction.child);
}
/**
* a resolved instruction has an outlet instruction for itself, but maybe not for...
*/
export class ResolvedInstruction extends Instruction {
constructor(public component: ComponentInstruction, public child: Instruction,
public auxInstruction: {[key: string]: Instruction}) {
super();
}
resolveComponent(): Promise<ComponentInstruction> {
return PromiseWrapper.resolve(this.component);
}
}
/**
* Represents a resolved default route
*/
export class DefaultInstruction extends Instruction {
constructor(public component: ComponentInstruction, public child: DefaultInstruction) { super(); }
resolveComponent(): Promise<ComponentInstruction> {
return PromiseWrapper.resolve(this.component);
}
toLinkUrl(): string { return ''; }
/** @internal */
_toLinkUrl(): string { return ''; }
}
/**
* Represents a component that may need to do some redirection or lazy loading at a later time.
*/
export class UnresolvedInstruction extends Instruction {
constructor(private _resolver: () => Promise<Instruction>, private _urlPath: string = '',
private _urlParams: string[] = CONST_EXPR([])) {
super();
}
get urlPath(): string {
if (isPresent(this.component)) {
return this.component.urlPath;
}
if (isPresent(this._urlPath)) {
return this._urlPath;
}
return '';
}
get urlParams(): string[] {
if (isPresent(this.component)) {
return this.component.urlParams;
}
if (isPresent(this._urlParams)) {
return this._urlParams;
}
return [];
}
resolveComponent(): Promise<ComponentInstruction> {
if (isPresent(this.component)) {
return PromiseWrapper.resolve(this.component);
}
return this._resolver().then((resolution: Instruction) => {
this.child = resolution.child;
return this.component = resolution.component;
});
}
}
export class RedirectInstruction extends ResolvedInstruction {
constructor(component: ComponentInstruction, child: Instruction,
auxInstruction: {[key: string]: Instruction}) {
super(component, child, auxInstruction);
function stringifyAux(instruction: Instruction): string {
var routes = [];
StringMapWrapper.forEach(instruction.auxInstruction, (auxInstruction, _) => {
routes.push(stringifyPrimary(auxInstruction));
});
if (routes.length > 0) {
return '(' + routes.join('//') + ')';
}
return '';
}
@ -292,18 +185,67 @@ export class RedirectInstruction extends ResolvedInstruction {
* to route lifecycle hooks, like {@link CanActivate}.
*
* `ComponentInstruction`s are [https://en.wikipedia.org/wiki/Hash_consing](hash consed). You should
* never construct one yourself with "new." Instead, rely on {@link Router/RouteRecognizer} to
* never construct one yourself with "new." Instead, rely on {@link Router/PathRecognizer} to
* construct `ComponentInstruction`s.
*
* You should not modify this object. It should be treated as immutable.
*/
export class ComponentInstruction {
export abstract class ComponentInstruction {
reuse: boolean = false;
public routeData: RouteData;
public urlPath: string;
public urlParams: string[];
public params: {[key: string]: any};
constructor(public urlPath: string, public urlParams: string[], data: RouteData,
public componentType, public terminal: boolean, public specificity: number,
public params: {[key: string]: any} = null) {
this.routeData = isPresent(data) ? data : BLANK_ROUTE_DATA;
}
/**
* Returns the component type of the represented route, or `null` if this instruction
* hasn't been resolved.
*/
get componentType() { return unimplemented(); };
/**
* Returns a promise that will resolve to component type of the represented route.
* If this instruction references an {@link AsyncRoute}, the `loader` function of that route
* will run.
*/
abstract resolveComponentType(): Promise<Type>;
/**
* Returns the specificity of the route associated with this `Instruction`.
*/
get specificity() { return unimplemented(); };
/**
* Returns `true` if the component type of this instruction has no child {@link RouteConfig},
* or `false` if it does.
*/
get terminal() { return unimplemented(); };
/**
* Returns the route data of the given route that was specified in the {@link RouteDefinition},
* or an empty object if no route data was specified.
*/
get routeData(): RouteData { return unimplemented(); };
}
export class ComponentInstruction_ extends ComponentInstruction {
private _routeData: RouteData;
constructor(urlPath: string, urlParams: string[], private _recognizer: PathRecognizer,
params: {[key: string]: any} = null) {
super();
this.urlPath = urlPath;
this.urlParams = urlParams;
this.params = params;
if (isPresent(this._recognizer.handler.data)) {
this._routeData = new RouteData(this._recognizer.handler.data);
} else {
this._routeData = BLANK_ROUTE_DATA;
}
}
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; }
get routeData(): RouteData { return this._routeData; }
}

View File

@ -7,9 +7,12 @@ import {
isBlank
} from 'angular2/src/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
import {Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {RouteHandler} from './route_handler';
import {Url, RootUrl, serializeParams} from './url_parser';
import {ComponentInstruction, ComponentInstruction_} from './instruction';
class TouchMap {
map: {[key: string]: string} = {};
@ -30,7 +33,7 @@ class TouchMap {
}
getUnused(): {[key: string]: any} {
var unused: {[key: string]: any} = {};
var unused: {[key: string]: any} = StringMapWrapper.create();
var keys = StringMapWrapper.keys(this.keys);
keys.forEach(key => unused[key] = StringMapWrapper.get(this.map, key));
return unused;
@ -123,6 +126,7 @@ function parsePathString(route: string): {[key: string]: any} {
results.push(new StarSegment(match[1]));
} else if (segment == '...') {
if (i < limit) {
// TODO (matsko): setup a proper error here `
throw new BaseException(`Unexpected "..." before the end of the path for "${route}".`);
}
results.push(new ContinuationSegment());
@ -171,17 +175,23 @@ function assertPath(path: string) {
}
}
export class PathMatch {
constructor(public instruction: ComponentInstruction, public remaining: Url,
public remainingAux: Url[]) {}
}
/**
* Parses a URL string using a given matcher DSL, and generates URLs from param maps
*/
// represents something like '/foo/:bar'
export class PathRecognizer {
private _segments: Segment[];
specificity: number;
terminal: boolean = true;
hash: string;
private _cache: Map<string, ComponentInstruction> = new Map<string, ComponentInstruction>();
constructor(public path: string) {
// TODO: cache component instruction instances by params and by ParsedUrl instance
constructor(public path: string, public handler: RouteHandler) {
assertPath(path);
var parsed = parsePathString(path);
@ -193,7 +203,8 @@ export class PathRecognizer {
this.terminal = !(lastSegment instanceof ContinuationSegment);
}
recognize(beginningSegment: Url): {[key: string]: any} {
recognize(beginningSegment: Url): PathMatch {
var nextSegment = beginningSegment;
var currentSegment: Url;
var positionalParams = {};
@ -236,6 +247,7 @@ export class PathRecognizer {
var urlPath = captured.join('/');
var auxiliary;
var instruction: ComponentInstruction;
var urlParams;
var allParams;
if (isPresent(currentSegment)) {
@ -255,11 +267,12 @@ export class PathRecognizer {
auxiliary = [];
urlParams = [];
}
return {urlPath, urlParams, allParams, auxiliary, nextSegment};
instruction = this._getInstruction(urlPath, urlParams, this, allParams);
return new PathMatch(instruction, nextSegment, auxiliary);
}
generate(params: {[key: string]: any}): {[key: string]: any} {
generate(params: {[key: string]: any}): ComponentInstruction {
var paramTokens = new TouchMap(params);
var path = [];
@ -275,6 +288,18 @@ export class PathRecognizer {
var nonPositionalParams = paramTokens.getUnused();
var urlParams = serializeParams(nonPositionalParams);
return {urlPath, urlParams};
return this._getInstruction(urlPath, urlParams, this, params);
}
private _getInstruction(urlPath: string, urlParams: string[], _recognizer: PathRecognizer,
params: {[key: string]: any}): ComponentInstruction {
var hashKey = urlPath + '?' + urlParams.join('?');
if (this._cache.has(hashKey)) {
return this._cache.get(hashKey);
}
var instruction = new ComponentInstruction_(urlPath, urlParams, _recognizer, params);
this._cache.set(hashKey, instruction);
return instruction;
}
}

View File

@ -21,8 +21,6 @@ export class RouteConfig {
* - `name` is an optional `CamelCase` string representing the name of the route.
* - `data` is an optional property of any type representing arbitrary route metadata for the given
* route. It is injectable via {@link RouteData}.
* - `useAsDefault` is a boolean value. If `true`, the child route will be navigated to if no child
* route is specified during the navigation.
*
* ### Example
* ```
@ -40,20 +38,16 @@ export class Route implements RouteDefinition {
path: string;
component: Type;
name: string;
useAsDefault: boolean;
// added next three properties to work around https://github.com/Microsoft/TypeScript/issues/4107
aux: string = null;
loader: Function = null;
redirectTo: any[] = null;
constructor({path, component, name, data, useAsDefault}: {
path: string,
component: Type, name?: string, data?: {[key: string]: any}, useAsDefault?: boolean
}) {
redirectTo: string = null;
constructor({path, component, name,
data}: {path: string, component: Type, name?: string, data?: {[key: string]: any}}) {
this.path = path;
this.component = component;
this.name = name;
this.data = data;
this.useAsDefault = useAsDefault;
}
}
@ -86,8 +80,7 @@ export class AuxRoute implements RouteDefinition {
// added next three properties to work around https://github.com/Microsoft/TypeScript/issues/4107
aux: string = null;
loader: Function = null;
redirectTo: any[] = null;
useAsDefault: boolean = false;
redirectTo: string = null;
constructor({path, component, name}: {path: string, component: Type, name?: string}) {
this.path = path;
this.component = component;
@ -105,8 +98,6 @@ export class AuxRoute implements RouteDefinition {
* - `name` is an optional `CamelCase` string representing the name of the route.
* - `data` is an optional property of any type representing arbitrary route metadata for the given
* route. It is injectable via {@link RouteData}.
* - `useAsDefault` is a boolean value. If `true`, the child route will be navigated to if no child
* route is specified during the navigation.
*
* ### Example
* ```
@ -124,37 +115,31 @@ export class AsyncRoute implements RouteDefinition {
path: string;
loader: Function;
name: string;
useAsDefault: boolean;
aux: string = null;
constructor({path, loader, name, data, useAsDefault}: {
path: string,
loader: Function, name?: string, data?: {[key: string]: any}, useAsDefault?: boolean
}) {
constructor({path, loader, name, data}:
{path: string, loader: Function, name?: string, data?: {[key: string]: any}}) {
this.path = path;
this.loader = loader;
this.name = name;
this.data = data;
this.useAsDefault = useAsDefault;
}
}
/**
* `Redirect` is a type of {@link RouteDefinition} used to route a path to a canonical route.
* `Redirect` is a type of {@link RouteDefinition} used to route a path to an asynchronously loaded
* component.
*
* It has the following properties:
* - `path` is a string that uses the route matcher DSL.
* - `redirectTo` is an array representing the link DSL.
*
* Note that redirects **do not** affect how links are generated. For that, see the `useAsDefault`
* option.
* - `redirectTo` is a string representing the new URL to be matched against.
*
* ### Example
* ```
* import {RouteConfig} from 'angular2/router';
*
* @RouteConfig([
* {path: '/', redirectTo: ['/Home'] },
* {path: '/home', component: HomeCmp, name: 'Home'}
* {path: '/', redirectTo: '/home'},
* {path: '/home', component: HomeCmp}
* ])
* class MyApp {}
* ```
@ -162,14 +147,13 @@ export class AsyncRoute implements RouteDefinition {
@CONST()
export class Redirect implements RouteDefinition {
path: string;
redirectTo: any[];
redirectTo: string;
name: string = null;
// added next three properties to work around https://github.com/Microsoft/TypeScript/issues/4107
loader: Function = null;
data: any = null;
aux: string = null;
useAsDefault: boolean = false;
constructor({path, redirectTo}: {path: string, redirectTo: any[]}) {
constructor({path, redirectTo}: {path: string, redirectTo: string}) {
this.path = path;
this.redirectTo = redirectTo;
}

View File

@ -1,22 +1,9 @@
library angular2.src.router.route_config_normalizer;
import "route_config_decorator.dart";
import "route_registry.dart";
import "package:angular2/src/facade/exceptions.dart" show BaseException;
RouteDefinition normalizeRouteConfig(RouteDefinition config, RouteRegistry registry) {
if (config is AsyncRoute) {
configRegistryAndReturnType(componentType) {
registry.configFromComponent(componentType);
return componentType;
}
loader() {
return config.loader().then(configRegistryAndReturnType);
}
return new AsyncRoute(path: config.path, loader: loader, name: config.name, data: config.data, useAsDefault: config.useAsDefault);
}
RouteDefinition normalizeRouteConfig(RouteDefinition config) {
return config;
}

View File

@ -2,29 +2,14 @@ import {AsyncRoute, AuxRoute, Route, Redirect, RouteDefinition} from './route_co
import {ComponentDefinition} from './route_definition';
import {isType, Type} from 'angular2/src/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
import {RouteRegistry} from './route_registry';
/**
* Given a JS Object that represents a route config, returns a corresponding Route, AsyncRoute,
* AuxRoute or Redirect object.
*
* Also wraps an AsyncRoute's loader function to add the loaded component's route config to the
* `RouteRegistry`.
* Given a JS Object that represents... returns a corresponding Route, AsyncRoute, or Redirect
*/
export function normalizeRouteConfig(config: RouteDefinition,
registry: RouteRegistry): RouteDefinition {
if (config instanceof AsyncRoute) {
var wrappedLoader = wrapLoaderToReconfigureRegistry(config.loader, registry);
return new AsyncRoute({
path: config.path,
loader: wrappedLoader,
name: config.name,
data: config.data,
useAsDefault: config.useAsDefault
});
}
if (config instanceof Route || config instanceof Redirect || config instanceof AuxRoute) {
export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
if (config instanceof Route || config instanceof Redirect || config instanceof AsyncRoute ||
config instanceof AuxRoute) {
return <RouteDefinition>config;
}
@ -39,13 +24,7 @@ export function normalizeRouteConfig(config: RouteDefinition,
config.name = config.as;
}
if (config.loader) {
var wrappedLoader = wrapLoaderToReconfigureRegistry(config.loader, registry);
return new AsyncRoute({
path: config.path,
loader: wrappedLoader,
name: config.name,
useAsDefault: config.useAsDefault
});
return new AsyncRoute({path: config.path, loader: config.loader, name: config.name});
}
if (config.aux) {
return new AuxRoute({path: config.aux, component:<Type>config.component, name: config.name});
@ -57,17 +36,11 @@ export function normalizeRouteConfig(config: RouteDefinition,
return new Route({
path: config.path,
component:<Type>componentDefinitionObject.constructor,
name: config.name,
data: config.data,
useAsDefault: config.useAsDefault
name: config.name
});
} else if (componentDefinitionObject.type == 'loader') {
return new AsyncRoute({
path: config.path,
loader: componentDefinitionObject.loader,
name: config.name,
useAsDefault: config.useAsDefault
});
return new AsyncRoute(
{path: config.path, loader: componentDefinitionObject.loader, name: config.name});
} else {
throw new BaseException(
`Invalid component type "${componentDefinitionObject.type}". Valid types are "constructor" and "loader".`);
@ -77,8 +50,6 @@ export function normalizeRouteConfig(config: RouteDefinition,
path: string;
component: Type;
name?: string;
data?: {[key: string]: any};
useAsDefault?: boolean;
}>config);
}
@ -89,16 +60,6 @@ export function normalizeRouteConfig(config: RouteDefinition,
return config;
}
function wrapLoaderToReconfigureRegistry(loader: Function, registry: RouteRegistry): Function {
return () => {
return loader().then((componentType) => {
registry.configFromComponent(componentType);
return componentType;
});
};
}
export function assertComponentExists(component: Type, path: string): void {
if (!isType(component)) {
throw new BaseException(`Component for route "${path}" is not defined, or is not a class.`);

View File

@ -3,6 +3,5 @@ library angular2.src.router.route_definition;
abstract class RouteDefinition {
final String path;
final String name;
final bool useAsDefault;
const RouteDefinition({this.path, this.name, this.useAsDefault : false});
const RouteDefinition({this.path, this.name});
}

View File

@ -16,11 +16,10 @@ export interface RouteDefinition {
aux?: string;
component?: Type | ComponentDefinition;
loader?: Function;
redirectTo?: any[];
redirectTo?: string;
as?: string;
name?: string;
data?: any;
useAsDefault?: boolean;
}
export interface ComponentDefinition {

View File

@ -1,9 +1,8 @@
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {Type} from 'angular2/src/facade/lang';
import {RouteData} from './instruction';
export interface RouteHandler {
componentType: Type;
resolveComponentType(): Promise<any>;
data: RouteData;
data?: {[key: string]: any};
}

View File

@ -1,119 +1,184 @@
import {isPresent, isBlank} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import {PromiseWrapper, Promise} from 'angular2/src/facade/promise';
import {Map} from 'angular2/src/facade/collection';
import {
RegExp,
RegExpWrapper,
isBlank,
isPresent,
isType,
isStringMap,
Type
} from 'angular2/src/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
import {Map, MapWrapper, ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {RouteHandler} from './route_handler';
import {PathRecognizer, PathMatch} from './path_recognizer';
import {Route, AsyncRoute, AuxRoute, Redirect, RouteDefinition} from './route_config_impl';
import {AsyncRouteHandler} from './async_route_handler';
import {SyncRouteHandler} from './sync_route_handler';
import {Url} from './url_parser';
import {ComponentInstruction} from './instruction';
import {PathRecognizer} from './path_recognizer';
export abstract class RouteMatch {}
/**
* `RouteRecognizer` is responsible for recognizing routes for a single component.
* It is consumed by `RouteRegistry`, which knows how to recognize an entire hierarchy of
* components.
*/
export class RouteRecognizer {
names = new Map<string, PathRecognizer>();
export interface AbstractRecognizer {
hash: string;
path: string;
recognize(beginningSegment: Url): Promise<RouteMatch>;
generate(params: {[key: string]: any}): ComponentInstruction;
}
// map from name to recognizer
auxNames = new Map<string, PathRecognizer>();
// map from starting path to recognizer
auxRoutes = new Map<string, PathRecognizer>();
// TODO: optimize this into a trie
matchers: PathRecognizer[] = [];
// TODO: optimize this into a trie
redirects: Redirector[] = [];
config(config: RouteDefinition): boolean {
var handler;
if (isPresent(config.name) && config.name[0].toUpperCase() != config.name[0]) {
var suggestedName = config.name[0].toUpperCase() + config.name.substring(1);
throw new BaseException(
`Route "${config.path}" with name "${config.name}" does not begin with an uppercase letter. Route names should be CamelCase like "${suggestedName}".`);
}
if (config instanceof AuxRoute) {
handler = new SyncRouteHandler(config.component, config.data);
let path = config.path.startsWith('/') ? config.path.substring(1) : config.path;
var recognizer = new PathRecognizer(config.path, handler);
this.auxRoutes.set(path, recognizer);
if (isPresent(config.name)) {
this.auxNames.set(config.name, recognizer);
}
return recognizer.terminal;
}
if (config instanceof Redirect) {
this.redirects.push(new Redirector(config.path, config.redirectTo));
return true;
}
if (config instanceof Route) {
handler = new SyncRouteHandler(config.component, config.data);
} else if (config instanceof AsyncRoute) {
handler = new AsyncRouteHandler(config.loader, config.data);
}
var recognizer = new PathRecognizer(config.path, handler);
this.matchers.forEach((matcher) => {
if (recognizer.hash == matcher.hash) {
throw new BaseException(
`Configuration '${config.path}' conflicts with existing route '${matcher.path}'`);
}
});
this.matchers.push(recognizer);
if (isPresent(config.name)) {
this.names.set(config.name, recognizer);
}
return recognizer.terminal;
}
export class PathMatch extends RouteMatch {
constructor(public instruction: ComponentInstruction, public remaining: Url,
public remainingAux: Url[]) {
super();
/**
* Given a URL, returns a list of `RouteMatch`es, which are partial recognitions for some route.
*
*/
recognize(urlParse: Url): PathMatch[] {
var solutions = [];
urlParse = this._redirect(urlParse);
this.matchers.forEach((pathRecognizer: PathRecognizer) => {
var pathMatch = pathRecognizer.recognize(urlParse);
if (isPresent(pathMatch)) {
solutions.push(pathMatch);
}
});
return solutions;
}
/** @internal */
_redirect(urlParse: Url): Url {
for (var i = 0; i < this.redirects.length; i += 1) {
let redirector = this.redirects[i];
var redirectedUrl = redirector.redirect(urlParse);
if (isPresent(redirectedUrl)) {
return redirectedUrl;
}
}
return urlParse;
}
recognizeAuxiliary(urlParse: Url): PathMatch {
var pathRecognizer = this.auxRoutes.get(urlParse.path);
if (isBlank(pathRecognizer)) {
return null;
}
return pathRecognizer.recognize(urlParse);
}
hasRoute(name: string): boolean { return this.names.has(name); }
generate(name: string, params: any): ComponentInstruction {
var pathRecognizer: PathRecognizer = this.names.get(name);
if (isBlank(pathRecognizer)) {
return null;
}
return pathRecognizer.generate(params);
}
generateAuxiliary(name: string, params: any): ComponentInstruction {
var pathRecognizer: PathRecognizer = this.auxNames.get(name);
if (isBlank(pathRecognizer)) {
return null;
}
return pathRecognizer.generate(params);
}
}
export class Redirector {
segments: string[] = [];
toSegments: string[] = [];
export class RedirectMatch extends RouteMatch {
constructor(public redirectTo: any[], public specificity) { super(); }
}
export class RedirectRecognizer implements AbstractRecognizer {
private _pathRecognizer: PathRecognizer;
public hash: string;
constructor(public path: string, public redirectTo: any[]) {
this._pathRecognizer = new PathRecognizer(path);
this.hash = this._pathRecognizer.hash;
constructor(path: string, redirectTo: string) {
if (path.startsWith('/')) {
path = path.substring(1);
}
this.segments = path.split('/');
if (redirectTo.startsWith('/')) {
redirectTo = redirectTo.substring(1);
}
this.toSegments = redirectTo.split('/');
}
/**
* Returns `null` or a `ParsedUrl` representing the new path to match
*/
recognize(beginningSegment: Url): Promise<RouteMatch> {
var match = null;
if (isPresent(this._pathRecognizer.recognize(beginningSegment))) {
match = new RedirectMatch(this.redirectTo, this._pathRecognizer.specificity);
redirect(urlParse: Url): Url {
for (var i = 0; i < this.segments.length; i += 1) {
if (isBlank(urlParse)) {
return null;
}
let segment = this.segments[i];
if (segment != urlParse.path) {
return null;
}
urlParse = urlParse.child;
}
return PromiseWrapper.resolve(match);
}
generate(params: {[key: string]: any}): ComponentInstruction {
throw new BaseException(`Tried to generate a redirect.`);
}
}
// represents something like '/foo/:bar'
export class RouteRecognizer implements AbstractRecognizer {
specificity: number;
terminal: boolean = true;
hash: string;
private _cache: Map<string, ComponentInstruction> = new Map<string, ComponentInstruction>();
private _pathRecognizer: PathRecognizer;
// TODO: cache component instruction instances by params and by ParsedUrl instance
constructor(public path: string, public handler: RouteHandler) {
this._pathRecognizer = new PathRecognizer(path);
this.specificity = this._pathRecognizer.specificity;
this.hash = this._pathRecognizer.hash;
this.terminal = this._pathRecognizer.terminal;
}
recognize(beginningSegment: Url): Promise<RouteMatch> {
var res = this._pathRecognizer.recognize(beginningSegment);
if (isBlank(res)) {
return null;
}
return this.handler.resolveComponentType().then((_) => {
var componentInstruction =
this._getInstruction(res['urlPath'], res['urlParams'], res['allParams']);
return new PathMatch(componentInstruction, res['nextSegment'], res['auxiliary']);
});
}
generate(params: {[key: string]: any}): ComponentInstruction {
var generated = this._pathRecognizer.generate(params);
var urlPath = generated['urlPath'];
var urlParams = generated['urlParams'];
return this._getInstruction(urlPath, urlParams, params);
}
generateComponentPathValues(params: {[key: string]: any}): {[key: string]: any} {
return this._pathRecognizer.generate(params);
}
private _getInstruction(urlPath: string, urlParams: string[],
params: {[key: string]: any}): ComponentInstruction {
if (isBlank(this.handler.componentType)) {
throw new BaseException(`Tried to get instruction before the type was loaded.`);
}
var hashKey = urlPath + '?' + urlParams.join('?');
if (this._cache.has(hashKey)) {
return this._cache.get(hashKey);
}
var instruction =
new ComponentInstruction(urlPath, urlParams, this.handler.data, this.handler.componentType,
this.terminal, this.specificity, params);
this._cache.set(hashKey, instruction);
return instruction;
for (var i = this.toSegments.length - 1; i >= 0; i -= 1) {
let segment = this.toSegments[i];
urlParse = new Url(segment, urlParse);
}
return urlParse;
}
}

View File

@ -1,3 +1,6 @@
import {PathMatch} from './path_recognizer';
import {RouteRecognizer} from './route_recognizer';
import {Instruction, ComponentInstruction, PrimaryInstruction} from './instruction';
import {ListWrapper, Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {
@ -13,7 +16,6 @@ import {
getTypeNameForDebugging
} from 'angular2/src/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
import {reflector} from 'angular2/src/core/reflection/reflection';
import {
RouteConfig,
AsyncRoute,
@ -22,16 +24,7 @@ import {
Redirect,
RouteDefinition
} from './route_config_impl';
import {PathMatch, RedirectMatch, RouteMatch} from './route_recognizer';
import {ComponentRecognizer} from './component_recognizer';
import {
Instruction,
ResolvedInstruction,
RedirectInstruction,
UnresolvedInstruction,
DefaultInstruction
} from './instruction';
import {reflector} from 'angular2/src/core/reflection/reflection';
import {Injectable} from 'angular2/angular2';
import {normalizeRouteConfig, assertComponentExists} from './route_config_nomalizer';
import {parser, Url, pathSegmentsToUrl} from './url_parser';
@ -45,13 +38,13 @@ var _resolveToNull = PromiseWrapper.resolve(null);
*/
@Injectable()
export class RouteRegistry {
private _rules = new Map<any, ComponentRecognizer>();
private _rules = new Map<any, RouteRecognizer>();
/**
* Given a component and a configuration object, add the route to this registry
*/
config(parentComponent: any, config: RouteDefinition): void {
config = normalizeRouteConfig(config, this);
config = normalizeRouteConfig(config);
// this is here because Dart type guard reasons
if (config instanceof Route) {
@ -60,10 +53,10 @@ export class RouteRegistry {
assertComponentExists(config.component, config.path);
}
var recognizer: ComponentRecognizer = this._rules.get(parentComponent);
var recognizer: RouteRecognizer = this._rules.get(parentComponent);
if (isBlank(recognizer)) {
recognizer = new ComponentRecognizer();
recognizer = new RouteRecognizer();
this._rules.set(parentComponent, recognizer);
}
@ -109,162 +102,102 @@ export class RouteRegistry {
* Given a URL and a parent component, return the most specific instruction for navigating
* the application into the state specified by the url
*/
recognize(url: string, ancestorComponents: any[]): Promise<Instruction> {
recognize(url: string, parentComponent: any): Promise<Instruction> {
var parsedUrl = parser.parse(url);
return this._recognize(parsedUrl, ancestorComponents);
return this._recognize(parsedUrl, parentComponent);
}
private _recognize(parsedUrl: Url, parentComponent): Promise<Instruction> {
return this._recognizePrimaryRoute(parsedUrl, parentComponent)
.then((instruction: PrimaryInstruction) =>
this._completeAuxiliaryRouteMatches(instruction, parentComponent));
}
/**
* Recognizes all parent-child routes, but creates unresolved auxiliary routes
*/
private _recognize(parsedUrl: Url, ancestorComponents: any[],
_aux = false): Promise<Instruction> {
var parentComponent = ancestorComponents[ancestorComponents.length - 1];
private _recognizePrimaryRoute(parsedUrl: Url, parentComponent): Promise<PrimaryInstruction> {
var componentRecognizer = this._rules.get(parentComponent);
if (isBlank(componentRecognizer)) {
return _resolveToNull;
}
// Matches some beginning part of the given URL
var possibleMatches: Promise<RouteMatch>[] =
_aux ? componentRecognizer.recognizeAuxiliary(parsedUrl) :
componentRecognizer.recognize(parsedUrl);
var possibleMatches = componentRecognizer.recognize(parsedUrl);
var matchPromises: Promise<Instruction>[] = possibleMatches.map(
(candidate: Promise<RouteMatch>) => candidate.then((candidate: RouteMatch) => {
if (candidate instanceof PathMatch) {
if (candidate.instruction.terminal) {
var unresolvedAux =
this._auxRoutesToUnresolved(candidate.remainingAux, parentComponent);
return new ResolvedInstruction(candidate.instruction, null, unresolvedAux);
}
var newAncestorComponents =
ancestorComponents.concat([candidate.instruction.componentType]);
return this._recognize(candidate.remaining, newAncestorComponents)
.then((childInstruction) => {
if (isBlank(childInstruction)) {
return null;
}
// redirect instructions are already absolute
if (childInstruction instanceof RedirectInstruction) {
return childInstruction;
}
var unresolvedAux =
this._auxRoutesToUnresolved(candidate.remainingAux, parentComponent);
return new ResolvedInstruction(candidate.instruction, childInstruction,
unresolvedAux);
});
}
if (candidate instanceof RedirectMatch) {
var instruction = this.generate(candidate.redirectTo, ancestorComponents);
return new RedirectInstruction(instruction.component, instruction.child,
instruction.auxInstruction);
}
}));
if ((isBlank(parsedUrl) || parsedUrl.path == '') && possibleMatches.length == 0) {
return PromiseWrapper.resolve(this.generateDefault(parentComponent));
}
var matchPromises =
possibleMatches.map(candidate => this._completePrimaryRouteMatch(candidate));
return PromiseWrapper.all(matchPromises).then(mostSpecific);
}
private _auxRoutesToUnresolved(auxRoutes: Url[], parentComponent): {[key: string]: Instruction} {
var unresolvedAuxInstructions: {[key: string]: Instruction} = {};
private _completePrimaryRouteMatch(partialMatch: PathMatch): Promise<PrimaryInstruction> {
var instruction = partialMatch.instruction;
return instruction.resolveComponentType().then((componentType) => {
this.configFromComponent(componentType);
auxRoutes.forEach((auxUrl: Url) => {
unresolvedAuxInstructions[auxUrl.path] = new UnresolvedInstruction(
() => { return this._recognize(auxUrl, [parentComponent], true); });
if (instruction.terminal) {
return new PrimaryInstruction(instruction, null, partialMatch.remainingAux);
}
return this._recognizePrimaryRoute(partialMatch.remaining, componentType)
.then((childInstruction) => {
if (isBlank(childInstruction)) {
return null;
} else {
return new PrimaryInstruction(instruction, childInstruction,
partialMatch.remainingAux);
}
});
});
return unresolvedAuxInstructions;
}
private _completeAuxiliaryRouteMatches(instruction: PrimaryInstruction,
parentComponent: any): Promise<Instruction> {
if (isBlank(instruction)) {
return _resolveToNull;
}
var componentRecognizer = this._rules.get(parentComponent);
var auxInstructions: {[key: string]: Instruction} = {};
var promises = instruction.auxUrls.map((auxSegment: Url) => {
var match = componentRecognizer.recognizeAuxiliary(auxSegment);
if (isBlank(match)) {
return _resolveToNull;
}
return this._completePrimaryRouteMatch(match).then((auxInstruction: PrimaryInstruction) => {
if (isPresent(auxInstruction)) {
return this._completeAuxiliaryRouteMatches(auxInstruction, parentComponent)
.then((finishedAuxRoute: Instruction) => {
auxInstructions[auxSegment.path] = finishedAuxRoute;
});
}
});
});
return PromiseWrapper.all(promises).then((_) => {
if (isBlank(instruction.child)) {
return new Instruction(instruction.component, null, auxInstructions);
}
return this._completeAuxiliaryRouteMatches(instruction.child,
instruction.component.componentType)
.then((completeChild) => {
return new Instruction(instruction.component, completeChild, auxInstructions);
});
});
}
/**
* Given a normalized list with component names and params like: `['user', {id: 3 }]`
* generates a url with a leading slash relative to the provided `parentComponent`.
*
* If the optional param `_aux` is `true`, then we generate starting at an auxiliary
* route boundary.
*/
generate(linkParams: any[], ancestorComponents: any[], _aux = false): Instruction {
let parentComponent = ancestorComponents[ancestorComponents.length - 1];
let grandparentComponent =
ancestorComponents.length > 1 ? ancestorComponents[ancestorComponents.length - 2] : null;
let normalizedLinkParams = splitAndFlattenLinkParams(linkParams);
var first = ListWrapper.first(normalizedLinkParams);
var rest = ListWrapper.slice(normalizedLinkParams, 1);
// The first segment should be either '.' (generate from parent) or '' (generate from root).
// When we normalize above, we strip all the slashes, './' becomes '.' and '/' becomes ''.
if (first == '') {
var firstComponent = ancestorComponents[0];
ListWrapper.clear(ancestorComponents);
ancestorComponents.push(firstComponent);
} else if (first == '..') {
// we already captured the first instance of "..", so we need to pop off an ancestor
ancestorComponents.pop();
while (ListWrapper.first(rest) == '..') {
rest = ListWrapper.slice(rest, 1);
ancestorComponents.pop();
if (ancestorComponents.length <= 0) {
throw new BaseException(
`Link "${ListWrapper.toJSON(linkParams)}" has too many "../" segments.`);
}
}
} else if (first != '.') {
// For a link with no leading `./`, `/`, or `../`, we look for a sibling and child.
// If both exist, we throw. Otherwise, we prefer whichever exists.
var childRouteExists = this.hasRoute(first, parentComponent);
var parentRouteExists =
isPresent(grandparentComponent) && this.hasRoute(first, grandparentComponent);
if (parentRouteExists && childRouteExists) {
let msg =
`Link "${ListWrapper.toJSON(linkParams)}" is ambiguous, use "./" or "../" to disambiguate.`;
throw new BaseException(msg);
}
if (parentRouteExists) {
ancestorComponents.pop();
}
rest = linkParams;
}
if (rest[rest.length - 1] == '') {
rest.pop();
}
if (rest.length < 1) {
let msg = `Link "${ListWrapper.toJSON(linkParams)}" must include a route name.`;
throw new BaseException(msg);
}
return this._generate(rest, ancestorComponents, _aux);
}
/*
* Internal helper that does not make any assertions about the beginning of the link DSL
*/
private _generate(linkParams: any[], ancestorComponents: any[], _aux = false): Instruction {
let parentComponent = ancestorComponents[ancestorComponents.length - 1];
if (linkParams.length == 0) {
return this.generateDefault(parentComponent);
}
generate(linkParams: any[], parentComponent: any, _aux = false): Instruction {
let linkIndex = 0;
let routeName = linkParams[linkIndex];
// TODO: this is kind of odd but it makes existing assertions pass
if (isBlank(parentComponent)) {
throw new BaseException(`Could not find route named "${routeName}".`);
}
if (!isString(routeName)) {
throw new BaseException(`Unexpected segment "${routeName}" in link DSL. Expected a string.`);
} else if (routeName == '' || routeName == '.' || routeName == '..') {
@ -283,10 +216,7 @@ export class RouteRegistry {
let auxInstructions: {[key: string]: Instruction} = {};
var nextSegment;
while (linkIndex + 1 < linkParams.length && isArray(nextSegment = linkParams[linkIndex + 1])) {
let auxInstruction = this._generate(nextSegment, [parentComponent], true);
// TODO: this will not work for aux routes with parameters or multiple segments
auxInstructions[auxInstruction.component.urlPath] = auxInstruction;
auxInstructions[nextSegment[0]] = this.generate(nextSegment, parentComponent, true);
linkIndex += 1;
}
@ -296,105 +226,74 @@ export class RouteRegistry {
`Component "${getTypeNameForDebugging(parentComponent)}" has no route config.`);
}
var routeRecognizer =
(_aux ? componentRecognizer.auxNames : componentRecognizer.names).get(routeName);
var componentInstruction = _aux ? componentRecognizer.generateAuxiliary(routeName, params) :
componentRecognizer.generate(routeName, params);
if (!isPresent(routeRecognizer)) {
if (isBlank(componentInstruction)) {
throw new BaseException(
`Component "${getTypeNameForDebugging(parentComponent)}" has no route named "${routeName}".`);
}
if (!isPresent(routeRecognizer.handler.componentType)) {
var compInstruction = routeRecognizer.generateComponentPathValues(params);
return new UnresolvedInstruction(() => {
return routeRecognizer.handler.resolveComponentType().then(
(_) => { return this._generate(linkParams, ancestorComponents, _aux); });
}, compInstruction['urlPath'], compInstruction['urlParams']);
var childInstruction = null;
if (linkIndex + 1 < linkParams.length) {
var remaining = linkParams.slice(linkIndex + 1);
childInstruction = this.generate(remaining, componentInstruction.componentType);
} else if (!componentInstruction.terminal) {
throw new BaseException(
`Link "${ListWrapper.toJSON(linkParams)}" does not resolve to a terminal or async instruction.`);
}
var componentInstruction = _aux ? componentRecognizer.generateAuxiliary(routeName, params) :
componentRecognizer.generate(routeName, params);
var childInstruction: Instruction = null;
var remaining = linkParams.slice(linkIndex + 1);
// the component is sync
if (isPresent(componentInstruction.componentType)) {
if (linkIndex + 1 < linkParams.length) {
let childAncestorComponents =
ancestorComponents.concat([componentInstruction.componentType]);
childInstruction = this._generate(remaining, childAncestorComponents);
} else if (!componentInstruction.terminal) {
// ... look for defaults
childInstruction = this.generateDefault(componentInstruction.componentType);
if (isBlank(childInstruction)) {
throw new BaseException(
`Link "${ListWrapper.toJSON(linkParams)}" does not resolve to a terminal instruction.`);
}
}
}
return new ResolvedInstruction(componentInstruction, childInstruction, auxInstructions);
return new Instruction(componentInstruction, childInstruction, auxInstructions);
}
public hasRoute(name: string, parentComponent: any): boolean {
var componentRecognizer: ComponentRecognizer = this._rules.get(parentComponent);
var componentRecognizer: RouteRecognizer = this._rules.get(parentComponent);
if (isBlank(componentRecognizer)) {
return false;
}
return componentRecognizer.hasRoute(name);
}
public generateDefault(componentCursor: Type): Instruction {
// if the child includes a redirect like : "/" -> "/something",
// we want to honor that redirection when creating the link
private _generateRedirects(componentCursor: Type): Instruction {
if (isBlank(componentCursor)) {
return null;
}
var componentRecognizer = this._rules.get(componentCursor);
if (isBlank(componentRecognizer) || isBlank(componentRecognizer.defaultRoute)) {
if (isBlank(componentRecognizer)) {
return null;
}
for (let i = 0; i < componentRecognizer.redirects.length; i += 1) {
let redirect = componentRecognizer.redirects[i];
var defaultChild = null;
if (isPresent(componentRecognizer.defaultRoute.handler.componentType)) {
var componentInstruction = componentRecognizer.defaultRoute.generate({});
if (!componentRecognizer.defaultRoute.terminal) {
defaultChild = this.generateDefault(componentRecognizer.defaultRoute.handler.componentType);
// we only handle redirecting from an empty segment
if (redirect.segments.length == 1 && redirect.segments[0] == '') {
var toSegments = pathSegmentsToUrl(redirect.toSegments);
var matches = componentRecognizer.recognize(toSegments);
var primaryInstruction =
ListWrapper.maximum(matches, (match: PathMatch) => match.instruction.specificity);
if (isPresent(primaryInstruction)) {
var child = this._generateRedirects(primaryInstruction.instruction.componentType);
return new Instruction(primaryInstruction.instruction, child, {});
}
return null;
}
return new DefaultInstruction(componentInstruction, defaultChild);
}
return new UnresolvedInstruction(() => {
return componentRecognizer.defaultRoute.handler.resolveComponentType().then(
() => this.generateDefault(componentCursor));
});
return null;
}
}
/*
* Given: ['/a/b', {c: 2}]
* Returns: ['', 'a', 'b', {c: 2}]
*/
function splitAndFlattenLinkParams(linkParams: any[]): any[] {
return linkParams.reduce((accumulation: any[], item) => {
if (isString(item)) {
let strItem: string = item;
return accumulation.concat(strItem.split('/'));
}
accumulation.push(item);
return accumulation;
}, []);
}
/*
* Given a list of instructions, returns the most specific instruction
*/
function mostSpecific(instructions: Instruction[]): Instruction {
return ListWrapper.maximum(instructions, (instruction: Instruction) => instruction.specificity);
function mostSpecific(instructions: PrimaryInstruction[]): PrimaryInstruction {
return ListWrapper.maximum(
instructions, (instruction: PrimaryInstruction) => instruction.component.specificity);
}
function assertTerminalComponent(component, path) {

View File

@ -6,6 +6,9 @@ import {RouteRegistry} from './route_registry';
import {
ComponentInstruction,
Instruction,
stringifyInstruction,
stringifyInstructionPath,
stringifyInstructionQuery
} from './instruction';
import {RouterOutlet} from './router_outlet';
import {Location} from './location';
@ -209,7 +212,7 @@ export class Router {
if (result) {
return this.commit(instruction, _skipLocationChange)
.then((_) => {
this._emitNavigationFinish(instruction.toRootUrl());
this._emitNavigationFinish(stringifyInstruction(instruction));
return true;
});
}
@ -217,20 +220,25 @@ export class Router {
});
}
// TODO(btford): it'd be nice to remove this method as part of cleaning up the traversal logic
// Since refactoring `Router.generate` to return an instruction rather than a string, it's not
// guaranteed that the `componentType`s for the terminal async routes have been loaded by the time
// we begin navigation. The method below simply traverses instructions and resolves any components
// for which `componentType` is not present
/** @internal */
_settleInstruction(instruction: Instruction): Promise<any> {
return instruction.resolveComponent().then((_) => {
var unsettledInstructions: Array<Promise<any>> = [];
if (isPresent(instruction.child)) {
unsettledInstructions.push(this._settleInstruction(instruction.child));
}
StringMapWrapper.forEach(instruction.auxInstruction, (instruction, _) => {
unsettledInstructions.push(this._settleInstruction(instruction));
});
return PromiseWrapper.all(unsettledInstructions);
var unsettledInstructions: Array<Promise<any>> = [];
if (isBlank(instruction.component.componentType)) {
unsettledInstructions.push(instruction.component.resolveComponentType().then(
(type: Type) => { this.registry.configFromComponent(type); }));
}
if (isPresent(instruction.child)) {
unsettledInstructions.push(this._settleInstruction(instruction.child));
}
StringMapWrapper.forEach(instruction.auxInstruction, (instruction, _) => {
unsettledInstructions.push(this._settleInstruction(instruction));
});
return PromiseWrapper.all(unsettledInstructions);
}
private _emitNavigationFinish(url): void { ObservableWrapper.callEmit(this._subject, url); }
@ -370,22 +378,7 @@ export class Router {
* Given a URL, returns an instruction representing the component graph
*/
recognize(url: string): Promise<Instruction> {
var ancestorComponents = this._getAncestorComponents();
return this.registry.recognize(url, ancestorComponents);
}
/**
* get all the host components for this and
*/
private _getAncestorComponents(): any[] {
var ancestorComponents = [];
var ancestorRouter = this;
do {
ancestorComponents.unshift(ancestorRouter.hostComponent);
ancestorRouter = ancestorRouter.parent;
} while (isPresent(ancestorRouter));
return ancestorComponents;
return this.registry.recognize(url, this.hostComponent);
}
@ -406,27 +399,67 @@ export class Router {
* app's base href.
*/
generate(linkParams: any[]): Instruction {
var ancestorComponents = this._getAncestorComponents();
var startingNumberOfAncestors = ancestorComponents.length;
let normalizedLinkParams = splitAndFlattenLinkParams(linkParams);
var nextInstruction = this.registry.generate(linkParams, ancestorComponents);
if (isBlank(nextInstruction)) {
return null;
}
var first = ListWrapper.first(normalizedLinkParams);
var rest = ListWrapper.slice(normalizedLinkParams, 1);
var parentInstructionsToClone = startingNumberOfAncestors - ancestorComponents.length;
var router = this;
var router = this.parent;
for (var i = 0; i < parentInstructionsToClone; i++) {
if (isBlank(router)) {
break;
// The first segment should be either '.' (generate from parent) or '' (generate from root).
// When we normalize above, we strip all the slashes, './' becomes '.' and '/' becomes ''.
if (first == '') {
while (isPresent(router.parent)) {
router = router.parent;
}
} else if (first == '..') {
router = router.parent;
while (ListWrapper.first(rest) == '..') {
rest = ListWrapper.slice(rest, 1);
router = router.parent;
if (isBlank(router)) {
throw new BaseException(
`Link "${ListWrapper.toJSON(linkParams)}" has too many "../" segments.`);
}
}
} else if (first != '.') {
// For a link with no leading `./`, `/`, or `../`, we look for a sibling and child.
// If both exist, we throw. Otherwise, we prefer whichever exists.
var childRouteExists = this.registry.hasRoute(first, this.hostComponent);
var parentRouteExists =
isPresent(this.parent) && this.registry.hasRoute(first, this.parent.hostComponent);
if (parentRouteExists && childRouteExists) {
let msg =
`Link "${ListWrapper.toJSON(linkParams)}" is ambiguous, use "./" or "../" to disambiguate.`;
throw new BaseException(msg);
}
if (parentRouteExists) {
router = this.parent;
}
rest = linkParams;
}
while (isPresent(router) && isPresent(router._currentInstruction)) {
nextInstruction = router._currentInstruction.replaceChild(nextInstruction);
router = router.parent;
if (rest[rest.length - 1] == '') {
rest.pop();
}
if (rest.length < 1) {
let msg = `Link "${ListWrapper.toJSON(linkParams)}" must include a route name.`;
throw new BaseException(msg);
}
var nextInstruction = this.registry.generate(rest, router.hostComponent);
var url = [];
var parent = router.parent;
while (isPresent(parent)) {
url.unshift(parent._currentInstruction);
parent = parent.parent;
}
while (url.length > 0) {
nextInstruction = url.pop().replaceChild(nextInstruction);
}
return nextInstruction;
@ -449,8 +482,8 @@ export class RootRouter extends Router {
}
commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> {
var emitPath = instruction.toUrlPath();
var emitQuery = instruction.toUrlQuery();
var emitPath = stringifyInstructionPath(instruction);
var emitQuery = stringifyInstructionQuery(instruction);
if (emitPath.length > 0) {
emitPath = '/' + emitPath;
}
@ -488,6 +521,20 @@ class ChildRouter extends Router {
}
}
/*
* Given: ['/a/b', {c: 2}]
* Returns: ['', 'a', 'b', {c: 2}]
*/
function splitAndFlattenLinkParams(linkParams: any[]): any[] {
return linkParams.reduce((accumulation: any[], item) => {
if (isString(item)) {
let strItem: string = item;
return accumulation.concat(strItem.split('/'));
}
accumulation.push(item);
return accumulation;
}, []);
}
function canActivateOne(nextInstruction: Instruction,
prevInstruction: Instruction): Promise<boolean> {

View File

@ -3,7 +3,7 @@ import {isString} from 'angular2/src/facade/lang';
import {Router} from './router';
import {Location} from './location';
import {Instruction} from './instruction';
import {Instruction, stringifyInstruction} from './instruction';
/**
* The RouterLink directive lets you link to specific parts of your app.
@ -61,7 +61,7 @@ export class RouterLink {
this._routeParams = changes;
this._navigationInstruction = this._router.generate(this._routeParams);
var navigationHref = this._navigationInstruction.toLinkUrl();
var navigationHref = stringifyInstruction(this._navigationInstruction);
this.visibleHref = this._location.prepareExternalUrl(navigationHref);
}

View File

@ -1,19 +1,13 @@
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {isPresent, Type} from 'angular2/src/facade/lang';
import {RouteHandler} from './route_handler';
import {RouteData, BLANK_ROUTE_DATA} from './instruction';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {Type} from 'angular2/src/facade/lang';
export class SyncRouteHandler implements RouteHandler {
public data: RouteData;
/** @internal */
_resolvedComponent: Promise<any> = null;
constructor(public componentType: Type, data?: {[key: string]: any}) {
constructor(public componentType: Type, public data?: {[key: string]: any}) {
this._resolvedComponent = PromiseWrapper.resolve(componentType);
this.data = isPresent(data) ? new RouteData(data) : BLANK_ROUTE_DATA;
}
resolveComponentType(): Promise<any> { return this._resolvedComponent; }

View File

@ -1,216 +0,0 @@
import {
AsyncTestCompleter,
describe,
it,
iit,
ddescribe,
expect,
inject,
beforeEach,
SpyObject
} from 'angular2/testing_internal';
import {Map, StringMapWrapper} from 'angular2/src/facade/collection';
import {RouteMatch, PathMatch, RedirectMatch} from 'angular2/src/router/route_recognizer';
import {ComponentRecognizer} from 'angular2/src/router/component_recognizer';
import {Route, Redirect} from 'angular2/src/router/route_config_decorator';
import {parser} from 'angular2/src/router/url_parser';
import {Promise, PromiseWrapper} from 'angular2/src/facade/promise';
export function main() {
describe('ComponentRecognizer', () => {
var recognizer: ComponentRecognizer;
beforeEach(() => { recognizer = new ComponentRecognizer(); });
it('should recognize a static segment', inject([AsyncTestCompleter], (async) => {
recognizer.config(new Route({path: '/test', component: DummyCmpA}));
recognize(recognizer, '/test')
.then((solutions: RouteMatch[]) => {
expect(solutions.length).toBe(1);
expect(getComponentType(solutions[0])).toEqual(DummyCmpA);
async.done();
});
}));
it('should recognize a single slash', inject([AsyncTestCompleter], (async) => {
recognizer.config(new Route({path: '/', component: DummyCmpA}));
recognize(recognizer, '/')
.then((solutions: RouteMatch[]) => {
expect(solutions.length).toBe(1);
expect(getComponentType(solutions[0])).toEqual(DummyCmpA);
async.done();
});
}));
it('should recognize a dynamic segment', inject([AsyncTestCompleter], (async) => {
recognizer.config(new Route({path: '/user/:name', component: DummyCmpA}));
recognize(recognizer, '/user/brian')
.then((solutions: RouteMatch[]) => {
expect(solutions.length).toBe(1);
expect(getComponentType(solutions[0])).toEqual(DummyCmpA);
expect(getParams(solutions[0])).toEqual({'name': 'brian'});
async.done();
});
}));
it('should recognize a star segment', inject([AsyncTestCompleter], (async) => {
recognizer.config(new Route({path: '/first/*rest', component: DummyCmpA}));
recognize(recognizer, '/first/second/third')
.then((solutions: RouteMatch[]) => {
expect(solutions.length).toBe(1);
expect(getComponentType(solutions[0])).toEqual(DummyCmpA);
expect(getParams(solutions[0])).toEqual({'rest': 'second/third'});
async.done();
});
}));
it('should throw when given two routes that start with the same static segment', () => {
recognizer.config(new Route({path: '/hello', component: DummyCmpA}));
expect(() => recognizer.config(new Route({path: '/hello', component: DummyCmpB})))
.toThrowError('Configuration \'/hello\' conflicts with existing route \'/hello\'');
});
it('should throw when given two routes that have dynamic segments in the same order', () => {
recognizer.config(new Route({path: '/hello/:person/how/:doyoudou', component: DummyCmpA}));
expect(() => recognizer.config(
new Route({path: '/hello/:friend/how/:areyou', component: DummyCmpA})))
.toThrowError(
'Configuration \'/hello/:friend/how/:areyou\' conflicts with existing route \'/hello/:person/how/:doyoudou\'');
expect(() => recognizer.config(
new Redirect({path: '/hello/:pal/how/:goesit', redirectTo: ['/Foo']})))
.toThrowError(
'Configuration \'/hello/:pal/how/:goesit\' conflicts with existing route \'/hello/:person/how/:doyoudou\'');
});
it('should recognize redirects', inject([AsyncTestCompleter], (async) => {
recognizer.config(new Route({path: '/b', component: DummyCmpA}));
recognizer.config(new Redirect({path: '/a', redirectTo: ['B']}));
recognize(recognizer, '/a')
.then((solutions: RouteMatch[]) => {
expect(solutions.length).toBe(1);
var solution = solutions[0];
expect(solution).toBeAnInstanceOf(RedirectMatch);
if (solution instanceof RedirectMatch) {
expect(solution.redirectTo).toEqual(['B']);
}
async.done();
});
}));
it('should generate URLs with params', () => {
recognizer.config(new Route({path: '/app/user/:name', component: DummyCmpA, name: 'User'}));
var instruction = recognizer.generate('User', {'name': 'misko'});
expect(instruction.urlPath).toEqual('app/user/misko');
});
it('should generate URLs with numeric params', () => {
recognizer.config(new Route({path: '/app/page/:number', component: DummyCmpA, name: 'Page'}));
expect(recognizer.generate('Page', {'number': 42}).urlPath).toEqual('app/page/42');
});
it('should throw in the absence of required params URLs', () => {
recognizer.config(new Route({path: 'app/user/:name', component: DummyCmpA, name: 'User'}));
expect(() => recognizer.generate('User', {}))
.toThrowError('Route generator for \'name\' was not included in parameters passed.');
});
it('should throw if the route alias is not TitleCase', () => {
expect(() => recognizer.config(
new Route({path: 'app/user/:name', component: DummyCmpA, name: 'user'})))
.toThrowError(
`Route "app/user/:name" with name "user" does not begin with an uppercase letter. Route names should be CamelCase like "User".`);
});
describe('params', () => {
it('should recognize parameters within the URL path',
inject([AsyncTestCompleter], (async) => {
recognizer.config(
new Route({path: 'profile/:name', component: DummyCmpA, name: 'User'}));
recognize(recognizer, '/profile/matsko?comments=all')
.then((solutions: RouteMatch[]) => {
expect(solutions.length).toBe(1);
expect(getParams(solutions[0])).toEqual({'name': 'matsko', 'comments': 'all'});
async.done();
});
}));
it('should generate and populate the given static-based route with querystring params',
() => {
recognizer.config(
new Route({path: 'forum/featured', component: DummyCmpA, name: 'ForumPage'}));
var params = {'start': 10, 'end': 100};
var result = recognizer.generate('ForumPage', params);
expect(result.urlPath).toEqual('forum/featured');
expect(result.urlParams).toEqual(['start=10', 'end=100']);
});
it('should prefer positional params over query params',
inject([AsyncTestCompleter], (async) => {
recognizer.config(
new Route({path: 'profile/:name', component: DummyCmpA, name: 'User'}));
recognize(recognizer, '/profile/yegor?name=igor')
.then((solutions: RouteMatch[]) => {
expect(solutions.length).toBe(1);
expect(getParams(solutions[0])).toEqual({'name': 'yegor'});
async.done();
});
}));
it('should ignore matrix params for the top-level component',
inject([AsyncTestCompleter], (async) => {
recognizer.config(
new Route({path: '/home/:subject', component: DummyCmpA, name: 'User'}));
recognize(recognizer, '/home;sort=asc/zero;one=1?two=2')
.then((solutions: RouteMatch[]) => {
expect(solutions.length).toBe(1);
expect(getParams(solutions[0])).toEqual({'subject': 'zero', 'two': '2'});
async.done();
});
}));
});
});
}
function recognize(recognizer: ComponentRecognizer, url: string): Promise<RouteMatch[]> {
var parsedUrl = parser.parse(url);
return PromiseWrapper.all(recognizer.recognize(parsedUrl));
}
function getComponentType(routeMatch: RouteMatch): any {
if (routeMatch instanceof PathMatch) {
return routeMatch.instruction.componentType;
}
return null;
}
function getParams(routeMatch: RouteMatch): any {
if (routeMatch instanceof PathMatch) {
return routeMatch.instruction.params;
}
return null;
}
class DummyCmpA {}
class DummyCmpB {}

View File

@ -1,9 +0,0 @@
# Router integration tests
These tests only mock out `Location`, and otherwise use all the real parts of routing to ensure that
various routing scenarios work as expected.
The Component Router in Angular 2 exposes only a handful of different options, but because they can
be combined and nested in so many ways, it's difficult to rigorously test all the cases.
The address this problem, we introduce `describeRouter`, `describeWith`, and `describeWithout`.

View File

@ -1,28 +0,0 @@
import {
describeRouter,
ddescribeRouter,
describeWith,
describeWithout,
describeWithAndWithout,
itShouldRoute
} from './util';
import {registerSpecs} from './impl/async_route_spec_impl';
export function main() {
registerSpecs();
ddescribeRouter('async routes', () => {
describeWithout('children', () => {
describeWith('route data', itShouldRoute);
describeWithAndWithout('params', itShouldRoute);
});
describeWith('sync children',
() => { describeWithAndWithout('default routes', itShouldRoute); });
describeWith('async children', () => {
describeWithAndWithout('params', () => { describeWithout('default routes', itShouldRoute); });
});
});
}

View File

@ -1,98 +0,0 @@
import {
RootTestComponent,
AsyncTestCompleter,
TestComponentBuilder,
beforeEach,
ddescribe,
xdescribe,
describe,
el,
expect,
iit,
inject,
beforeEachProviders,
it,
xit
} from 'angular2/testing_internal';
import {provide, Component, Injector, Inject} from 'angular2/core';
import {Router, ROUTER_DIRECTIVES, RouteParams, RouteData, Location} from 'angular2/router';
import {RouteConfig, Route, AuxRoute, Redirect} from 'angular2/src/router/route_config_decorator';
import {TEST_ROUTER_PROVIDERS, RootCmp, compile} from './util';
var cmpInstanceCount;
var childCmpInstanceCount;
export function main() {
describe('auxiliary routes', () => {
var tcb: TestComponentBuilder;
var rootTC: RootTestComponent;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
rtr = router;
childCmpInstanceCount = 0;
cmpInstanceCount = 0;
}));
it('should recognize and navigate from the URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
.then((rtc) => {rootTC = rtc})
.then((_) => rtr.config([
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Aux'})
]))
.then((_) => rtr.navigateByUrl('/hello(modal)'))
.then((_) => {
rootTC.detectChanges();
expect(rootTC.debugElement.nativeElement).toHaveText('main {hello} | aux {modal}');
async.done();
});
}));
it('should navigate via the link DSL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
.then((rtc) => {rootTC = rtc})
.then((_) => rtr.config([
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Modal'})
]))
.then((_) => rtr.navigate(['/Hello', ['Modal']]))
.then((_) => {
rootTC.detectChanges();
expect(rootTC.debugElement.nativeElement).toHaveText('main {hello} | aux {modal}');
async.done();
});
}));
});
}
@Component({selector: 'hello-cmp', template: `{{greeting}}`})
class HelloCmp {
greeting: string;
constructor() { this.greeting = 'hello'; }
}
@Component({selector: 'modal-cmp', template: `modal`})
class ModalCmp {
}
@Component({
selector: 'aux-cmp',
template: 'main {<router-outlet></router-outlet>} | ' +
'aux {<router-outlet name="modal"></router-outlet>}',
directives: [ROUTER_DIRECTIVES],
})
@RouteConfig([
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Aux'})
])
class AuxCmp {
}

View File

@ -1,655 +0,0 @@
import {
AsyncTestCompleter,
beforeEach,
beforeEachProviders,
expect,
iit,
flushMicrotasks,
inject,
it,
TestComponentBuilder,
RootTestComponent,
xit,
} from 'angular2/testing_internal';
import {specs, compile, TEST_ROUTER_PROVIDERS, clickOnElement, getHref} from '../util';
import {Router, AsyncRoute, Route, Location} from 'angular2/router';
import {
HelloCmp,
helloCmpLoader,
UserCmp,
userCmpLoader,
TeamCmp,
asyncTeamLoader,
ParentCmp,
parentCmpLoader,
asyncParentCmpLoader,
asyncDefaultParentCmpLoader,
ParentWithDefaultCmp,
parentWithDefaultCmpLoader,
asyncRouteDataCmp
} from './fixture_components';
function getLinkElement(rtc: RootTestComponent) {
return rtc.debugElement.componentViewChildren[0].nativeElement;
}
function asyncRoutesWithoutChildrenWithRouteData() {
var fixture;
var tcb;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
rtr = router;
}));
it('should inject route data into the component', inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config([
new AsyncRoute(
{path: '/route-data', loader: asyncRouteDataCmp, data: {isAdmin: true}})
]))
.then((_) => rtr.navigateByUrl('/route-data'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('true');
async.done();
});
}));
it('should inject empty object if the route has no data property',
inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new AsyncRoute({path: '/route-data-default', loader: asyncRouteDataCmp})]))
.then((_) => rtr.navigateByUrl('/route-data-default'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('');
async.done();
});
}));
}
function asyncRoutesWithoutChildrenWithoutParams() {
var fixture;
var tcb;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
rtr = router;
}));
it('should navigate by URL', inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new AsyncRoute({path: '/test', loader: helloCmpLoader, name: 'Hello'})]))
.then((_) => rtr.navigateByUrl('/test'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('hello');
async.done();
});
}));
it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new AsyncRoute({path: '/test', loader: helloCmpLoader, name: 'Hello'})]))
.then((_) => rtr.navigate(['/Hello']))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('hello');
async.done();
});
}));
it('should generate a link URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `<a [router-link]="['Hello']">go to hello</a> | <router-outlet></router-outlet>`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new AsyncRoute({path: '/test', loader: helloCmpLoader, name: 'Hello'})]))
.then((_) => {
fixture.detectChanges();
expect(getHref(getLinkElement(fixture))).toEqual('/test');
async.done();
});
}));
it('should navigate from a link click',
inject([AsyncTestCompleter, Location], (async, location) => {
compile(tcb, `<a [router-link]="['Hello']">go to hello</a> | <router-outlet></router-outlet>`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new AsyncRoute({path: '/test', loader: helloCmpLoader, name: 'Hello'})]))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('go to hello | ');
rtr.subscribe((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('go to hello | hello');
expect(location.urlChanges).toEqual(['/test']);
async.done();
});
clickOnElement(getLinkElement(fixture));
});
}));
}
function asyncRoutesWithoutChildrenWithParams() {
var fixture;
var tcb;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
rtr = router;
}));
it('should navigate by URL', inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new AsyncRoute({path: '/user/:name', loader: userCmpLoader, name: 'User'})]))
.then((_) => rtr.navigateByUrl('/user/igor'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('hello igor');
async.done();
});
}));
it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new Route({path: '/user/:name', component: UserCmp, name: 'User'})]))
.then((_) => rtr.navigate(['/User', {name: 'brian'}]))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('hello brian');
async.done();
});
}));
it('should generate a link URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `<a [router-link]="['User', {name: 'naomi'}]">greet naomi</a> | <router-outlet></router-outlet>`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new AsyncRoute({path: '/user/:name', loader: userCmpLoader, name: 'User'})]))
.then((_) => {
fixture.detectChanges();
expect(getHref(getLinkElement(fixture))).toEqual('/user/naomi');
async.done();
});
}));
it('should navigate from a link click',
inject([AsyncTestCompleter, Location], (async, location) => {
compile(tcb, `<a [router-link]="['User', {name: 'naomi'}]">greet naomi</a> | <router-outlet></router-outlet>`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new AsyncRoute({path: '/user/:name', loader: userCmpLoader, name: 'User'})]))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('greet naomi | ');
rtr.subscribe((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('greet naomi | hello naomi');
expect(location.urlChanges).toEqual(['/user/naomi']);
async.done();
});
clickOnElement(getLinkElement(fixture));
});
}));
it('should navigate between components with different parameters',
inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new AsyncRoute({path: '/user/:name', loader: userCmpLoader, name: 'User'})]))
.then((_) => rtr.navigateByUrl('/user/brian'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('hello brian');
})
.then((_) => rtr.navigateByUrl('/user/igor'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('hello igor');
async.done();
});
}));
}
function asyncRoutesWithSyncChildrenWithoutDefaultRoutes() {
var fixture;
var tcb;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
rtr = router;
}));
it('should navigate by URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `outer { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new AsyncRoute({path: '/a/...', loader: parentCmpLoader, name: 'Parent'})]))
.then((_) => rtr.navigateByUrl('/a/b'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('outer { inner { hello } }');
async.done();
});
}));
it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `outer { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new AsyncRoute({path: '/a/...', loader: parentCmpLoader, name: 'Parent'})]))
.then((_) => rtr.navigate(['/Parent', 'Child']))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('outer { inner { hello } }');
async.done();
});
}));
it('should generate a link URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `<a [router-link]="['Parent']">nav to child</a> | outer { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new AsyncRoute({path: '/a/...', loader: parentCmpLoader, name: 'Parent'})]))
.then((_) => {
fixture.detectChanges();
expect(getHref(getLinkElement(fixture))).toEqual('/a');
async.done();
});
}));
it('should navigate from a link click',
inject([AsyncTestCompleter, Location], (async, location) => {
compile(tcb, `<a [router-link]="['Parent', 'Child']">nav to child</a> | outer { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new AsyncRoute({path: '/a/...', loader: parentCmpLoader, name: 'Parent'})]))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('nav to child | outer { }');
rtr.subscribe((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement)
.toHaveText('nav to child | outer { inner { hello } }');
expect(location.urlChanges).toEqual(['/a/b']);
async.done();
});
clickOnElement(getLinkElement(fixture));
});
}));
}
function asyncRoutesWithSyncChildrenWithDefaultRoutes() {
var fixture;
var tcb;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
rtr = router;
}));
it('should navigate by URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `outer { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config([
new AsyncRoute({path: '/a/...', loader: parentWithDefaultCmpLoader, name: 'Parent'})
]))
.then((_) => rtr.navigateByUrl('/a'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('outer { inner { hello } }');
async.done();
});
}));
it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `outer { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config([
new AsyncRoute({path: '/a/...', loader: parentWithDefaultCmpLoader, name: 'Parent'})
]))
.then((_) => rtr.navigate(['/Parent']))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('outer { inner { hello } }');
async.done();
});
}));
it('should generate a link URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `<a [router-link]="['/Parent']">link to inner</a> | outer { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config([
new AsyncRoute({path: '/a/...', loader: parentWithDefaultCmpLoader, name: 'Parent'})
]))
.then((_) => {
fixture.detectChanges();
expect(getHref(getLinkElement(fixture))).toEqual('/a');
async.done();
});
}));
it('should navigate from a link click',
inject([AsyncTestCompleter, Location], (async, location) => {
compile(tcb, `<a [router-link]="['/Parent']">link to inner</a> | outer { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config([
new AsyncRoute({path: '/a/...', loader: parentWithDefaultCmpLoader, name: 'Parent'})
]))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('link to inner | outer { }');
rtr.subscribe((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement)
.toHaveText('link to inner | outer { inner { hello } }');
expect(location.urlChanges).toEqual(['/a/b']);
async.done();
});
clickOnElement(getLinkElement(fixture));
});
}));
}
function asyncRoutesWithAsyncChildrenWithoutParamsWithoutDefaultRoutes() {
var rootTC;
var tcb;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
rtr = router;
}));
it('should navigate by URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `outer { <router-outlet></router-outlet> }`)
.then((rtc) => {rootTC = rtc})
.then((_) => rtr.config([
new AsyncRoute({path: '/a/...', loader: asyncParentCmpLoader, name: 'Parent'})
]))
.then((_) => rtr.navigateByUrl('/a/b'))
.then((_) => {
rootTC.detectChanges();
expect(rootTC.debugElement.nativeElement).toHaveText('outer { inner { hello } }');
async.done();
});
}));
it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `outer { <router-outlet></router-outlet> }`)
.then((rtc) => {rootTC = rtc})
.then((_) => rtr.config([
new AsyncRoute({path: '/a/...', loader: asyncParentCmpLoader, name: 'Parent'})
]))
.then((_) => rtr.navigate(['/Parent', 'Child']))
.then((_) => {
rootTC.detectChanges();
expect(rootTC.debugElement.nativeElement).toHaveText('outer { inner { hello } }');
async.done();
});
}));
it('should generate a link URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `<a [router-link]="['Parent', 'Child']">nav to child</a> | outer { <router-outlet></router-outlet> }`)
.then((rtc) => {rootTC = rtc})
.then((_) => rtr.config([
new AsyncRoute({path: '/a/...', loader: asyncParentCmpLoader, name: 'Parent'})
]))
.then((_) => {
rootTC.detectChanges();
expect(getHref(getLinkElement(rootTC))).toEqual('/a');
async.done();
});
}));
it('should navigate from a link click',
inject([AsyncTestCompleter, Location], (async, location) => {
compile(tcb, `<a [router-link]="['Parent', 'Child']">nav to child</a> | outer { <router-outlet></router-outlet> }`)
.then((rtc) => {rootTC = rtc})
.then((_) => rtr.config([
new AsyncRoute({path: '/a/...', loader: asyncParentCmpLoader, name: 'Parent'})
]))
.then((_) => {
rootTC.detectChanges();
expect(rootTC.debugElement.nativeElement).toHaveText('nav to child | outer { }');
rtr.subscribe((_) => {
rootTC.detectChanges();
expect(rootTC.debugElement.nativeElement)
.toHaveText('nav to child | outer { inner { hello } }');
expect(location.urlChanges).toEqual(['/a/b']);
async.done();
});
clickOnElement(getLinkElement(rootTC));
});
}));
}
function asyncRoutesWithAsyncChildrenWithoutParamsWithDefaultRoutes() {
var rootTC;
var tcb;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
rtr = router;
}));
it('should navigate by URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `outer { <router-outlet></router-outlet> }`)
.then((rtc) => {rootTC = rtc})
.then((_) => rtr.config([
new AsyncRoute(
{path: '/a/...', loader: asyncDefaultParentCmpLoader, name: 'Parent'})
]))
.then((_) => rtr.navigateByUrl('/a'))
.then((_) => {
rootTC.detectChanges();
expect(rootTC.debugElement.nativeElement).toHaveText('outer { inner { hello } }');
async.done();
});
}));
it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `outer { <router-outlet></router-outlet> }`)
.then((rtc) => {rootTC = rtc})
.then((_) => rtr.config([
new AsyncRoute(
{path: '/a/...', loader: asyncDefaultParentCmpLoader, name: 'Parent'})
]))
.then((_) => rtr.navigate(['/Parent']))
.then((_) => {
rootTC.detectChanges();
expect(rootTC.debugElement.nativeElement).toHaveText('outer { inner { hello } }');
async.done();
});
}));
it('should generate a link URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `<a [router-link]="['Parent']">nav to child</a> | outer { <router-outlet></router-outlet> }`)
.then((rtc) => {rootTC = rtc})
.then((_) => rtr.config([
new AsyncRoute(
{path: '/a/...', loader: asyncDefaultParentCmpLoader, name: 'Parent'})
]))
.then((_) => {
rootTC.detectChanges();
expect(getHref(getLinkElement(rootTC))).toEqual('/a');
async.done();
});
}));
it('should navigate from a link click',
inject([AsyncTestCompleter, Location], (async, location) => {
compile(tcb, `<a [router-link]="['Parent']">nav to child</a> | outer { <router-outlet></router-outlet> }`)
.then((rtc) => {rootTC = rtc})
.then((_) => rtr.config([
new AsyncRoute(
{path: '/a/...', loader: asyncDefaultParentCmpLoader, name: 'Parent'})
]))
.then((_) => {
rootTC.detectChanges();
expect(rootTC.debugElement.nativeElement).toHaveText('nav to child | outer { }');
rtr.subscribe((_) => {
rootTC.detectChanges();
expect(rootTC.debugElement.nativeElement)
.toHaveText('nav to child | outer { inner { hello } }');
expect(location.urlChanges).toEqual(['/a/b']);
async.done();
});
clickOnElement(getLinkElement(rootTC));
});
}));
}
function asyncRoutesWithAsyncChildrenWithParamsWithoutDefaultRoutes() {
var fixture;
var tcb;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
rtr = router;
}));
it('should navigate by URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `{ <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config([
new AsyncRoute({path: '/team/:id/...', loader: asyncTeamLoader, name: 'Team'})
]))
.then((_) => rtr.navigateByUrl('/team/angular/user/matias'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement)
.toHaveText('{ team angular | user { hello matias } }');
async.done();
});
}));
it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `{ <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config([
new AsyncRoute({path: '/team/:id/...', loader: asyncTeamLoader, name: 'Team'})
]))
.then((_) => rtr.navigate(['/Team', {id: 'angular'}, 'User', {name: 'matias'}]))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement)
.toHaveText('{ team angular | user { hello matias } }');
async.done();
});
}));
it('should generate a link URL', inject([AsyncTestCompleter], (async) => {
compile(
tcb,
`<a [router-link]="['/Team', {id: 'angular'}, 'User', {name: 'matias'}]">nav to matias</a> { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config([
new AsyncRoute({path: '/team/:id/...', loader: asyncTeamLoader, name: 'Team'})
]))
.then((_) => {
fixture.detectChanges();
expect(getHref(getLinkElement(fixture))).toEqual('/team/angular');
async.done();
});
}));
it('should navigate from a link click',
inject([AsyncTestCompleter, Location], (async, location) => {
compile(
tcb,
`<a [router-link]="['/Team', {id: 'angular'}, 'User', {name: 'matias'}]">nav to matias</a> { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config([
new AsyncRoute({path: '/team/:id/...', loader: asyncTeamLoader, name: 'Team'})
]))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('nav to matias { }');
rtr.subscribe((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement)
.toHaveText('nav to matias { team angular | user { hello matias } }');
expect(location.urlChanges).toEqual(['/team/angular/user/matias']);
async.done();
});
clickOnElement(getLinkElement(fixture));
});
}));
}
export function registerSpecs() {
specs['asyncRoutesWithoutChildrenWithRouteData'] = asyncRoutesWithoutChildrenWithRouteData;
specs['asyncRoutesWithoutChildrenWithoutParams'] = asyncRoutesWithoutChildrenWithoutParams;
specs['asyncRoutesWithoutChildrenWithParams'] = asyncRoutesWithoutChildrenWithParams;
specs['asyncRoutesWithSyncChildrenWithoutDefaultRoutes'] =
asyncRoutesWithSyncChildrenWithoutDefaultRoutes;
specs['asyncRoutesWithSyncChildrenWithDefaultRoutes'] =
asyncRoutesWithSyncChildrenWithDefaultRoutes;
specs['asyncRoutesWithAsyncChildrenWithoutParamsWithoutDefaultRoutes'] =
asyncRoutesWithAsyncChildrenWithoutParamsWithoutDefaultRoutes;
specs['asyncRoutesWithAsyncChildrenWithoutParamsWithDefaultRoutes'] =
asyncRoutesWithAsyncChildrenWithoutParamsWithDefaultRoutes;
specs['asyncRoutesWithAsyncChildrenWithParamsWithoutDefaultRoutes'] =
asyncRoutesWithAsyncChildrenWithParamsWithoutDefaultRoutes;
}

View File

@ -1,131 +0,0 @@
import {Component} from 'angular2/angular2';
import {
AsyncRoute,
Route,
Redirect,
RouteConfig,
RouteParams,
RouteData,
ROUTER_DIRECTIVES
} from 'angular2/router';
import {PromiseWrapper} from 'angular2/src/facade/async';
@Component({selector: 'hello-cmp', template: `{{greeting}}`})
export class HelloCmp {
greeting: string;
constructor() { this.greeting = 'hello'; }
}
export function helloCmpLoader() {
return PromiseWrapper.resolve(HelloCmp);
}
@Component({selector: 'user-cmp', template: `hello {{user}}`})
export class UserCmp {
user: string;
constructor(params: RouteParams) { this.user = params.get('name'); }
}
export function userCmpLoader() {
return PromiseWrapper.resolve(UserCmp);
}
@Component({
selector: 'parent-cmp',
template: `inner { <router-outlet></router-outlet> }`,
directives: [ROUTER_DIRECTIVES],
})
@RouteConfig([new Route({path: '/b', component: HelloCmp, name: 'Child'})])
export class ParentCmp {
}
export function parentCmpLoader() {
return PromiseWrapper.resolve(ParentCmp);
}
@Component({
selector: 'parent-cmp',
template: `inner { <router-outlet></router-outlet> }`,
directives: [ROUTER_DIRECTIVES],
})
@RouteConfig([new AsyncRoute({path: '/b', loader: helloCmpLoader, name: 'Child'})])
export class AsyncParentCmp {
}
export function asyncParentCmpLoader() {
return PromiseWrapper.resolve(AsyncParentCmp);
}
@Component({
selector: 'parent-cmp',
template: `inner { <router-outlet></router-outlet> }`,
directives: [ROUTER_DIRECTIVES],
})
@RouteConfig(
[new AsyncRoute({path: '/b', loader: helloCmpLoader, name: 'Child', useAsDefault: true})])
export class AsyncDefaultParentCmp {
}
export function asyncDefaultParentCmpLoader() {
return PromiseWrapper.resolve(AsyncDefaultParentCmp);
}
@Component({
selector: 'parent-cmp',
template: `inner { <router-outlet></router-outlet> }`,
directives: [ROUTER_DIRECTIVES],
})
@RouteConfig([new Route({path: '/b', component: HelloCmp, name: 'Child', useAsDefault: true})])
export class ParentWithDefaultCmp {
}
export function parentWithDefaultCmpLoader() {
return PromiseWrapper.resolve(ParentWithDefaultCmp);
}
@Component({
selector: 'team-cmp',
template: `team {{id}} | user { <router-outlet></router-outlet> }`,
directives: [ROUTER_DIRECTIVES],
})
@RouteConfig([new Route({path: '/user/:name', component: UserCmp, name: 'User'})])
export class TeamCmp {
id: string;
constructor(params: RouteParams) { this.id = params.get('id'); }
}
@Component({
selector: 'team-cmp',
template: `team {{id}} | user { <router-outlet></router-outlet> }`,
directives: [ROUTER_DIRECTIVES],
})
@RouteConfig([new AsyncRoute({path: '/user/:name', loader: userCmpLoader, name: 'User'})])
export class AsyncTeamCmp {
id: string;
constructor(params: RouteParams) { this.id = params.get('id'); }
}
export function asyncTeamLoader() {
return PromiseWrapper.resolve(AsyncTeamCmp);
}
@Component({selector: 'data-cmp', template: `{{myData}}`})
export class RouteDataCmp {
myData: boolean;
constructor(data: RouteData) { this.myData = data.get('isAdmin'); }
}
export function asyncRouteDataCmp() {
return PromiseWrapper.resolve(RouteDataCmp);
}
@Component({selector: 'redirect-to-parent-cmp', template: 'redirect-to-parent'})
@RouteConfig([new Redirect({path: '/child-redirect', redirectTo: ['../HelloSib']})])
export class RedirectToParentCmp {
}

View File

@ -1,431 +0,0 @@
import {
AsyncTestCompleter,
beforeEach,
beforeEachProviders,
expect,
iit,
flushMicrotasks,
inject,
it,
TestComponentBuilder,
RootTestComponent,
xit,
} from 'angular2/testing_internal';
import {specs, compile, TEST_ROUTER_PROVIDERS, clickOnElement, getHref} from '../util';
import {Router, Route, Location} from 'angular2/router';
import {HelloCmp, UserCmp, TeamCmp, ParentCmp, ParentWithDefaultCmp} from './fixture_components';
function getLinkElement(rtc: RootTestComponent) {
return rtc.debugElement.componentViewChildren[0].nativeElement;
}
function syncRoutesWithoutChildrenWithoutParams() {
var fixture;
var tcb;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
rtr = router;
}));
it('should navigate by URL', inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
.then((_) =>
rtr.config([new Route({path: '/test', component: HelloCmp, name: 'Hello'})]))
.then((_) => rtr.navigateByUrl('/test'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('hello');
async.done();
});
}));
it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
.then((_) =>
rtr.config([new Route({path: '/test', component: HelloCmp, name: 'Hello'})]))
.then((_) => rtr.navigate(['/Hello']))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('hello');
async.done();
});
}));
it('should generate a link URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `<a [router-link]="['Hello']">go to hello</a> | <router-outlet></router-outlet>`)
.then((rtc) => {fixture = rtc})
.then((_) =>
rtr.config([new Route({path: '/test', component: HelloCmp, name: 'Hello'})]))
.then((_) => {
fixture.detectChanges();
expect(getHref(getLinkElement(fixture))).toEqual('/test');
async.done();
});
}));
it('should navigate from a link click',
inject([AsyncTestCompleter, Location], (async, location) => {
compile(tcb, `<a [router-link]="['Hello']">go to hello</a> | <router-outlet></router-outlet>`)
.then((rtc) => {fixture = rtc})
.then((_) =>
rtr.config([new Route({path: '/test', component: HelloCmp, name: 'Hello'})]))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('go to hello | ');
rtr.subscribe((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('go to hello | hello');
expect(location.urlChanges).toEqual(['/test']);
async.done();
});
clickOnElement(getLinkElement(fixture));
});
}));
}
function syncRoutesWithoutChildrenWithParams() {
var fixture;
var tcb;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
rtr = router;
}));
it('should navigate by URL', inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new Route({path: '/user/:name', component: UserCmp, name: 'User'})]))
.then((_) => rtr.navigateByUrl('/user/igor'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('hello igor');
async.done();
});
}));
it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new Route({path: '/user/:name', component: UserCmp, name: 'User'})]))
.then((_) => rtr.navigate(['/User', {name: 'brian'}]))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('hello brian');
async.done();
});
}));
it('should generate a link URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `<a [router-link]="['User', {name: 'naomi'}]">greet naomi</a> | <router-outlet></router-outlet>`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new Route({path: '/user/:name', component: UserCmp, name: 'User'})]))
.then((_) => {
fixture.detectChanges();
expect(getHref(getLinkElement(fixture))).toEqual('/user/naomi');
async.done();
});
}));
it('should navigate from a link click',
inject([AsyncTestCompleter, Location], (async, location) => {
compile(tcb, `<a [router-link]="['User', {name: 'naomi'}]">greet naomi</a> | <router-outlet></router-outlet>`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new Route({path: '/user/:name', component: UserCmp, name: 'User'})]))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('greet naomi | ');
rtr.subscribe((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('greet naomi | hello naomi');
expect(location.urlChanges).toEqual(['/user/naomi']);
async.done();
});
clickOnElement(getLinkElement(fixture));
});
}));
it('should navigate between components with different parameters',
inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new Route({path: '/user/:name', component: UserCmp, name: 'User'})]))
.then((_) => rtr.navigateByUrl('/user/brian'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('hello brian');
})
.then((_) => rtr.navigateByUrl('/user/igor'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('hello igor');
async.done();
});
}));
}
function syncRoutesWithSyncChildrenWithoutDefaultRoutesWithoutParams() {
var fixture;
var tcb;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
rtr = router;
}));
it('should navigate by URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `outer { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new Route({path: '/a/...', component: ParentCmp, name: 'Parent'})]))
.then((_) => rtr.navigateByUrl('/a/b'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('outer { inner { hello } }');
async.done();
});
}));
it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `outer { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new Route({path: '/a/...', component: ParentCmp, name: 'Parent'})]))
.then((_) => rtr.navigate(['/Parent', 'Child']))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('outer { inner { hello } }');
async.done();
});
}));
it('should generate a link URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `<a [router-link]="['Parent', 'Child']">nav to child</a> | outer { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new Route({path: '/a/...', component: ParentCmp, name: 'Parent'})]))
.then((_) => {
fixture.detectChanges();
expect(getHref(getLinkElement(fixture))).toEqual('/a/b');
async.done();
});
}));
it('should navigate from a link click',
inject([AsyncTestCompleter, Location], (async, location) => {
compile(tcb, `<a [router-link]="['Parent', 'Child']">nav to child</a> | outer { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new Route({path: '/a/...', component: ParentCmp, name: 'Parent'})]))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('nav to child | outer { }');
rtr.subscribe((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement)
.toHaveText('nav to child | outer { inner { hello } }');
expect(location.urlChanges).toEqual(['/a/b']);
async.done();
});
clickOnElement(getLinkElement(fixture));
});
}));
}
function syncRoutesWithSyncChildrenWithoutDefaultRoutesWithParams() {
var fixture;
var tcb;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
rtr = router;
}));
it('should navigate by URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `{ <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new Route({path: '/team/:id/...', component: TeamCmp, name: 'Team'})]))
.then((_) => rtr.navigateByUrl('/team/angular/user/matias'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement)
.toHaveText('{ team angular | user { hello matias } }');
async.done();
});
}));
it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `{ <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new Route({path: '/team/:id/...', component: TeamCmp, name: 'Team'})]))
.then((_) => rtr.navigate(['/Team', {id: 'angular'}, 'User', {name: 'matias'}]))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement)
.toHaveText('{ team angular | user { hello matias } }');
async.done();
});
}));
it('should generate a link URL', inject([AsyncTestCompleter], (async) => {
compile(
tcb,
`<a [router-link]="['/Team', {id: 'angular'}, 'User', {name: 'matias'}]">nav to matias</a> { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new Route({path: '/team/:id/...', component: TeamCmp, name: 'Team'})]))
.then((_) => {
fixture.detectChanges();
expect(getHref(getLinkElement(fixture))).toEqual('/team/angular/user/matias');
async.done();
});
}));
it('should navigate from a link click',
inject([AsyncTestCompleter, Location], (async, location) => {
compile(
tcb,
`<a [router-link]="['/Team', {id: 'angular'}, 'User', {name: 'matias'}]">nav to matias</a> { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then((_) => rtr.config(
[new Route({path: '/team/:id/...', component: TeamCmp, name: 'Team'})]))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('nav to matias { }');
rtr.subscribe((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement)
.toHaveText('nav to matias { team angular | user { hello matias } }');
expect(location.urlChanges).toEqual(['/team/angular/user/matias']);
async.done();
});
clickOnElement(getLinkElement(fixture));
});
}));
}
function syncRoutesWithSyncChildrenWithDefaultRoutesWithoutParams() {
var fixture;
var tcb;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
rtr = router;
}));
it('should navigate by URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `outer { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then(
(_) => rtr.config(
[new Route({path: '/a/...', component: ParentWithDefaultCmp, name: 'Parent'})]))
.then((_) => rtr.navigateByUrl('/a'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('outer { inner { hello } }');
async.done();
});
}));
it('should navigate by link DSL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `outer { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then(
(_) => rtr.config(
[new Route({path: '/a/...', component: ParentWithDefaultCmp, name: 'Parent'})]))
.then((_) => rtr.navigate(['/Parent']))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('outer { inner { hello } }');
async.done();
});
}));
it('should generate a link URL', inject([AsyncTestCompleter], (async) => {
compile(tcb, `<a [router-link]="['/Parent']">link to inner</a> | outer { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then(
(_) => rtr.config(
[new Route({path: '/a/...', component: ParentWithDefaultCmp, name: 'Parent'})]))
.then((_) => {
fixture.detectChanges();
expect(getHref(getLinkElement(fixture))).toEqual('/a');
async.done();
});
}));
it('should navigate from a link click',
inject([AsyncTestCompleter, Location], (async, location) => {
compile(tcb, `<a [router-link]="['/Parent']">link to inner</a> | outer { <router-outlet></router-outlet> }`)
.then((rtc) => {fixture = rtc})
.then(
(_) => rtr.config(
[new Route({path: '/a/...', component: ParentWithDefaultCmp, name: 'Parent'})]))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('link to inner | outer { }');
rtr.subscribe((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement)
.toHaveText('link to inner | outer { inner { hello } }');
expect(location.urlChanges).toEqual(['/a/b']);
async.done();
});
clickOnElement(getLinkElement(fixture));
});
}));
}
export function registerSpecs() {
specs['syncRoutesWithoutChildrenWithoutParams'] = syncRoutesWithoutChildrenWithoutParams;
specs['syncRoutesWithoutChildrenWithParams'] = syncRoutesWithoutChildrenWithParams;
specs['syncRoutesWithSyncChildrenWithoutDefaultRoutesWithoutParams'] =
syncRoutesWithSyncChildrenWithoutDefaultRoutesWithoutParams;
specs['syncRoutesWithSyncChildrenWithoutDefaultRoutesWithParams'] =
syncRoutesWithSyncChildrenWithoutDefaultRoutesWithParams;
specs['syncRoutesWithSyncChildrenWithDefaultRoutesWithoutParams'] =
syncRoutesWithSyncChildrenWithDefaultRoutesWithoutParams;
}

View File

@ -10,7 +10,7 @@ import {
expect,
iit,
inject,
beforeEachProviders,
beforeEachBindings,
it,
xit
} from 'angular2/testing_internal';
@ -25,6 +25,7 @@ import {
ObservableWrapper
} from 'angular2/src/facade/async';
import {RootRouter} from 'angular2/src/router/router';
import {Router, RouterOutlet, RouterLink, RouteParams} from 'angular2/router';
import {
RouteConfig,
@ -34,6 +35,9 @@ import {
Redirect
} from 'angular2/src/router/route_config_decorator';
import {SpyLocation} from 'angular2/src/mock/location_mock';
import {Location} from 'angular2/src/router/location';
import {RouteRegistry} from 'angular2/src/router/route_registry';
import {
OnActivate,
OnDeactivate,
@ -43,9 +47,7 @@ import {
} from 'angular2/src/router/interfaces';
import {CanActivate} from 'angular2/src/router/lifecycle_annotations';
import {ComponentInstruction} from 'angular2/src/router/instruction';
import {TEST_ROUTER_PROVIDERS, RootCmp, compile} from './util';
import {DirectiveResolver} from 'angular2/src/core/linker/directive_resolver';
var cmpInstanceCount;
var log: string[];
@ -59,7 +61,17 @@ export function main() {
var fixture: ComponentFixture;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEachBindings(() => [
RouteRegistry,
DirectiveResolver,
provide(Location, {useClass: SpyLocation}),
provide(Router,
{
useFactory:
(registry, location) => { return new RootRouter(registry, location, MyComp); },
deps: [RouteRegistry, Location]
})
]);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
@ -69,9 +81,17 @@ export function main() {
eventBus = new EventEmitter();
}));
function compile(template: string = "<router-outlet></router-outlet>") {
return tcb.overrideView(MyComp, new View({
template: ('<div>' + template + '</div>'),
directives: [RouterOutlet, RouterLink]
}))
.createAsync(MyComp)
.then((tc) => { fixture = tc; });
}
it('should call the onActivate hook', inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
.then((_) => rtr.navigateByUrl('/on-activate'))
.then((_) => {
@ -84,8 +104,7 @@ export function main() {
it('should wait for a parent component\'s onActivate hook to resolve before calling its child\'s',
inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
.then((_) => {
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
@ -107,8 +126,7 @@ export function main() {
}));
it('should call the onDeactivate hook', inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
.then((_) => rtr.navigateByUrl('/on-deactivate'))
.then((_) => rtr.navigateByUrl('/a'))
@ -122,8 +140,7 @@ export function main() {
it('should wait for a child component\'s onDeactivate hook to resolve before calling its parent\'s',
inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
.then((_) => rtr.navigateByUrl('/parent-deactivate/child-deactivate'))
.then((_) => {
@ -148,8 +165,7 @@ export function main() {
it('should reuse a component when the canReuse hook returns true',
inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
.then((_) => rtr.navigateByUrl('/on-reuse/1/a'))
.then((_) => {
@ -171,8 +187,7 @@ export function main() {
it('should not reuse a component when the canReuse hook returns false',
inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
.then((_) => rtr.navigateByUrl('/never-reuse/1/a'))
.then((_) => {
@ -193,8 +208,7 @@ export function main() {
it('should navigate when canActivate returns true', inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
.then((_) => {
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
@ -214,8 +228,7 @@ export function main() {
it('should not navigate when canActivate returns false',
inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
.then((_) => {
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
@ -235,8 +248,7 @@ export function main() {
it('should navigate away when canDeactivate returns true',
inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
.then((_) => rtr.navigateByUrl('/can-deactivate/a'))
.then((_) => {
@ -261,8 +273,7 @@ export function main() {
it('should not navigate away when canDeactivate returns false',
inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
.then((_) => rtr.navigateByUrl('/can-deactivate/a'))
.then((_) => {
@ -288,8 +299,7 @@ export function main() {
it('should run activation and deactivation hooks in the correct order',
inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
.then((_) => rtr.navigateByUrl('/activation-hooks/child'))
.then((_) => {
@ -315,8 +325,7 @@ export function main() {
}));
it('should only run reuse hooks when reusing', inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
.then((_) => rtr.navigateByUrl('/reuse-hooks/1'))
.then((_) => {
@ -343,7 +352,7 @@ export function main() {
}));
it('should not run reuse hooks when not reusing', inject([AsyncTestCompleter], (async) => {
compile(tcb)
compile()
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
.then((_) => rtr.navigateByUrl('/reuse-hooks/1'))
.then((_) => {
@ -374,16 +383,23 @@ export function main() {
}
@Component({selector: 'a-cmp', template: "A"})
@Component({selector: 'a-cmp'})
@View({template: "A"})
class A {
}
@Component({selector: 'b-cmp', template: "B"})
@Component({selector: 'b-cmp'})
@View({template: "B"})
class B {
}
@Component({selector: 'my-comp'})
class MyComp {
name;
}
function logHook(name: string, next: ComponentInstruction, prev: ComponentInstruction) {
var message = name + ': ' + (isPresent(prev) ? ('/' + prev.urlPath) : 'null') + ' -> ' +
(isPresent(next) ? ('/' + next.urlPath) : 'null');
@ -391,18 +407,16 @@ function logHook(name: string, next: ComponentInstruction, prev: ComponentInstru
ObservableWrapper.callEmit(eventBus, message);
}
@Component({selector: 'activate-cmp', template: 'activate cmp'})
@Component({selector: 'activate-cmp'})
@View({template: 'activate cmp'})
class ActivateCmp implements OnActivate {
onActivate(next: ComponentInstruction, prev: ComponentInstruction) {
logHook('activate', next, prev);
}
}
@Component({
selector: 'parent-activate-cmp',
template: `parent {<router-outlet></router-outlet>}`,
directives: [RouterOutlet]
})
@Component({selector: 'parent-activate-cmp'})
@View({template: `parent {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
@RouteConfig([new Route({path: '/child-activate', component: ActivateCmp})])
class ParentActivateCmp implements OnActivate {
onActivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<any> {
@ -412,14 +426,16 @@ class ParentActivateCmp implements OnActivate {
}
}
@Component({selector: 'deactivate-cmp', template: 'deactivate cmp'})
@Component({selector: 'deactivate-cmp'})
@View({template: 'deactivate cmp'})
class DeactivateCmp implements OnDeactivate {
onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
logHook('deactivate', next, prev);
}
}
@Component({selector: 'deactivate-cmp', template: 'deactivate cmp'})
@Component({selector: 'deactivate-cmp'})
@View({template: 'deactivate cmp'})
class WaitDeactivateCmp implements OnDeactivate {
onDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<any> {
completer = PromiseWrapper.completer();
@ -428,11 +444,8 @@ class WaitDeactivateCmp implements OnDeactivate {
}
}
@Component({
selector: 'parent-deactivate-cmp',
template: `parent {<router-outlet></router-outlet>}`,
directives: [RouterOutlet]
})
@Component({selector: 'parent-deactivate-cmp'})
@View({template: `parent {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
@RouteConfig([new Route({path: '/child-deactivate', component: WaitDeactivateCmp})])
class ParentDeactivateCmp implements OnDeactivate {
onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
@ -440,37 +453,26 @@ class ParentDeactivateCmp implements OnDeactivate {
}
}
@Component({
selector: 'reuse-cmp',
template: `reuse {<router-outlet></router-outlet>}`,
directives: [RouterOutlet]
})
@Component({selector: 'reuse-cmp'})
@View({template: `reuse {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
@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; }
canReuse(next: ComponentInstruction, prev: ComponentInstruction) { return true; }
onReuse(next: ComponentInstruction, prev: ComponentInstruction) { logHook('reuse', next, prev); }
}
@Component({
selector: 'never-reuse-cmp',
template: `reuse {<router-outlet></router-outlet>}`,
directives: [RouterOutlet]
})
@Component({selector: 'never-reuse-cmp'})
@View({template: `reuse {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
@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; }
canReuse(next: ComponentInstruction, prev: ComponentInstruction) { return false; }
onReuse(next: ComponentInstruction, prev: ComponentInstruction) { logHook('reuse', next, prev); }
}
@Component({
selector: 'can-activate-cmp',
template: `canActivate {<router-outlet></router-outlet>}`,
directives: [RouterOutlet]
})
@Component({selector: 'can-activate-cmp'})
@View({template: `canActivate {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})])
@CanActivate(CanActivateCmp.canActivate)
class CanActivateCmp {
@ -481,11 +483,8 @@ class CanActivateCmp {
}
}
@Component({
selector: 'can-deactivate-cmp',
template: `canDeactivate {<router-outlet></router-outlet>}`,
directives: [RouterOutlet]
})
@Component({selector: 'can-deactivate-cmp'})
@View({template: `canDeactivate {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
@RouteConfig([new Route({path: '/a', component: A}), new Route({path: '/b', component: B})])
class CanDeactivateCmp implements CanDeactivate {
canDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<boolean> {
@ -495,7 +494,8 @@ class CanDeactivateCmp implements CanDeactivate {
}
}
@Component({selector: 'all-hooks-child-cmp', template: `child`})
@Component({selector: 'all-hooks-child-cmp'})
@View({template: `child`})
@CanActivate(AllHooksChildCmp.canActivate)
class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate {
canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
@ -517,15 +517,11 @@ class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate {
}
}
@Component({
selector: 'all-hooks-parent-cmp',
template: `<router-outlet></router-outlet>`,
directives: [RouterOutlet]
})
@Component({selector: 'all-hooks-parent-cmp'})
@View({template: `<router-outlet></router-outlet>`, directives: [RouterOutlet]})
@RouteConfig([new Route({path: '/child', component: AllHooksChildCmp})])
@CanActivate(AllHooksParentCmp.canActivate)
class AllHooksParentCmp implements CanDeactivate,
OnDeactivate, OnActivate {
class AllHooksParentCmp implements CanDeactivate, OnDeactivate, OnActivate {
canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
logHook('canDeactivate parent', next, prev);
return true;
@ -545,7 +541,8 @@ class AllHooksParentCmp implements CanDeactivate,
}
}
@Component({selector: 'reuse-hooks-cmp', template: 'reuse hooks cmp'})
@Component({selector: 'reuse-hooks-cmp'})
@View({template: 'reuse hooks cmp'})
@CanActivate(ReuseHooksCmp.canActivate)
class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanDeactivate {
canReuse(next: ComponentInstruction, prev: ComponentInstruction): Promise<any> {
@ -577,11 +574,8 @@ class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanD
}
}
@Component({
selector: 'lifecycle-cmp',
template: `<router-outlet></router-outlet>`,
directives: [RouterOutlet]
})
@Component({selector: 'lifecycle-cmp'})
@View({template: `<router-outlet></router-outlet>`, directives: [RouterOutlet]})
@RouteConfig([
new Route({path: '/a', component: A}),
new Route({path: '/on-activate', component: ActivateCmp}),

View File

@ -10,7 +10,7 @@ import {
expect,
iit,
inject,
beforeEachProviders,
beforeEachBindings,
it,
xit
} from 'angular2/testing_internal';
@ -18,7 +18,8 @@ import {
import {provide, Component, View, Injector, Inject} from 'angular2/core';
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
import {Router, RouterOutlet, RouterLink, RouteParams, RouteData, Location} from 'angular2/router';
import {RootRouter} from 'angular2/src/router/router';
import {Router, RouterOutlet, RouterLink, RouteParams, RouteData} from 'angular2/router';
import {
RouteConfig,
Route,
@ -27,10 +28,14 @@ import {
Redirect
} from 'angular2/src/router/route_config_decorator';
import {TEST_ROUTER_PROVIDERS, RootCmp, compile} from './util';
import {SpyLocation} from 'angular2/src/mock/location_mock';
import {Location} from 'angular2/src/router/location';
import {RouteRegistry} from 'angular2/src/router/route_registry';
import {DirectiveResolver} from 'angular2/src/core/linker/directive_resolver';
var cmpInstanceCount;
var childCmpInstanceCount;
var log: string[];
export function main() {
describe('navigation', () => {
@ -39,18 +44,37 @@ export function main() {
var fixture: ComponentFixture;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEachBindings(() => [
RouteRegistry,
DirectiveResolver,
provide(Location, {useClass: SpyLocation}),
provide(Router,
{
useFactory:
(registry, location) => { return new RootRouter(registry, location, MyComp); },
deps: [RouteRegistry, Location]
})
]);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
rtr = router;
childCmpInstanceCount = 0;
cmpInstanceCount = 0;
log = [];
}));
function compile(template: string = "<router-outlet></router-outlet>") {
return tcb.overrideView(MyComp, new View({
template: ('<div>' + template + '</div>'),
directives: [RouterOutlet, RouterLink]
}))
.createAsync(MyComp)
.then((tc) => { fixture = tc; });
}
it('should work in a simple case', inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([new Route({path: '/test', component: HelloCmp})]))
.then((_) => rtr.navigateByUrl('/test'))
.then((_) => {
@ -63,8 +87,7 @@ export function main() {
it('should navigate between components with different parameters',
inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([new Route({path: '/user/:name', component: UserCmp})]))
.then((_) => rtr.navigateByUrl('/user/brian'))
.then((_) => {
@ -79,9 +102,9 @@ export function main() {
});
}));
it('should navigate to child routes', inject([AsyncTestCompleter], (async) => {
compile(tcb, 'outer { <router-outlet></router-outlet> }')
.then((rtc) => {fixture = rtc})
compile('outer { <router-outlet></router-outlet> }')
.then((_) => rtr.config([new Route({path: '/a/...', component: ParentCmp})]))
.then((_) => rtr.navigateByUrl('/a/b'))
.then((_) => {
@ -93,9 +116,7 @@ export function main() {
it('should navigate to child routes that capture an empty path',
inject([AsyncTestCompleter], (async) => {
compile(tcb, 'outer { <router-outlet></router-outlet> }')
.then((rtc) => {fixture = rtc})
compile('outer { <router-outlet></router-outlet> }')
.then((_) => rtr.config([new Route({path: '/a/...', component: ParentCmp})]))
.then((_) => rtr.navigateByUrl('/a'))
.then((_) => {
@ -105,9 +126,9 @@ export function main() {
});
}));
it('should navigate to child routes of async routes', inject([AsyncTestCompleter], (async) => {
compile(tcb, 'outer { <router-outlet></router-outlet> }')
.then((rtc) => {fixture = rtc})
compile('outer { <router-outlet></router-outlet> }')
.then((_) => rtr.config([new AsyncRoute({path: '/a/...', loader: parentLoader})]))
.then((_) => rtr.navigateByUrl('/a/b'))
.then((_) => {
@ -117,9 +138,26 @@ export function main() {
});
}));
it('should recognize and apply redirects',
inject([AsyncTestCompleter, Location], (async, location) => {
compile()
.then((_) => rtr.config([
new Redirect({path: '/original', redirectTo: '/redirected'}),
new Route({path: '/redirected', component: HelloCmp})
]))
.then((_) => rtr.navigateByUrl('/original'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement).toHaveText('hello');
expect(location.urlChanges).toEqual(['/redirected']);
async.done();
});
}));
it('should reuse common parent components', inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([new Route({path: '/team/:id/...', component: TeamCmp})]))
.then((_) => rtr.navigateByUrl('/team/angular/user/rado'))
.then((_) => {
@ -139,8 +177,7 @@ export function main() {
it('should not reuse children when parent components change',
inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([new Route({path: '/team/:id/...', component: TeamCmp})]))
.then((_) => rtr.navigateByUrl('/team/angular/user/rado'))
.then((_) => {
@ -160,8 +197,7 @@ export function main() {
}));
it('should inject route data into component', inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([
new Route({path: '/route-data', component: RouteDataCmp, data: {isAdmin: true}})
]))
@ -175,11 +211,10 @@ export function main() {
it('should inject route data into component with AsyncRoute',
inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config([
new AsyncRoute(
{path: '/route-data', loader: asyncRouteDataCmp, data: {isAdmin: true}})
{path: '/route-data', loader: AsyncRouteDataCmp, data: {isAdmin: true}})
]))
.then((_) => rtr.navigateByUrl('/route-data'))
.then((_) => {
@ -191,8 +226,7 @@ export function main() {
it('should inject empty object if the route has no data property',
inject([AsyncTestCompleter], (async) => {
compile(tcb)
.then((rtc) => {fixture = rtc})
compile()
.then((_) => rtr.config(
[new Route({path: '/route-data-default', component: RouteDataCmp})]))
.then((_) => rtr.navigateByUrl('/route-data-default'))
@ -202,28 +236,45 @@ export function main() {
async.done();
});
}));
describe('auxiliary routes', () => {
it('should recognize a simple case', inject([AsyncTestCompleter], (async) => {
compile()
.then((_) => rtr.config([new Route({path: '/...', component: AuxCmp})]))
.then((_) => rtr.navigateByUrl('/hello(modal)'))
.then((_) => {
fixture.detectChanges();
expect(fixture.debugElement.nativeElement)
.toHaveText('main {hello} | aux {modal}');
async.done();
});
}));
});
});
}
@Component({selector: 'hello-cmp', template: `{{greeting}}`})
@Component({selector: 'hello-cmp'})
@View({template: "{{greeting}}"})
class HelloCmp {
greeting: string;
constructor() { this.greeting = 'hello'; }
constructor() { this.greeting = "hello"; }
}
function asyncRouteDataCmp() {
function AsyncRouteDataCmp() {
return PromiseWrapper.resolve(RouteDataCmp);
}
@Component({selector: 'data-cmp', template: `{{myData}}`})
@Component({selector: 'data-cmp'})
@View({template: "{{myData}}"})
class RouteDataCmp {
myData: boolean;
constructor(data: RouteData) { this.myData = data.get('isAdmin'); }
}
@Component({selector: 'user-cmp', template: `hello {{user}}`})
@Component({selector: 'user-cmp'})
@View({template: "hello {{user}}"})
class UserCmp {
user: string;
constructor(params: RouteParams) {
@ -237,9 +288,9 @@ function parentLoader() {
return PromiseWrapper.resolve(ParentCmp);
}
@Component({
selector: 'parent-cmp',
template: `inner { <router-outlet></router-outlet> }`,
@Component({selector: 'parent-cmp'})
@View({
template: "inner { <router-outlet></router-outlet> }",
directives: [RouterOutlet],
})
@RouteConfig([
@ -247,12 +298,13 @@ function parentLoader() {
new Route({path: '/', component: HelloCmp}),
])
class ParentCmp {
constructor() {}
}
@Component({
selector: 'team-cmp',
template: `team {{id}} { <router-outlet></router-outlet> }`,
@Component({selector: 'team-cmp'})
@View({
template: "team {{id}} { <router-outlet></router-outlet> }",
directives: [RouterOutlet],
})
@RouteConfig([new Route({path: '/user/:name', component: UserCmp})])
@ -263,3 +315,27 @@ class TeamCmp {
cmpInstanceCount += 1;
}
}
@Component({selector: 'my-comp'})
class MyComp {
name;
}
@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 {
}

View File

@ -1,121 +0,0 @@
import {
RootTestComponent,
AsyncTestCompleter,
TestComponentBuilder,
beforeEach,
ddescribe,
xdescribe,
describe,
el,
expect,
iit,
inject,
beforeEachProviders,
it,
xit
} from 'angular2/testing_internal';
import {Router, RouterOutlet, RouterLink, RouteParams, RouteData, Location} from 'angular2/router';
import {
RouteConfig,
Route,
AuxRoute,
AsyncRoute,
Redirect
} from 'angular2/src/router/route_config_decorator';
import {TEST_ROUTER_PROVIDERS, RootCmp, compile} from './util';
import {HelloCmp, RedirectToParentCmp} from './impl/fixture_components';
var cmpInstanceCount;
var childCmpInstanceCount;
export function main() {
describe('redirects', () => {
var tcb: TestComponentBuilder;
var rootTC: RootTestComponent;
var rtr;
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
tcb = tcBuilder;
rtr = router;
childCmpInstanceCount = 0;
cmpInstanceCount = 0;
}));
it('should apply when navigating by URL',
inject([AsyncTestCompleter, Location], (async, location) => {
compile(tcb)
.then((rtc) => {rootTC = rtc})
.then((_) => rtr.config([
new Redirect({path: '/original', redirectTo: ['Hello']}),
new Route({path: '/redirected', component: HelloCmp, name: 'Hello'})
]))
.then((_) => rtr.navigateByUrl('/original'))
.then((_) => {
rootTC.detectChanges();
expect(rootTC.debugElement.nativeElement).toHaveText('hello');
expect(location.urlChanges).toEqual(['/redirected']);
async.done();
});
}));
it('should recognize and apply absolute redirects',
inject([AsyncTestCompleter, Location], (async, location) => {
compile(tcb)
.then((rtc) => {rootTC = rtc})
.then((_) => rtr.config([
new Redirect({path: '/original', redirectTo: ['/Hello']}),
new Route({path: '/redirected', component: HelloCmp, name: 'Hello'})
]))
.then((_) => rtr.navigateByUrl('/original'))
.then((_) => {
rootTC.detectChanges();
expect(rootTC.debugElement.nativeElement).toHaveText('hello');
expect(location.urlChanges).toEqual(['/redirected']);
async.done();
});
}));
it('should recognize and apply relative child redirects',
inject([AsyncTestCompleter, Location], (async, location) => {
compile(tcb)
.then((rtc) => {rootTC = rtc})
.then((_) => rtr.config([
new Redirect({path: '/original', redirectTo: ['./Hello']}),
new Route({path: '/redirected', component: HelloCmp, name: 'Hello'})
]))
.then((_) => rtr.navigateByUrl('/original'))
.then((_) => {
rootTC.detectChanges();
expect(rootTC.debugElement.nativeElement).toHaveText('hello');
expect(location.urlChanges).toEqual(['/redirected']);
async.done();
});
}));
it('should recognize and apply relative parent redirects',
inject([AsyncTestCompleter, Location], (async, location) => {
compile(tcb)
.then((rtc) => {rootTC = rtc})
.then((_) => rtr.config([
new Route({path: '/original/...', component: RedirectToParentCmp}),
new Route({path: '/redirected', component: HelloCmp, name: 'HelloSib'})
]))
.then((_) => rtr.navigateByUrl('/original/child-redirect'))
.then((_) => {
rootTC.detectChanges();
expect(rootTC.debugElement.nativeElement).toHaveText('hello');
expect(location.urlChanges).toEqual(['/redirected']);
async.done();
});
}));
});
}

View File

@ -38,39 +38,44 @@ import {ApplicationRef} from 'angular2/src/core/application_ref';
import {MockApplicationRef} from 'angular2/src/mock/mock_application_ref';
export function main() {
describe('router bootstrap', () => {
beforeEachProviders(() => [
ROUTER_PROVIDERS,
provide(LocationStrategy, {useClass: MockLocationStrategy}),
provide(ApplicationRef, {useClass: MockApplicationRef})
]);
describe('router injectables', () => {
beforeEachProviders(() => {
return [
ROUTER_PROVIDERS,
provide(LocationStrategy, {useClass: MockLocationStrategy}),
provide(ApplicationRef, {useClass: MockApplicationRef})
];
});
// do not refactor out the `bootstrap` functionality. We still want to
// keep this test around so we can ensure that bootstrapping a router works
it('should bootstrap a simple app', inject([AsyncTestCompleter], (async) => {
var fakeDoc = DOM.createHtmlDocument();
var el = DOM.createElement('app-cmp', fakeDoc);
DOM.appendChild(fakeDoc.body, el);
describe('bootstrap functionality', () => {
it('should bootstrap a simple app', inject([AsyncTestCompleter], (async) => {
var fakeDoc = DOM.createHtmlDocument();
var el = DOM.createElement('app-cmp', fakeDoc);
DOM.appendChild(fakeDoc.body, el);
bootstrap(AppCmp,
[
ROUTER_PROVIDERS,
provide(ROUTER_PRIMARY_COMPONENT, {useValue: AppCmp}),
provide(LocationStrategy, {useClass: MockLocationStrategy}),
provide(DOCUMENT, {useValue: fakeDoc})
])
.then((applicationRef) => {
var router = applicationRef.hostComponent.router;
router.subscribe((_) => {
expect(el).toHaveText('outer { hello }');
expect(applicationRef.hostComponent.location.path()).toEqual('');
async.done();
bootstrap(AppCmp,
[
ROUTER_PROVIDERS,
provide(ROUTER_PRIMARY_COMPONENT, {useValue: AppCmp}),
provide(LocationStrategy, {useClass: MockLocationStrategy}),
provide(DOCUMENT, {useValue: fakeDoc})
])
.then((applicationRef) => {
var router = applicationRef.hostComponent.router;
router.subscribe((_) => {
expect(el).toHaveText('outer { hello }');
expect(applicationRef.hostComponent.location.path()).toEqual('');
async.done();
});
});
});
}));
}));
});
describe('broken app', () => {
beforeEachProviders(() => [provide(ROUTER_PRIMARY_COMPONENT, {useValue: BrokenAppCmp})]);
beforeEachProviders(
() => { return [provide(ROUTER_PRIMARY_COMPONENT, {useValue: BrokenAppCmp})]; });
it('should rethrow exceptions from component constructors',
inject([AsyncTestCompleter, TestComponentBuilder], (async, tcb: TestComponentBuilder) => {
@ -86,7 +91,8 @@ export function main() {
});
describe('back button app', () => {
beforeEachProviders(() => [provide(ROUTER_PRIMARY_COMPONENT, {useValue: HierarchyAppCmp})]);
beforeEachProviders(
() => { return [provide(ROUTER_PRIMARY_COMPONENT, {useValue: HierarchyAppCmp})]; });
it('should change the url without pushing a new history state for back navigations',
inject([AsyncTestCompleter, TestComponentBuilder], (async, tcb: TestComponentBuilder) => {
@ -178,7 +184,7 @@ export function main() {
}));
});
});
// TODO: add a test in which the child component has bindings
describe('querystring params app', () => {
beforeEachProviders(
@ -237,21 +243,20 @@ export function main() {
}
@Component({selector: 'hello-cmp', template: 'hello'})
@Component({selector: 'hello-cmp'})
@View({template: 'hello'})
class HelloCmp {
public message: string;
}
@Component({selector: 'hello2-cmp', template: 'hello2'})
@Component({selector: 'hello2-cmp'})
@View({template: 'hello2'})
class Hello2Cmp {
public greeting: string;
}
@Component({
selector: 'app-cmp',
template: `outer { <router-outlet></router-outlet> }`,
directives: ROUTER_DIRECTIVES
})
@Component({selector: 'app-cmp'})
@View({template: "outer { <router-outlet></router-outlet> }", directives: ROUTER_DIRECTIVES})
@RouteConfig([new Route({path: '/', component: HelloCmp})])
class AppCmp {
constructor(public router: Router, public location: LocationStrategy) {}
@ -278,29 +283,20 @@ class AppWithViewChildren implements AfterViewInit {
afterViewInit() { this.helloCmp.message = 'Ahoy'; }
}
@Component({
selector: 'parent-cmp',
template: `parent { <router-outlet></router-outlet> }`,
directives: ROUTER_DIRECTIVES
})
@Component({selector: 'parent-cmp'})
@View({template: `parent { <router-outlet></router-outlet> }`, directives: ROUTER_DIRECTIVES})
@RouteConfig([new Route({path: '/child', component: HelloCmp})])
class ParentCmp {
}
@Component({
selector: 'super-parent-cmp',
template: `super-parent { <router-outlet></router-outlet> }`,
directives: ROUTER_DIRECTIVES
})
@Component({selector: 'super-parent-cmp'})
@View({template: `super-parent { <router-outlet></router-outlet> }`, directives: ROUTER_DIRECTIVES})
@RouteConfig([new Route({path: '/child', component: Hello2Cmp})])
class SuperParentCmp {
}
@Component({
selector: 'app-cmp',
template: `root { <router-outlet></router-outlet> }`,
directives: ROUTER_DIRECTIVES
})
@Component({selector: 'app-cmp'})
@View({template: `root { <router-outlet></router-outlet> }`, directives: ROUTER_DIRECTIVES})
@RouteConfig([
new Route({path: '/parent/...', component: ParentCmp}),
new Route({path: '/super-parent/...', component: SuperParentCmp})
@ -309,32 +305,28 @@ class HierarchyAppCmp {
constructor(public router: Router, public location: LocationStrategy) {}
}
@Component({selector: 'qs-cmp', template: `qParam = {{q}}`})
@Component({selector: 'qs-cmp'})
@View({template: "qParam = {{q}}"})
class QSCmp {
q: string;
constructor(params: RouteParams) { this.q = params.get('q'); }
}
@Component({
selector: 'app-cmp',
template: `<router-outlet></router-outlet>`,
directives: ROUTER_DIRECTIVES
})
@Component({selector: 'app-cmp'})
@View({template: `<router-outlet></router-outlet>`, directives: ROUTER_DIRECTIVES})
@RouteConfig([new Route({path: '/qs', component: QSCmp})])
class QueryStringAppCmp {
constructor(public router: Router, public location: LocationStrategy) {}
}
@Component({selector: 'oops-cmp', template: "oh no"})
@Component({selector: 'oops-cmp'})
@View({template: "oh no"})
class BrokenCmp {
constructor() { throw new BaseException('oops!'); }
}
@Component({
selector: 'app-cmp',
template: `outer { <router-outlet></router-outlet> }`,
directives: ROUTER_DIRECTIVES
})
@Component({selector: 'app-cmp'})
@View({template: `outer { <router-outlet></router-outlet> }`, directives: ROUTER_DIRECTIVES})
@RouteConfig([new Route({path: '/cause-error', component: BrokenCmp})])
class BrokenAppCmp {
constructor(public router: Router, public location: LocationStrategy) {}

View File

@ -9,7 +9,7 @@ import {
expect,
iit,
inject,
beforeEachProviders,
beforeEachBindings,
it,
xit,
TestComponentBuilder,
@ -21,7 +21,7 @@ import {NumberWrapper} from 'angular2/src/facade/lang';
import {PromiseWrapper} from 'angular2/src/facade/async';
import {ListWrapper} from 'angular2/src/facade/collection';
import {provide, Component, View, DirectiveResolver} from 'angular2/core';
import {provide, Component, DirectiveResolver} from 'angular2/core';
import {SpyLocation} from 'angular2/src/mock/location_mock';
import {
@ -47,7 +47,7 @@ export function main() {
var fixture: ComponentFixture;
var router, location;
beforeEachProviders(() => [
beforeEachBindings(() => [
RouteRegistry,
DirectiveResolver,
provide(Location, {useClass: SpyLocation}),
@ -240,8 +240,8 @@ export function main() {
.then((_) => router.config([new Route({path: '/...', component: AuxLinkCmp})]))
.then((_) => router.navigateByUrl('/'))
.then((_) => {
fixture.detectChanges();
expect(DOM.getAttribute(fixture.debugElement.componentViewChildren[1]
rootTC.detectChanges();
expect(DOM.getAttribute(rootTC.debugElement.componentViewChildren[1]
.componentViewChildren[0]
.nativeElement,
'href'))
@ -386,7 +386,10 @@ class MyComp {
name;
}
@Component({selector: 'user-cmp', template: "hello {{user}}"})
@Component({
selector: 'user-cmp',
template: "hello {{user}}"
})
class UserCmp {
user: string;
constructor(params: RouteParams) { this.user = params.get('name'); }
@ -422,11 +425,17 @@ class NoPrefixSiblingPageCmp {
}
}
@Component({selector: 'hello-cmp', template: 'hello'})
@Component({
selector: 'hello-cmp',
template: 'hello'
})
class HelloCmp {
}
@Component({selector: 'hello2-cmp', template: 'hello2'})
@Component({
selector: 'hello2-cmp',
template: 'hello2'
})
class Hello2Cmp {
}
@ -446,6 +455,7 @@ function parentCmpLoader() {
new Route({path: '/better-grandchild', component: Hello2Cmp, name: 'BetterGrandchild'})
])
class ParentCmp {
constructor(public router: Router) {}
}
@Component({

View File

@ -1,24 +0,0 @@
import {
describeRouter,
ddescribeRouter,
describeWith,
describeWithout,
describeWithAndWithout,
itShouldRoute
} from './util';
import {registerSpecs} from './impl/sync_route_spec_impl';
export function main() {
registerSpecs();
describeRouter('sync routes', () => {
describeWithout('children', () => { describeWithAndWithout('params', itShouldRoute); });
describeWith('sync children', () => {
describeWithout('default routes', () => { describeWithAndWithout('params', itShouldRoute); });
describeWith('default routes', () => { describeWithout('params', itShouldRoute); });
});
});
}

View File

@ -1,138 +0,0 @@
import {provide, Provider, Component, View} from 'angular2/core';
export {Provider} from 'angular2/core';
import {Type, isBlank} from 'angular2/src/facade/lang';
import {BaseException} from 'angular2/src/facade/exceptions';
import {
RootTestComponent,
AsyncTestCompleter,
TestComponentBuilder,
beforeEach,
ddescribe,
xdescribe,
describe,
el,
inject,
beforeEachProviders,
it,
xit
} from 'angular2/testing_internal';
import {RootRouter} from 'angular2/src/router/router';
import {Router, ROUTER_DIRECTIVES} from 'angular2/router';
import {SpyLocation} from 'angular2/src/mock/location_mock';
import {Location} from 'angular2/src/router/location';
import {RouteRegistry} from 'angular2/src/router/route_registry';
import {DirectiveResolver} from 'angular2/src/core/linker/directive_resolver';
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
export {ComponentFixture} from 'angular2/testing_internal';
/**
* Router test helpers and fixtures
*/
@Component({
selector: 'root-comp',
template: `<router-outlet></router-outlet>`,
directives: [ROUTER_DIRECTIVES]
})
export class RootCmp {
name: string;
}
export function compile(tcb: TestComponentBuilder,
template: string = "<router-outlet></router-outlet>") {
return tcb.overrideTemplate(RootCmp, ('<div>' + template + '</div>')).createAsync(RootCmp);
}
export var TEST_ROUTER_PROVIDERS = [
RouteRegistry,
DirectiveResolver,
provide(Location, {useClass: SpyLocation}),
provide(
Router,
{
useFactory: (registry, location) => { return new RootRouter(registry, location, RootCmp);},
deps: [RouteRegistry, Location]
})
];
export function clickOnElement(anchorEl) {
var dispatchedEvent = DOM.createMouseEvent('click');
DOM.dispatchEvent(anchorEl, dispatchedEvent);
return dispatchedEvent;
}
export function getHref(elt) {
return DOM.getAttribute(elt, 'href');
}
/**
* Router integration suite DSL
*/
var specNameBuilder = [];
// we add the specs themselves onto this map
export var specs = {};
export function describeRouter(description: string, fn: Function, exclusive = false): void {
var specName = descriptionToSpecName(description);
specNameBuilder.push(specName);
describe(description, fn);
specNameBuilder.pop();
}
export function ddescribeRouter(description: string, fn: Function, exclusive = false): void {
describeRouter(description, fn, true);
}
export function describeWithAndWithout(description: string, fn: Function): void {
// the "without" case is usually simpler, so we opt to run this spec first
describeWithout(description, fn);
describeWith(description, fn);
}
export function describeWith(description: string, fn: Function): void {
var specName = 'with ' + description;
specNameBuilder.push(specName);
describe(specName, fn);
specNameBuilder.pop();
}
export function describeWithout(description: string, fn: Function): void {
var specName = 'without ' + description;
specNameBuilder.push(specName);
describe(specName, fn);
specNameBuilder.pop();
}
function descriptionToSpecName(description: string): string {
return spaceCaseToCamelCase(description);
}
// this helper looks up the suite registered from the "impl" folder in this directory
export function itShouldRoute() {
var specSuiteName = spaceCaseToCamelCase(specNameBuilder.join(' '));
var spec = specs[specSuiteName];
if (isBlank(spec)) {
throw new BaseException(`Router integration spec suite "${specSuiteName}" was not found.`);
} else {
// todo: remove spec from map, throw if there are extra left over??
spec();
}
}
function spaceCaseToCamelCase(str: string): string {
var words = str.split(' ');
var first = words.shift();
return first + words.map(title).join('');
}
function title(str: string): string {
return str[0].toUpperCase() + str.substring(1);
}

View File

@ -12,82 +12,100 @@ import {
import {PathRecognizer} from 'angular2/src/router/path_recognizer';
import {parser, Url, RootUrl} from 'angular2/src/router/url_parser';
import {SyncRouteHandler} from 'angular2/src/router/sync_route_handler';
class DummyClass {
constructor() {}
}
var mockRouteHandler = new SyncRouteHandler(DummyClass);
export function main() {
describe('PathRecognizer', () => {
it('should throw when given an invalid path', () => {
expect(() => new PathRecognizer('/hi#'))
expect(() => new PathRecognizer('/hi#', mockRouteHandler))
.toThrowError(`Path "/hi#" should not include "#". Use "HashLocationStrategy" instead.`);
expect(() => new PathRecognizer('hi?'))
expect(() => new PathRecognizer('hi?', mockRouteHandler))
.toThrowError(`Path "hi?" contains "?" which is not allowed in a route config.`);
expect(() => new PathRecognizer('hi;'))
expect(() => new PathRecognizer('hi;', mockRouteHandler))
.toThrowError(`Path "hi;" contains ";" which is not allowed in a route config.`);
expect(() => new PathRecognizer('hi='))
expect(() => new PathRecognizer('hi=', mockRouteHandler))
.toThrowError(`Path "hi=" contains "=" which is not allowed in a route config.`);
expect(() => new PathRecognizer('hi('))
expect(() => new PathRecognizer('hi(', mockRouteHandler))
.toThrowError(`Path "hi(" contains "(" which is not allowed in a route config.`);
expect(() => new PathRecognizer('hi)'))
expect(() => new PathRecognizer('hi)', mockRouteHandler))
.toThrowError(`Path "hi)" contains ")" which is not allowed in a route config.`);
expect(() => new PathRecognizer('hi//there'))
expect(() => new PathRecognizer('hi//there', mockRouteHandler))
.toThrowError(`Path "hi//there" contains "//" which is not allowed in a route config.`);
});
it('should return the same instruction instance when recognizing the same path', () => {
var rec = new PathRecognizer('/one', mockRouteHandler);
var one = new Url('one', null, null, {});
var firstMatch = rec.recognize(one);
var secondMatch = rec.recognize(one);
expect(firstMatch.instruction).toBe(secondMatch.instruction);
});
describe('querystring params', () => {
it('should parse querystring params so long as the recognizer is a root', () => {
var rec = new PathRecognizer('/hello/there');
var rec = new PathRecognizer('/hello/there', mockRouteHandler);
var url = parser.parse('/hello/there?name=igor');
var match = rec.recognize(url);
expect(match['allParams']).toEqual({'name': 'igor'});
expect(match.instruction.params).toEqual({'name': 'igor'});
});
it('should return a combined map of parameters with the param expected in the URL path',
() => {
var rec = new PathRecognizer('/hello/:name');
var rec = new PathRecognizer('/hello/:name', mockRouteHandler);
var url = parser.parse('/hello/paul?topic=success');
var match = rec.recognize(url);
expect(match['allParams']).toEqual({'name': 'paul', 'topic': 'success'});
expect(match.instruction.params).toEqual({'name': 'paul', 'topic': 'success'});
});
});
describe('matrix params', () => {
it('should be parsed along with dynamic paths', () => {
var rec = new PathRecognizer('/hello/:id');
var rec = new PathRecognizer('/hello/:id', mockRouteHandler);
var url = new Url('hello', new Url('matias', null, null, {'key': 'value'}));
var match = rec.recognize(url);
expect(match['allParams']).toEqual({'id': 'matias', 'key': 'value'});
expect(match.instruction.params).toEqual({'id': 'matias', 'key': 'value'});
});
it('should be parsed on a static path', () => {
var rec = new PathRecognizer('/person');
var rec = new PathRecognizer('/person', mockRouteHandler);
var url = new Url('person', null, null, {'name': 'dave'});
var match = rec.recognize(url);
expect(match['allParams']).toEqual({'name': 'dave'});
expect(match.instruction.params).toEqual({'name': 'dave'});
});
it('should be ignored on a wildcard segment', () => {
var rec = new PathRecognizer('/wild/*everything');
var rec = new PathRecognizer('/wild/*everything', mockRouteHandler);
var url = parser.parse('/wild/super;variable=value');
var match = rec.recognize(url);
expect(match['allParams']).toEqual({'everything': 'super;variable=value'});
expect(match.instruction.params).toEqual({'everything': 'super;variable=value'});
});
it('should set matrix param values to true when no value is present', () => {
var rec = new PathRecognizer('/path');
var rec = new PathRecognizer('/path', mockRouteHandler);
var url = new Url('path', null, null, {'one': true, 'two': true, 'three': '3'});
var match = rec.recognize(url);
expect(match['allParams']).toEqual({'one': true, 'two': true, 'three': '3'});
expect(match.instruction.params).toEqual({'one': true, 'two': true, 'three': '3'});
});
it('should be parsed on the final segment of the path', () => {
var rec = new PathRecognizer('/one/two/three');
var rec = new PathRecognizer('/one/two/three', mockRouteHandler);
var three = new Url('three', null, null, {'c': '3'});
var two = new Url('two', three, null, {'b': '2'});
var one = new Url('one', two, null, {'a': '1'});
var match = rec.recognize(one);
expect(match['allParams']).toEqual({'c': '3'});
expect(match.instruction.params).toEqual({'c': '3'});
});
});
});

View File

@ -214,10 +214,7 @@ class HelloCmp {
@Component({selector: 'app-cmp'})
@View({template: `root { <router-outlet></router-outlet> }`, directives: ROUTER_DIRECTIVES})
@RouteConfig([
{path: '/before', redirectTo: ['Hello']},
{path: '/after', component: HelloCmp, name: 'Hello'}
])
@RouteConfig([{path: '/before', redirectTo: '/after'}, {path: '/after', component: HelloCmp}])
class RedirectAppCmp {
constructor(public router: Router, public location: LocationStrategy) {}
}

View File

@ -0,0 +1,185 @@
import {
AsyncTestCompleter,
describe,
it,
iit,
ddescribe,
expect,
inject,
beforeEach,
SpyObject
} from 'angular2/testing_internal';
import {Map, StringMapWrapper} from 'angular2/src/facade/collection';
import {RouteRecognizer} from 'angular2/src/router/route_recognizer';
import {ComponentInstruction} from 'angular2/src/router/instruction';
import {Route, Redirect} from 'angular2/src/router/route_config_decorator';
import {parser} from 'angular2/src/router/url_parser';
export function main() {
describe('RouteRecognizer', () => {
var recognizer;
beforeEach(() => { recognizer = new RouteRecognizer(); });
it('should recognize a static segment', () => {
recognizer.config(new Route({path: '/test', component: DummyCmpA}));
var solution = recognize(recognizer, '/test');
expect(getComponentType(solution)).toEqual(DummyCmpA);
});
it('should recognize a single slash', () => {
recognizer.config(new Route({path: '/', component: DummyCmpA}));
var solution = recognize(recognizer, '/');
expect(getComponentType(solution)).toEqual(DummyCmpA);
});
it('should recognize a dynamic segment', () => {
recognizer.config(new Route({path: '/user/:name', component: DummyCmpA}));
var solution = recognize(recognizer, '/user/brian');
expect(getComponentType(solution)).toEqual(DummyCmpA);
expect(solution.params).toEqual({'name': 'brian'});
});
it('should recognize a star segment', () => {
recognizer.config(new Route({path: '/first/*rest', component: DummyCmpA}));
var solution = recognize(recognizer, '/first/second/third');
expect(getComponentType(solution)).toEqual(DummyCmpA);
expect(solution.params).toEqual({'rest': 'second/third'});
});
it('should throw when given two routes that start with the same static segment', () => {
recognizer.config(new Route({path: '/hello', component: DummyCmpA}));
expect(() => recognizer.config(new Route({path: '/hello', component: DummyCmpB})))
.toThrowError('Configuration \'/hello\' conflicts with existing route \'/hello\'');
});
it('should throw when given two routes that have dynamic segments in the same order', () => {
recognizer.config(new Route({path: '/hello/:person/how/:doyoudou', component: DummyCmpA}));
expect(() => recognizer.config(
new Route({path: '/hello/:friend/how/:areyou', component: DummyCmpA})))
.toThrowError(
'Configuration \'/hello/:friend/how/:areyou\' conflicts with existing route \'/hello/:person/how/:doyoudou\'');
});
it('should recognize redirects', () => {
recognizer.config(new Route({path: '/b', component: DummyCmpA}));
recognizer.config(new Redirect({path: '/a', redirectTo: 'b'}));
var solution = recognize(recognizer, '/a');
expect(getComponentType(solution)).toEqual(DummyCmpA);
expect(solution.urlPath).toEqual('b');
});
it('should not perform root URL redirect on a non-root route', () => {
recognizer.config(new Redirect({path: '/', redirectTo: '/foo'}));
recognizer.config(new Route({path: '/bar', component: DummyCmpA}));
var solution = recognize(recognizer, '/bar');
expect(solution.componentType).toEqual(DummyCmpA);
expect(solution.urlPath).toEqual('bar');
});
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}));
var solution;
solution = recognize(recognizer, '/');
expect(solution.urlPath).toEqual('matias');
solution = recognize(recognizer, '/fatias');
expect(solution.urlPath).toEqual('fatias');
solution = recognize(recognizer, '');
expect(solution.urlPath).toEqual('matias');
});
it('should generate URLs with params', () => {
recognizer.config(new Route({path: '/app/user/:name', component: DummyCmpA, name: 'User'}));
var instruction = recognizer.generate('User', {'name': 'misko'});
expect(instruction.urlPath).toEqual('app/user/misko');
});
it('should generate URLs with numeric params', () => {
recognizer.config(new Route({path: '/app/page/:number', component: DummyCmpA, name: 'Page'}));
expect(recognizer.generate('Page', {'number': 42}).urlPath).toEqual('app/page/42');
});
it('should throw in the absence of required params URLs', () => {
recognizer.config(new Route({path: 'app/user/:name', component: DummyCmpA, name: 'User'}));
expect(() => recognizer.generate('User', {}))
.toThrowError('Route generator for \'name\' was not included in parameters passed.');
});
it('should throw if the route alias is not CamelCase', () => {
expect(() => recognizer.config(
new Route({path: 'app/user/:name', component: DummyCmpA, name: 'user'})))
.toThrowError(
`Route "app/user/:name" with name "user" does not begin with an uppercase letter. Route names should be CamelCase like "User".`);
});
describe('params', () => {
it('should recognize parameters within the URL path', () => {
recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, name: 'User'}));
var solution = recognize(recognizer, '/profile/matsko?comments=all');
expect(solution.params).toEqual({'name': 'matsko', 'comments': 'all'});
});
it('should generate and populate the given static-based route with querystring params',
() => {
recognizer.config(
new Route({path: 'forum/featured', component: DummyCmpA, name: 'ForumPage'}));
var params = {'start': 10, 'end': 100};
var result = recognizer.generate('ForumPage', params);
expect(result.urlPath).toEqual('forum/featured');
expect(result.urlParams).toEqual(['start=10', 'end=100']);
});
it('should prefer positional params over query params', () => {
recognizer.config(new Route({path: 'profile/:name', component: DummyCmpA, name: 'User'}));
var solution = recognize(recognizer, '/profile/yegor?name=igor');
expect(solution.params).toEqual({'name': 'yegor'});
});
it('should ignore matrix params for the top-level component', () => {
recognizer.config(new Route({path: '/home/:subject', component: DummyCmpA, name: 'User'}));
var solution = recognize(recognizer, '/home;sort=asc/zero;one=1?two=2');
expect(solution.params).toEqual({'subject': 'zero', 'two': '2'});
});
});
});
}
function recognize(recognizer: RouteRecognizer, url: string): ComponentInstruction {
return recognizer.recognize(parser.parse(url))[0].instruction;
}
function getComponentType(routeMatch: ComponentInstruction): any {
return routeMatch.componentType;
}
class DummyCmpA {}
class DummyCmpB {}

View File

@ -21,9 +21,9 @@ import {
AuxRoute,
AsyncRoute
} from 'angular2/src/router/route_config_decorator';
import {stringifyInstruction} from 'angular2/src/router/instruction';
import {IS_DART} from 'angular2/src/facade/lang';
export function main() {
describe('RouteRegistry', () => {
var registry;
@ -34,7 +34,7 @@ export function main() {
registry.config(RootHostCmp, new Route({path: '/', component: DummyCmpA}));
registry.config(RootHostCmp, new Route({path: '/test', component: DummyCmpB}));
registry.recognize('/test', [RootHostCmp])
registry.recognize('/test', RootHostCmp)
.then((instruction) => {
expect(instruction.component.componentType).toBe(DummyCmpB);
async.done();
@ -45,32 +45,28 @@ export function main() {
registry.config(RootHostCmp,
new Route({path: '/first/...', component: DummyParentCmp, name: 'FirstCmp'}));
expect(stringifyInstruction(registry.generate(['FirstCmp', 'SecondCmp'], [RootHostCmp])))
expect(stringifyInstruction(registry.generate(['FirstCmp', 'SecondCmp'], RootHostCmp)))
.toEqual('first/second');
expect(stringifyInstruction(registry.generate(['SecondCmp'], [DummyParentCmp])))
expect(stringifyInstruction(registry.generate(['SecondCmp'], DummyParentCmp)))
.toEqual('second');
});
it('should generate URLs that account for default routes', () => {
xit('should generate URLs that account for redirects', () => {
registry.config(
RootHostCmp,
new Route({path: '/first/...', component: ParentWithDefaultRouteCmp, name: 'FirstCmp'}));
new Route({path: '/first/...', component: DummyParentRedirectCmp, name: 'FirstCmp'}));
var instruction = registry.generate(['FirstCmp'], [RootHostCmp]);
expect(instruction.toLinkUrl()).toEqual('first');
expect(instruction.toRootUrl()).toEqual('first/second');
expect(stringifyInstruction(registry.generate(['FirstCmp'], RootHostCmp)))
.toEqual('first/second');
});
it('should generate URLs in a hierarchy of default routes', () => {
xit('should generate URLs in a hierarchy of redirects', () => {
registry.config(
RootHostCmp,
new Route({path: '/first/...', component: MultipleDefaultCmp, name: 'FirstCmp'}));
new Route({path: '/first/...', component: DummyMultipleRedirectCmp, name: 'FirstCmp'}));
var instruction = registry.generate(['FirstCmp'], [RootHostCmp]);
expect(instruction.toLinkUrl()).toEqual('first');
expect(instruction.toRootUrl()).toEqual('first/second/third');
expect(stringifyInstruction(registry.generate(['FirstCmp'], RootHostCmp)))
.toEqual('first/second/third');
});
it('should generate URLs with params', () => {
@ -79,13 +75,13 @@ export function main() {
new Route({path: '/first/:param/...', component: DummyParentParamCmp, name: 'FirstCmp'}));
var url = stringifyInstruction(registry.generate(
['FirstCmp', {param: 'one'}, 'SecondCmp', {param: 'two'}], [RootHostCmp]));
['FirstCmp', {param: 'one'}, 'SecondCmp', {param: 'two'}], RootHostCmp));
expect(url).toEqual('first/one/second/two');
});
it('should generate params as an empty StringMap when no params are given', () => {
registry.config(RootHostCmp, new Route({path: '/test', component: DummyCmpA, name: 'Test'}));
var instruction = registry.generate(['Test'], [RootHostCmp]);
var instruction = registry.generate(['Test'], RootHostCmp);
expect(instruction.component.params).toEqual({});
});
@ -95,20 +91,20 @@ export function main() {
RootHostCmp,
new AsyncRoute({path: '/first/...', loader: asyncParentLoader, name: 'FirstCmp'}));
var instruction = registry.generate(['FirstCmp', 'SecondCmp'], [RootHostCmp]);
expect(() => registry.generate(['FirstCmp', 'SecondCmp'], RootHostCmp))
.toThrowError('Could not find route named "SecondCmp".');
expect(stringifyInstruction(instruction)).toEqual('first');
registry.recognize('/first/second', [RootHostCmp])
registry.recognize('/first/second', RootHostCmp)
.then((_) => {
var instruction = registry.generate(['FirstCmp', 'SecondCmp'], [RootHostCmp]);
expect(stringifyInstruction(instruction)).toEqual('first/second');
expect(
stringifyInstruction(registry.generate(['FirstCmp', 'SecondCmp'], RootHostCmp)))
.toEqual('first/second');
async.done();
});
}));
it('should throw when generating a url and a parent has no config', () => {
expect(() => registry.generate(['FirstCmp', 'SecondCmp'], [RootHostCmp]))
expect(() => registry.generate(['FirstCmp', 'SecondCmp'], RootHostCmp))
.toThrowError('Component "RootHostCmp" has no route config.');
});
@ -117,7 +113,7 @@ export function main() {
new Route({path: '/primary', component: DummyCmpA, name: 'Primary'}));
registry.config(RootHostCmp, new AuxRoute({path: '/aux', component: DummyCmpB, name: 'Aux'}));
expect(stringifyInstruction(registry.generate(['Primary', ['Aux']], [RootHostCmp])))
expect(stringifyInstruction(registry.generate(['Primary', ['Aux']], RootHostCmp)))
.toEqual('primary(aux)');
});
@ -125,7 +121,7 @@ export function main() {
registry.config(RootHostCmp, new Route({path: '/:site', component: DummyCmpB}));
registry.config(RootHostCmp, new Route({path: '/home', component: DummyCmpA}));
registry.recognize('/home', [RootHostCmp])
registry.recognize('/home', RootHostCmp)
.then((instruction) => {
expect(instruction.component.componentType).toBe(DummyCmpA);
async.done();
@ -136,7 +132,7 @@ export function main() {
registry.config(RootHostCmp, new Route({path: '/:site', component: DummyCmpA}));
registry.config(RootHostCmp, new Route({path: '/*site', component: DummyCmpB}));
registry.recognize('/home', [RootHostCmp])
registry.recognize('/home', RootHostCmp)
.then((instruction) => {
expect(instruction.component.componentType).toBe(DummyCmpA);
async.done();
@ -147,7 +143,7 @@ export function main() {
registry.config(RootHostCmp, new Route({path: '/:first/*rest', component: DummyCmpA}));
registry.config(RootHostCmp, new Route({path: '/*all', component: DummyCmpB}));
registry.recognize('/some/path', [RootHostCmp])
registry.recognize('/some/path', RootHostCmp)
.then((instruction) => {
expect(instruction.component.componentType).toBe(DummyCmpA);
async.done();
@ -158,7 +154,7 @@ export function main() {
registry.config(RootHostCmp, new Route({path: '/first/:second', component: DummyCmpA}));
registry.config(RootHostCmp, new Route({path: '/:first/:second', component: DummyCmpB}));
registry.recognize('/first/second', [RootHostCmp])
registry.recognize('/first/second', RootHostCmp)
.then((instruction) => {
expect(instruction.component.componentType).toBe(DummyCmpA);
async.done();
@ -172,7 +168,7 @@ export function main() {
registry.config(RootHostCmp,
new Route({path: '/first/:second/third', component: DummyCmpA}));
registry.recognize('/first/second/third', [RootHostCmp])
registry.recognize('/first/second/third', RootHostCmp)
.then((instruction) => {
expect(instruction.component.componentType).toBe(DummyCmpB);
async.done();
@ -182,7 +178,7 @@ export function main() {
it('should match the full URL using child components', inject([AsyncTestCompleter], (async) => {
registry.config(RootHostCmp, new Route({path: '/first/...', component: DummyParentCmp}));
registry.recognize('/first/second', [RootHostCmp])
registry.recognize('/first/second', RootHostCmp)
.then((instruction) => {
expect(instruction.component.componentType).toBe(DummyParentCmp);
expect(instruction.child.component.componentType).toBe(DummyCmpB);
@ -194,14 +190,11 @@ export function main() {
inject([AsyncTestCompleter], (async) => {
registry.config(RootHostCmp, new Route({path: '/first/...', component: DummyAsyncCmp}));
registry.recognize('/first/second', [RootHostCmp])
registry.recognize('/first/second', RootHostCmp)
.then((instruction) => {
expect(instruction.component.componentType).toBe(DummyAsyncCmp);
instruction.child.resolveComponent().then((childComponentInstruction) => {
expect(childComponentInstruction.componentType).toBe(DummyCmpB);
async.done();
});
expect(instruction.child.component.componentType).toBe(DummyCmpB);
async.done();
});
}));
@ -210,14 +203,11 @@ export function main() {
registry.config(RootHostCmp,
new AsyncRoute({path: '/first/...', loader: asyncParentLoader}));
registry.recognize('/first/second', [RootHostCmp])
registry.recognize('/first/second', RootHostCmp)
.then((instruction) => {
expect(instruction.component.componentType).toBe(DummyParentCmp);
instruction.child.resolveComponent().then((childType) => {
expect(childType.componentType).toBe(DummyCmpB);
async.done();
});
expect(instruction.child.component.componentType).toBe(DummyCmpB);
async.done();
});
}));
@ -252,15 +242,15 @@ export function main() {
it('should throw when linkParams are not terminal', () => {
registry.config(RootHostCmp,
new Route({path: '/first/...', component: DummyParentCmp, name: 'First'}));
expect(() => { registry.generate(['First'], [RootHostCmp]); })
.toThrowError('Link "["First"]" does not resolve to a terminal instruction.');
expect(() => { registry.generate(['First'], RootHostCmp); })
.toThrowError('Link "["First"]" does not resolve to a terminal or async instruction.');
});
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])
registry.recognize('/first/second;filter=odd?comments=all', RootHostCmp)
.then((instruction) => {
expect(instruction.component.componentType).toBe(DummyParentCmp);
expect(instruction.component.params).toEqual({'comments': 'all'});
@ -286,18 +276,13 @@ export function main() {
sort: 'asc',
}
],
[RootHostCmp]));
RootHostCmp));
expect(url).toEqual('first/one/second/two;sort=asc?query=cats');
});
});
}
function stringifyInstruction(instruction): string {
return instruction.toRootUrl();
}
function asyncParentLoader() {
return PromiseWrapper.resolve(DummyParentCmp);
}
@ -315,22 +300,26 @@ class DummyAsyncCmp {
class DummyCmpA {}
class DummyCmpB {}
@RouteConfig(
[new Route({path: '/third', component: DummyCmpB, name: 'ThirdCmp', useAsDefault: true})])
class DefaultRouteCmp {
@RouteConfig([
new Redirect({path: '/', redirectTo: '/third'}),
new Route({path: '/third', component: DummyCmpB, name: 'ThirdCmp'})
])
class DummyRedirectCmp {
}
@RouteConfig([
new Route(
{path: '/second/...', component: DefaultRouteCmp, name: 'SecondCmp', useAsDefault: true})
new Redirect({path: '/', redirectTo: '/second'}),
new Route({path: '/second/...', component: DummyRedirectCmp, name: 'SecondCmp'})
])
class MultipleDefaultCmp {
class DummyMultipleRedirectCmp {
}
@RouteConfig(
[new Route({path: '/second', component: DummyCmpB, name: 'SecondCmp', useAsDefault: true})])
class ParentWithDefaultRouteCmp {
@RouteConfig([
new Redirect({path: '/', redirectTo: '/second'}),
new Route({path: '/second', component: DummyCmpB, name: 'SecondCmp'})
])
class DummyParentRedirectCmp {
}
@RouteConfig([new Route({path: '/second', component: DummyCmpB, name: 'SecondCmp'})])

View File

@ -8,7 +8,7 @@ import {
expect,
iit,
inject,
beforeEachProviders,
beforeEachBindings,
it,
xit,
TestComponentBuilder
@ -27,20 +27,24 @@ import {
RouterOutlet,
Route,
RouteParams,
Instruction,
ComponentInstruction
} from 'angular2/router';
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
import {ResolvedInstruction} from 'angular2/src/router/instruction';
import {ComponentInstruction_} from 'angular2/src/router/instruction';
import {PathRecognizer} from 'angular2/src/router/path_recognizer';
import {SyncRouteHandler} from 'angular2/src/router/sync_route_handler';
let dummyPathRecognizer = new PathRecognizer('', new SyncRouteHandler(null));
let dummyInstruction =
new ResolvedInstruction(new ComponentInstruction('detail', [], null, null, true, 0), null, {});
new Instruction(new ComponentInstruction_('detail', [], dummyPathRecognizer), null, {});
export function main() {
describe('router-link directive', function() {
var tcb: TestComponentBuilder;
beforeEachProviders(() => [
beforeEachBindings(() => [
provide(Location, {useValue: makeDummyLocation()}),
provide(Router, {useValue: makeDummyRouter()})
]);
@ -102,6 +106,11 @@ export function main() {
});
}
@Component({selector: 'my-comp'})
class MyComp {
name;
}
@Component({selector: 'user-cmp'})
@View({template: "hello {{user}}"})
class UserCmp {

View File

@ -18,6 +18,7 @@ import {ListWrapper} from 'angular2/src/facade/collection';
import {Router, RootRouter} from 'angular2/src/router/router';
import {SpyLocation} from 'angular2/src/mock/location_mock';
import {Location} from 'angular2/src/router/location';
import {stringifyInstruction} from 'angular2/src/router/instruction';
import {RouteRegistry} from 'angular2/src/router/route_registry';
import {RouteConfig, AsyncRoute, Route} from 'angular2/src/router/route_config_decorator';
@ -224,11 +225,6 @@ export function main() {
});
}
function stringifyInstruction(instruction): string {
return instruction.toRootUrl();
}
function loader(): Promise<Type> {
return PromiseWrapper.resolve(DummyComponent);
}