refactor(router): improve recognition and generation pipeline
This is a big change. @matsko also deserves much of the credit for the implementation. Previously, `ComponentInstruction`s held all the state for async components. Now, we introduce several subclasses for `Instruction` to describe each type of navigation. BREAKING CHANGE: Redirects now use the Link DSL syntax. Before: ``` @RouteConfig([ { path: '/foo', redirectTo: '/bar' }, { path: '/bar', component: BarCmp } ]) ``` After: ``` @RouteConfig([ { path: '/foo', redirectTo: ['Bar'] }, { path: '/bar', component: BarCmp, name: 'Bar' } ]) ``` BREAKING CHANGE: This also introduces `useAsDefault` in the RouteConfig, which makes cases like lazy-loading and encapsulating large routes with sub-routes easier. Previously, you could use `redirectTo` like this to expand a URL like `/tab` to `/tab/posts`: @RouteConfig([ { path: '/tab', redirectTo: '/tab/users' } { path: '/tab', component: TabsCmp, name: 'Tab' } ]) AppCmp { ... } Now the recommended way to handle this is case is to use `useAsDefault` like so: ``` @RouteConfig([ { path: '/tab', component: TabsCmp, name: 'Tab' } ]) AppCmp { ... } @RouteConfig([ { path: '/posts', component: PostsCmp, useAsDefault: true, name: 'Posts' }, { path: '/users', component: UsersCmp, name: 'Users' } ]) TabsCmp { ... } ``` In the above example, you can write just `['/Tab']` and the route `Users` is automatically selected as a child route. Closes #4728 Closes #4228 Closes #4170 Closes #4490 Closes #4694 Closes #5200 Closes #5475
This commit is contained in:
parent
a3253210b7
commit
6ddfff5cd5
|
@ -6,12 +6,13 @@ var ts = require('typescript');
|
|||
var files = [
|
||||
'lifecycle_annotations_impl.ts',
|
||||
'url_parser.ts',
|
||||
'path_recognizer.ts',
|
||||
'route_recognizer.ts',
|
||||
'route_config_impl.ts',
|
||||
'async_route_handler.ts',
|
||||
'sync_route_handler.ts',
|
||||
'route_recognizer.ts',
|
||||
'component_recognizer.ts',
|
||||
'instruction.ts',
|
||||
'path_recognizer.ts',
|
||||
'route_config_nomalizer.ts',
|
||||
'route_lifecycle_reflector.ts',
|
||||
'route_registry.ts',
|
||||
|
@ -39,7 +40,10 @@ function main() {
|
|||
* sourcemap, and exported variable identifier name for the content.
|
||||
*/
|
||||
var IMPORT_RE = new RegExp("import \\{?([\\w\\n_, ]+)\\}? from '(.+)';?", 'g');
|
||||
var INJECT_RE = new RegExp("@Inject\\(ROUTER_PRIMARY_COMPONENT\\)", 'g');
|
||||
var IMJECTABLE_RE = new RegExp("@Injectable\\(\\)", 'g');
|
||||
function transform(contents) {
|
||||
contents = contents.replace(INJECT_RE, '').replace(IMJECTABLE_RE, '');
|
||||
contents = contents.replace(IMPORT_RE, function (match, imports, includePath) {
|
||||
//TODO: remove special-case
|
||||
if (isFacadeModule(includePath) || includePath === './router_outlet') {
|
||||
|
|
|
@ -173,6 +173,10 @@ var StringMapWrapper = {
|
|||
|
||||
var List = Array;
|
||||
var ListWrapper = {
|
||||
clear: function (l) {
|
||||
l.length = 0;
|
||||
},
|
||||
|
||||
create: function () {
|
||||
return [];
|
||||
},
|
||||
|
|
|
@ -9,7 +9,11 @@ function routerFactory($q, $location, $$directiveIntrospector, $browser, $rootSc
|
|||
// the contents of `../lib/facades.es5`.
|
||||
//{{FACADES}}
|
||||
|
||||
var exports = {Injectable: function () {}};
|
||||
var exports = {
|
||||
Injectable: function () {},
|
||||
OpaqueToken: function () {},
|
||||
Inject: function () {}
|
||||
};
|
||||
var require = function () {return exports;};
|
||||
|
||||
// When this file is processed, the line below is replaced with
|
||||
|
@ -31,12 +35,19 @@ function routerFactory($q, $location, $$directiveIntrospector, $browser, $rootSc
|
|||
// property in a route config
|
||||
exports.assertComponentExists = function () {};
|
||||
|
||||
angular.stringifyInstruction = exports.stringifyInstruction;
|
||||
angular.stringifyInstruction = function (instruction) {
|
||||
return instruction.toRootUrl();
|
||||
};
|
||||
|
||||
var RouteRegistry = exports.RouteRegistry;
|
||||
var RootRouter = exports.RootRouter;
|
||||
|
||||
var registry = new RouteRegistry();
|
||||
|
||||
// Because Angular 1 has no notion of a root component, we use an object with unique identity
|
||||
// to represent this.
|
||||
var ROOT_COMPONENT_OBJECT = new Object();
|
||||
|
||||
var registry = new RouteRegistry(ROOT_COMPONENT_OBJECT);
|
||||
var location = new Location();
|
||||
|
||||
$$directiveIntrospector(function (name, factory) {
|
||||
|
@ -47,10 +58,6 @@ function routerFactory($q, $location, $$directiveIntrospector, $browser, $rootSc
|
|||
}
|
||||
});
|
||||
|
||||
// Because Angular 1 has no notion of a root component, we use an object with unique identity
|
||||
// to represent this.
|
||||
var ROOT_COMPONENT_OBJECT = new Object();
|
||||
|
||||
var router = new RootRouter(registry, location, ROOT_COMPONENT_OBJECT);
|
||||
$rootScope.$watch(function () { return $location.path(); }, function (path) {
|
||||
if (router.lastNavigationAttempt !== path) {
|
||||
|
|
|
@ -110,7 +110,7 @@
|
|||
routeMap[path] = routeCopy;
|
||||
|
||||
if (route.redirectTo) {
|
||||
routeDefinition.redirectTo = route.redirectTo;
|
||||
routeDefinition.redirectTo = [routeMap[route.redirectTo].name];
|
||||
} else {
|
||||
if (routeCopy.controller && !routeCopy.controllerAs) {
|
||||
console.warn('Route for "' + path + '" should use "controllerAs".');
|
||||
|
@ -123,7 +123,7 @@
|
|||
}
|
||||
|
||||
routeDefinition.component = directiveName;
|
||||
routeDefinition.as = upperCase(directiveName);
|
||||
routeDefinition.name = route.name || upperCase(directiveName);
|
||||
|
||||
var directiveController = routeCopy.controller;
|
||||
|
||||
|
|
|
@ -113,8 +113,7 @@ describe('navigation', function () {
|
|||
});
|
||||
|
||||
|
||||
// TODO: fix this
|
||||
xit('should work with recursive nested outlets', function () {
|
||||
it('should work with recursive nested outlets', function () {
|
||||
registerComponent('recurCmp', {
|
||||
template: '<div>recur { <div ng-outlet></div> }</div>',
|
||||
$routeConfig: [
|
||||
|
@ -152,8 +151,8 @@ describe('navigation', function () {
|
|||
compile('<div ng-outlet></div>');
|
||||
|
||||
$router.config([
|
||||
{ path: '/', redirectTo: '/user' },
|
||||
{ path: '/user', component: 'userCmp' }
|
||||
{ path: '/', redirectTo: ['/User'] },
|
||||
{ path: '/user', component: 'userCmp', name: 'User' }
|
||||
]);
|
||||
|
||||
$router.navigateByUrl('/');
|
||||
|
@ -167,16 +166,15 @@ describe('navigation', function () {
|
|||
registerComponent('childRouter', {
|
||||
template: '<div>inner { <div ng-outlet></div> }</div>',
|
||||
$routeConfig: [
|
||||
{ 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'}
|
||||
{ path: '/new-child', component: 'oneCmp', name: 'NewChild'},
|
||||
{ path: '/new-child-two', component: 'twoCmp', name: 'NewChildTwo'}
|
||||
]
|
||||
});
|
||||
|
||||
$router.config([
|
||||
{ path: '/old-parent', redirectTo: '/new-parent' },
|
||||
{ path: '/new-parent/...', component: 'childRouter' }
|
||||
{ path: '/old-parent/old-child', redirectTo: ['/NewParent', 'NewChild'] },
|
||||
{ path: '/old-parent/old-child-two', redirectTo: ['/NewParent', 'NewChildTwo'] },
|
||||
{ path: '/new-parent/...', component: 'childRouter', name: 'NewParent' }
|
||||
]);
|
||||
|
||||
compile('<div ng-outlet></div>');
|
||||
|
|
|
@ -139,11 +139,12 @@ 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();
|
||||
|
||||
|
|
|
@ -8,8 +8,8 @@ export {Router} from './src/router/router';
|
|||
export {RouterOutlet} from './src/router/router_outlet';
|
||||
export {RouterLink} from './src/router/router_link';
|
||||
export {RouteParams, RouteData} from './src/router/instruction';
|
||||
export {RouteRegistry} from './src/router/route_registry';
|
||||
export {PlatformLocation} from './src/router/platform_location';
|
||||
export {RouteRegistry, ROUTER_PRIMARY_COMPONENT} from './src/router/route_registry';
|
||||
export {LocationStrategy, APP_BASE_HREF} from './src/router/location_strategy';
|
||||
export {HashLocationStrategy} from './src/router/hash_location_strategy';
|
||||
export {PathLocationStrategy} from './src/router/path_location_strategy';
|
||||
|
@ -27,41 +27,12 @@ import {PathLocationStrategy} from './src/router/path_location_strategy';
|
|||
import {Router, RootRouter} from './src/router/router';
|
||||
import {RouterOutlet} from './src/router/router_outlet';
|
||||
import {RouterLink} from './src/router/router_link';
|
||||
import {RouteRegistry} from './src/router/route_registry';
|
||||
import {RouteRegistry, ROUTER_PRIMARY_COMPONENT} from './src/router/route_registry';
|
||||
import {Location} from './src/router/location';
|
||||
import {ApplicationRef, provide, OpaqueToken, Provider} from 'angular2/core';
|
||||
import {CONST_EXPR} from './src/facade/lang';
|
||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||
|
||||
|
||||
/**
|
||||
* Token used to bind the component with the top-level {@link RouteConfig}s for the
|
||||
* application.
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/iRUP8B5OUbxCWQ3AcIDm))
|
||||
*
|
||||
* ```
|
||||
* import {Component} from 'angular2/angular2';
|
||||
* import {
|
||||
* ROUTER_DIRECTIVES,
|
||||
* ROUTER_PROVIDERS,
|
||||
* RouteConfig
|
||||
* } from 'angular2/router';
|
||||
*
|
||||
* @Component({directives: [ROUTER_DIRECTIVES]})
|
||||
* @RouteConfig([
|
||||
* {...},
|
||||
* ])
|
||||
* class AppCmp {
|
||||
* // ...
|
||||
* }
|
||||
*
|
||||
* bootstrap(AppCmp, [ROUTER_PROVIDERS]);
|
||||
* ```
|
||||
*/
|
||||
export const ROUTER_PRIMARY_COMPONENT: OpaqueToken =
|
||||
CONST_EXPR(new OpaqueToken('RouterPrimaryComponent'));
|
||||
|
||||
/**
|
||||
* A list of directives. To use the router directives like {@link RouterOutlet} and
|
||||
* {@link RouterLink}, add this to your `directives` array in the {@link View} decorator of your
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
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, public data?: {[key: string]: any}) {}
|
||||
constructor(private _loader: Function, data: {[key: string]: any} = null) {
|
||||
this.data = isPresent(data) ? new RouteData(data) : BLANK_ROUTE_DATA;
|
||||
}
|
||||
|
||||
resolveComponentType(): Promise<any> {
|
||||
if (isPresent(this._resolvedComponent)) {
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -1,10 +1,7 @@
|
|||
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} from 'angular2/src/facade/async';
|
||||
import {Promise, PromiseWrapper} 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
|
||||
|
@ -77,7 +74,7 @@ export class RouteData {
|
|||
get(key: string): any { return normalizeBlank(StringMapWrapper.get(this.data, key)); }
|
||||
}
|
||||
|
||||
var BLANK_ROUTE_DATA = new RouteData();
|
||||
export var BLANK_ROUTE_DATA = new RouteData();
|
||||
|
||||
/**
|
||||
* `Instruction` is a tree of {@link ComponentInstruction}s with all the information needed
|
||||
|
@ -106,75 +103,185 @@ var BLANK_ROUTE_DATA = new RouteData();
|
|||
* bootstrap(AppCmp, ROUTER_PROVIDERS);
|
||||
* ```
|
||||
*/
|
||||
export class Instruction {
|
||||
constructor(public component: ComponentInstruction, public child: Instruction,
|
||||
public auxInstruction: {[key: string]: Instruction}) {}
|
||||
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('&')) : ''; }
|
||||
|
||||
/**
|
||||
* 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 Instruction(this.component, child, this.auxInstruction);
|
||||
}
|
||||
return new ResolvedInstruction(this.component, child, this.auxInstruction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a partially completed instruction during recognition that only has the
|
||||
* primary (non-aux) route instructions matched.
|
||||
*
|
||||
* `PrimaryInstruction` is an internal class used by `RouteRecognizer` while it's
|
||||
* figuring out where to navigate.
|
||||
* If the final URL for the instruction is ``
|
||||
*/
|
||||
export class PrimaryInstruction {
|
||||
constructor(public component: ComponentInstruction, public child: PrimaryInstruction,
|
||||
public auxUrls: Url[]) {}
|
||||
toUrlPath(): string {
|
||||
return this.urlPath + this._stringifyAux() +
|
||||
(isPresent(this.child) ? this.child._toNonRootUrl() : '');
|
||||
}
|
||||
|
||||
export function stringifyInstruction(instruction: Instruction): string {
|
||||
return stringifyInstructionPath(instruction) + stringifyInstructionQuery(instruction);
|
||||
// default instructions override these
|
||||
toLinkUrl(): string {
|
||||
return this.urlPath + this._stringifyAux() +
|
||||
(isPresent(this.child) ? this.child._toLinkUrl() : '');
|
||||
}
|
||||
|
||||
export function stringifyInstructionPath(instruction: Instruction): string {
|
||||
return instruction.component.urlPath + stringifyAux(instruction) +
|
||||
stringifyPrimaryPrefixed(instruction.child);
|
||||
// this is the non-root version (called recursively)
|
||||
/** @internal */
|
||||
_toLinkUrl(): string {
|
||||
return this._stringifyPathMatrixAuxPrefixed() +
|
||||
(isPresent(this.child) ? this.child._toLinkUrl() : '');
|
||||
}
|
||||
|
||||
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);
|
||||
/** @internal */
|
||||
_stringifyPathMatrixAuxPrefixed(): string {
|
||||
var primary = this._stringifyPathMatrixAux();
|
||||
if (primary.length > 0) {
|
||||
primary = '/' + primary;
|
||||
}
|
||||
return primary;
|
||||
}
|
||||
|
||||
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);
|
||||
/** @internal */
|
||||
_stringifyMatrixParams(): string {
|
||||
return this.urlParams.length > 0 ? (';' + this.component.urlParams.join(';')) : '';
|
||||
}
|
||||
|
||||
function stringifyAux(instruction: Instruction): string {
|
||||
/** @internal */
|
||||
_stringifyPathMatrixAux(): string {
|
||||
if (isBlank(this.component)) {
|
||||
return '';
|
||||
}
|
||||
return this.urlPath + this._stringifyMatrixParams() + this._stringifyAux();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_stringifyAux(): string {
|
||||
var routes = [];
|
||||
StringMapWrapper.forEach(instruction.auxInstruction, (auxInstruction, _) => {
|
||||
routes.push(stringifyPrimary(auxInstruction));
|
||||
StringMapWrapper.forEach(this.auxInstruction, (auxInstruction, _) => {
|
||||
routes.push(auxInstruction._stringifyPathMatrixAux());
|
||||
});
|
||||
if (routes.length > 0) {
|
||||
return '(' + routes.join('//') + ')';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
@ -185,67 +292,18 @@ function stringifyAux(instruction: Instruction): string {
|
|||
* 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/PathRecognizer} to
|
||||
* never construct one yourself with "new." Instead, rely on {@link Router/RouteRecognizer} to
|
||||
* construct `ComponentInstruction`s.
|
||||
*
|
||||
* You should not modify this object. It should be treated as immutable.
|
||||
*/
|
||||
export abstract class ComponentInstruction {
|
||||
export class ComponentInstruction {
|
||||
reuse: boolean = false;
|
||||
public urlPath: string;
|
||||
public urlParams: string[];
|
||||
public params: {[key: string]: any};
|
||||
public routeData: RouteData;
|
||||
|
||||
/**
|
||||
* 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;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
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; }
|
||||
}
|
||||
|
|
|
@ -7,12 +7,9 @@ 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} = {};
|
||||
|
@ -33,7 +30,7 @@ class TouchMap {
|
|||
}
|
||||
|
||||
getUnused(): {[key: string]: any} {
|
||||
var unused: {[key: string]: any} = StringMapWrapper.create();
|
||||
var unused: {[key: string]: any} = {};
|
||||
var keys = StringMapWrapper.keys(this.keys);
|
||||
keys.forEach(key => unused[key] = StringMapWrapper.get(this.map, key));
|
||||
return unused;
|
||||
|
@ -126,7 +123,6 @@ 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());
|
||||
|
@ -175,23 +171,17 @@ function assertPath(path: string) {
|
|||
}
|
||||
}
|
||||
|
||||
export class PathMatch {
|
||||
constructor(public instruction: ComponentInstruction, public remaining: Url,
|
||||
public remainingAux: Url[]) {}
|
||||
}
|
||||
|
||||
// represents something like '/foo/:bar'
|
||||
/**
|
||||
* Parses a URL string using a given matcher DSL, and generates URLs from param maps
|
||||
*/
|
||||
export class PathRecognizer {
|
||||
private _segments: Segment[];
|
||||
specificity: number;
|
||||
terminal: boolean = true;
|
||||
hash: string;
|
||||
private _cache: Map<string, ComponentInstruction> = new Map<string, ComponentInstruction>();
|
||||
|
||||
|
||||
// TODO: cache component instruction instances by params and by ParsedUrl instance
|
||||
|
||||
constructor(public path: string, public handler: RouteHandler) {
|
||||
constructor(public path: string) {
|
||||
assertPath(path);
|
||||
var parsed = parsePathString(path);
|
||||
|
||||
|
@ -203,8 +193,7 @@ export class PathRecognizer {
|
|||
this.terminal = !(lastSegment instanceof ContinuationSegment);
|
||||
}
|
||||
|
||||
|
||||
recognize(beginningSegment: Url): PathMatch {
|
||||
recognize(beginningSegment: Url): {[key: string]: any} {
|
||||
var nextSegment = beginningSegment;
|
||||
var currentSegment: Url;
|
||||
var positionalParams = {};
|
||||
|
@ -247,7 +236,6 @@ export class PathRecognizer {
|
|||
var urlPath = captured.join('/');
|
||||
|
||||
var auxiliary;
|
||||
var instruction: ComponentInstruction;
|
||||
var urlParams;
|
||||
var allParams;
|
||||
if (isPresent(currentSegment)) {
|
||||
|
@ -267,12 +255,11 @@ export class PathRecognizer {
|
|||
auxiliary = [];
|
||||
urlParams = [];
|
||||
}
|
||||
instruction = this._getInstruction(urlPath, urlParams, this, allParams);
|
||||
return new PathMatch(instruction, nextSegment, auxiliary);
|
||||
return {urlPath, urlParams, allParams, auxiliary, nextSegment};
|
||||
}
|
||||
|
||||
|
||||
generate(params: {[key: string]: any}): ComponentInstruction {
|
||||
generate(params: {[key: string]: any}): {[key: string]: any} {
|
||||
var paramTokens = new TouchMap(params);
|
||||
|
||||
var path = [];
|
||||
|
@ -288,18 +275,6 @@ export class PathRecognizer {
|
|||
var nonPositionalParams = paramTokens.getUnused();
|
||||
var urlParams = serializeParams(nonPositionalParams);
|
||||
|
||||
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;
|
||||
return {urlPath, urlParams};
|
||||
}
|
||||
}
|
||||
|
|
|
@ -21,6 +21,8 @@ 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
|
||||
* ```
|
||||
|
@ -38,16 +40,20 @@ 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: string = null;
|
||||
constructor({path, component, name,
|
||||
data}: {path: string, component: Type, name?: string, data?: {[key: string]: any}}) {
|
||||
redirectTo: any[] = null;
|
||||
constructor({path, component, name, data, useAsDefault}: {
|
||||
path: string,
|
||||
component: Type, name?: string, data?: {[key: string]: any}, useAsDefault?: boolean
|
||||
}) {
|
||||
this.path = path;
|
||||
this.component = component;
|
||||
this.name = name;
|
||||
this.data = data;
|
||||
this.useAsDefault = useAsDefault;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -80,7 +86,8 @@ 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: string = null;
|
||||
redirectTo: any[] = null;
|
||||
useAsDefault: boolean = false;
|
||||
constructor({path, component, name}: {path: string, component: Type, name?: string}) {
|
||||
this.path = path;
|
||||
this.component = component;
|
||||
|
@ -98,6 +105,8 @@ 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
|
||||
* ```
|
||||
|
@ -115,31 +124,37 @@ export class AsyncRoute implements RouteDefinition {
|
|||
path: string;
|
||||
loader: Function;
|
||||
name: string;
|
||||
useAsDefault: boolean;
|
||||
aux: string = null;
|
||||
constructor({path, loader, name, data}:
|
||||
{path: string, loader: Function, name?: string, data?: {[key: string]: any}}) {
|
||||
constructor({path, loader, name, data, useAsDefault}: {
|
||||
path: string,
|
||||
loader: Function, name?: string, data?: {[key: string]: any}, useAsDefault?: boolean
|
||||
}) {
|
||||
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 an asynchronously loaded
|
||||
* component.
|
||||
* `Redirect` is a type of {@link RouteDefinition} used to route a path to a canonical route.
|
||||
*
|
||||
* It has the following properties:
|
||||
* - `path` is a string that uses the route matcher DSL.
|
||||
* - `redirectTo` is a string representing the new URL to be matched against.
|
||||
* - `redirectTo` is an array representing the link DSL.
|
||||
*
|
||||
* Note that redirects **do not** affect how links are generated. For that, see the `useAsDefault`
|
||||
* option.
|
||||
*
|
||||
* ### Example
|
||||
* ```
|
||||
* import {RouteConfig} from 'angular2/router';
|
||||
*
|
||||
* @RouteConfig([
|
||||
* {path: '/', redirectTo: '/home'},
|
||||
* {path: '/home', component: HomeCmp}
|
||||
* {path: '/', redirectTo: ['/Home'] },
|
||||
* {path: '/home', component: HomeCmp, name: 'Home'}
|
||||
* ])
|
||||
* class MyApp {}
|
||||
* ```
|
||||
|
@ -147,13 +162,14 @@ export class AsyncRoute implements RouteDefinition {
|
|||
@CONST()
|
||||
export class Redirect implements RouteDefinition {
|
||||
path: string;
|
||||
redirectTo: string;
|
||||
redirectTo: any[];
|
||||
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;
|
||||
constructor({path, redirectTo}: {path: string, redirectTo: string}) {
|
||||
useAsDefault: boolean = false;
|
||||
constructor({path, redirectTo}: {path: string, redirectTo: any[]}) {
|
||||
this.path = path;
|
||||
this.redirectTo = redirectTo;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,22 @@
|
|||
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) {
|
||||
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);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
|
|
|
@ -2,14 +2,29 @@ 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... returns a corresponding Route, AsyncRoute, or Redirect
|
||||
* 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`.
|
||||
*/
|
||||
export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
||||
if (config instanceof Route || config instanceof Redirect || config instanceof AsyncRoute ||
|
||||
config instanceof AuxRoute) {
|
||||
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) {
|
||||
return <RouteDefinition>config;
|
||||
}
|
||||
|
||||
|
@ -24,7 +39,13 @@ export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
|||
config.name = config.as;
|
||||
}
|
||||
if (config.loader) {
|
||||
return new AsyncRoute({path: config.path, loader: config.loader, name: config.name});
|
||||
var wrappedLoader = wrapLoaderToReconfigureRegistry(config.loader, registry);
|
||||
return new AsyncRoute({
|
||||
path: config.path,
|
||||
loader: wrappedLoader,
|
||||
name: config.name,
|
||||
useAsDefault: config.useAsDefault
|
||||
});
|
||||
}
|
||||
if (config.aux) {
|
||||
return new AuxRoute({path: config.aux, component:<Type>config.component, name: config.name});
|
||||
|
@ -36,11 +57,17 @@ export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
|||
return new Route({
|
||||
path: config.path,
|
||||
component:<Type>componentDefinitionObject.constructor,
|
||||
name: config.name
|
||||
name: config.name,
|
||||
data: config.data,
|
||||
useAsDefault: config.useAsDefault
|
||||
});
|
||||
} else if (componentDefinitionObject.type == 'loader') {
|
||||
return new AsyncRoute(
|
||||
{path: config.path, loader: componentDefinitionObject.loader, name: config.name});
|
||||
return new AsyncRoute({
|
||||
path: config.path,
|
||||
loader: componentDefinitionObject.loader,
|
||||
name: config.name,
|
||||
useAsDefault: config.useAsDefault
|
||||
});
|
||||
} else {
|
||||
throw new BaseException(
|
||||
`Invalid component type "${componentDefinitionObject.type}". Valid types are "constructor" and "loader".`);
|
||||
|
@ -50,6 +77,8 @@ export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
|||
path: string;
|
||||
component: Type;
|
||||
name?: string;
|
||||
data?: {[key: string]: any};
|
||||
useAsDefault?: boolean;
|
||||
}>config);
|
||||
}
|
||||
|
||||
|
@ -60,6 +89,16 @@ export function normalizeRouteConfig(config: RouteDefinition): 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.`);
|
||||
|
|
|
@ -3,5 +3,6 @@ library angular2.src.router.route_definition;
|
|||
abstract class RouteDefinition {
|
||||
final String path;
|
||||
final String name;
|
||||
const RouteDefinition({this.path, this.name});
|
||||
final bool useAsDefault;
|
||||
const RouteDefinition({this.path, this.name, this.useAsDefault : false});
|
||||
}
|
||||
|
|
|
@ -16,10 +16,11 @@ export interface RouteDefinition {
|
|||
aux?: string;
|
||||
component?: Type | ComponentDefinition;
|
||||
loader?: Function;
|
||||
redirectTo?: string;
|
||||
redirectTo?: any[];
|
||||
as?: string;
|
||||
name?: string;
|
||||
data?: any;
|
||||
useAsDefault?: boolean;
|
||||
}
|
||||
|
||||
export interface ComponentDefinition {
|
||||
|
|
|
@ -1,8 +1,9 @@
|
|||
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?: {[key: string]: any};
|
||||
data: RouteData;
|
||||
}
|
||||
|
|
|
@ -1,184 +1,119 @@
|
|||
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 {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 {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 {RouteHandler} from './route_handler';
|
||||
import {Url} from './url_parser';
|
||||
import {ComponentInstruction} from './instruction';
|
||||
import {PathRecognizer} from './path_recognizer';
|
||||
|
||||
|
||||
/**
|
||||
* `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 abstract class RouteMatch {}
|
||||
|
||||
// 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 interface AbstractRecognizer {
|
||||
hash: string;
|
||||
path: string;
|
||||
recognize(beginningSegment: Url): Promise<RouteMatch>;
|
||||
generate(params: {[key: string]: any}): ComponentInstruction;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 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;
|
||||
export class PathMatch extends RouteMatch {
|
||||
constructor(public instruction: ComponentInstruction, public remaining: Url,
|
||||
public remainingAux: Url[]) {
|
||||
super();
|
||||
}
|
||||
}
|
||||
|
||||
return urlParse;
|
||||
|
||||
export class RedirectMatch extends RouteMatch {
|
||||
constructor(public redirectTo: any[], public specificity) { super(); }
|
||||
}
|
||||
|
||||
recognizeAuxiliary(urlParse: Url): PathMatch {
|
||||
var pathRecognizer = this.auxRoutes.get(urlParse.path);
|
||||
if (isBlank(pathRecognizer)) {
|
||||
return null;
|
||||
}
|
||||
return pathRecognizer.recognize(urlParse);
|
||||
}
|
||||
export class RedirectRecognizer implements AbstractRecognizer {
|
||||
private _pathRecognizer: PathRecognizer;
|
||||
public hash: string;
|
||||
|
||||
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[] = [];
|
||||
|
||||
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('/');
|
||||
constructor(public path: string, public redirectTo: any[]) {
|
||||
this._pathRecognizer = new PathRecognizer(path);
|
||||
this.hash = this._pathRecognizer.hash;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `null` or a `ParsedUrl` representing the new path to match
|
||||
*/
|
||||
redirect(urlParse: Url): Url {
|
||||
for (var i = 0; i < this.segments.length; i += 1) {
|
||||
if (isBlank(urlParse)) {
|
||||
return null;
|
||||
recognize(beginningSegment: Url): Promise<RouteMatch> {
|
||||
var match = null;
|
||||
if (isPresent(this._pathRecognizer.recognize(beginningSegment))) {
|
||||
match = new RedirectMatch(this.redirectTo, this._pathRecognizer.specificity);
|
||||
}
|
||||
let segment = this.segments[i];
|
||||
if (segment != urlParse.path) {
|
||||
return null;
|
||||
}
|
||||
urlParse = urlParse.child;
|
||||
return PromiseWrapper.resolve(match);
|
||||
}
|
||||
|
||||
for (var i = this.toSegments.length - 1; i >= 0; i -= 1) {
|
||||
let segment = this.toSegments[i];
|
||||
urlParse = new Url(segment, urlParse);
|
||||
}
|
||||
return urlParse;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,3 @@
|
|||
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 {
|
||||
|
@ -10,12 +7,14 @@ import {
|
|||
isType,
|
||||
isString,
|
||||
isStringMap,
|
||||
isFunction,
|
||||
StringWrapper,
|
||||
Type,
|
||||
getTypeNameForDebugging
|
||||
getTypeNameForDebugging,
|
||||
CONST_EXPR
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
|
||||
import {reflector} from 'angular2/src/core/reflection/reflection';
|
||||
import {Injectable, Inject, OpaqueToken} from 'angular2/core';
|
||||
|
||||
import {
|
||||
RouteConfig,
|
||||
AsyncRoute,
|
||||
|
@ -24,13 +23,52 @@ import {
|
|||
Redirect,
|
||||
RouteDefinition
|
||||
} from './route_config_impl';
|
||||
import {reflector} from 'angular2/src/core/reflection/reflection';
|
||||
import {Injectable} from 'angular2/core';
|
||||
import {PathMatch, RedirectMatch, RouteMatch} from './route_recognizer';
|
||||
import {ComponentRecognizer} from './component_recognizer';
|
||||
import {
|
||||
Instruction,
|
||||
ResolvedInstruction,
|
||||
RedirectInstruction,
|
||||
UnresolvedInstruction,
|
||||
DefaultInstruction
|
||||
} from './instruction';
|
||||
|
||||
import {normalizeRouteConfig, assertComponentExists} from './route_config_nomalizer';
|
||||
import {parser, Url, pathSegmentsToUrl} from './url_parser';
|
||||
|
||||
var _resolveToNull = PromiseWrapper.resolve(null);
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Token used to bind the component with the top-level {@link RouteConfig}s for the
|
||||
* application.
|
||||
*
|
||||
* ### Example ([live demo](http://plnkr.co/edit/iRUP8B5OUbxCWQ3AcIDm))
|
||||
*
|
||||
* ```
|
||||
* import {Component} from 'angular2/angular2';
|
||||
* import {
|
||||
* ROUTER_DIRECTIVES,
|
||||
* ROUTER_PROVIDERS,
|
||||
* RouteConfig
|
||||
* } from 'angular2/router';
|
||||
*
|
||||
* @Component({directives: [ROUTER_DIRECTIVES]})
|
||||
* @RouteConfig([
|
||||
* {...},
|
||||
* ])
|
||||
* class AppCmp {
|
||||
* // ...
|
||||
* }
|
||||
*
|
||||
* bootstrap(AppCmp, [ROUTER_PROVIDERS]);
|
||||
* ```
|
||||
*/
|
||||
export const ROUTER_PRIMARY_COMPONENT: OpaqueToken =
|
||||
CONST_EXPR(new OpaqueToken('RouterPrimaryComponent'));
|
||||
|
||||
|
||||
/**
|
||||
* The RouteRegistry holds route configurations for each component in an Angular app.
|
||||
* It is responsible for creating Instructions from URLs, and generating URLs based on route and
|
||||
|
@ -38,13 +76,15 @@ var _resolveToNull = PromiseWrapper.resolve(null);
|
|||
*/
|
||||
@Injectable()
|
||||
export class RouteRegistry {
|
||||
private _rules = new Map<any, RouteRecognizer>();
|
||||
private _rules = new Map<any, ComponentRecognizer>();
|
||||
|
||||
constructor(@Inject(ROUTER_PRIMARY_COMPONENT) private _rootComponent: Type) {}
|
||||
|
||||
/**
|
||||
* Given a component and a configuration object, add the route to this registry
|
||||
*/
|
||||
config(parentComponent: any, config: RouteDefinition): void {
|
||||
config = normalizeRouteConfig(config);
|
||||
config = normalizeRouteConfig(config, this);
|
||||
|
||||
// this is here because Dart type guard reasons
|
||||
if (config instanceof Route) {
|
||||
|
@ -53,10 +93,10 @@ export class RouteRegistry {
|
|||
assertComponentExists(config.component, config.path);
|
||||
}
|
||||
|
||||
var recognizer: RouteRecognizer = this._rules.get(parentComponent);
|
||||
var recognizer: ComponentRecognizer = this._rules.get(parentComponent);
|
||||
|
||||
if (isBlank(recognizer)) {
|
||||
recognizer = new RouteRecognizer();
|
||||
recognizer = new ComponentRecognizer();
|
||||
this._rules.set(parentComponent, recognizer);
|
||||
}
|
||||
|
||||
|
@ -102,102 +142,188 @@ 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, parentComponent: any): Promise<Instruction> {
|
||||
recognize(url: string, ancestorInstructions: Instruction[]): Promise<Instruction> {
|
||||
var parsedUrl = parser.parse(url);
|
||||
return this._recognize(parsedUrl, parentComponent);
|
||||
return this._recognize(parsedUrl, ancestorInstructions);
|
||||
}
|
||||
|
||||
private _recognize(parsedUrl: Url, parentComponent): Promise<Instruction> {
|
||||
return this._recognizePrimaryRoute(parsedUrl, parentComponent)
|
||||
.then((instruction: PrimaryInstruction) =>
|
||||
this._completeAuxiliaryRouteMatches(instruction, parentComponent));
|
||||
}
|
||||
|
||||
private _recognizePrimaryRoute(parsedUrl: Url, parentComponent): Promise<PrimaryInstruction> {
|
||||
/**
|
||||
* Recognizes all parent-child routes, but creates unresolved auxiliary routes
|
||||
*/
|
||||
|
||||
private _recognize(parsedUrl: Url, ancestorInstructions: Instruction[],
|
||||
_aux = false): Promise<Instruction> {
|
||||
var parentComponent =
|
||||
ancestorInstructions.length > 0 ?
|
||||
ancestorInstructions[ancestorInstructions.length - 1].component.componentType :
|
||||
this._rootComponent;
|
||||
|
||||
var componentRecognizer = this._rules.get(parentComponent);
|
||||
if (isBlank(componentRecognizer)) {
|
||||
return _resolveToNull;
|
||||
}
|
||||
|
||||
// Matches some beginning part of the given URL
|
||||
var possibleMatches = componentRecognizer.recognize(parsedUrl);
|
||||
var possibleMatches: Promise<RouteMatch>[] =
|
||||
_aux ? componentRecognizer.recognizeAuxiliary(parsedUrl) :
|
||||
componentRecognizer.recognize(parsedUrl);
|
||||
|
||||
var matchPromises =
|
||||
possibleMatches.map(candidate => this._completePrimaryRouteMatch(candidate));
|
||||
var matchPromises: Promise<Instruction>[] = possibleMatches.map(
|
||||
(candidate: Promise<RouteMatch>) => candidate.then((candidate: RouteMatch) => {
|
||||
|
||||
if (candidate instanceof PathMatch) {
|
||||
var auxParentInstructions =
|
||||
ancestorInstructions.length > 0 ?
|
||||
[ancestorInstructions[ancestorInstructions.length - 1]] :
|
||||
[];
|
||||
var auxInstructions =
|
||||
this._auxRoutesToUnresolved(candidate.remainingAux, auxParentInstructions);
|
||||
var instruction = new ResolvedInstruction(candidate.instruction, null, auxInstructions);
|
||||
|
||||
if (candidate.instruction.terminal) {
|
||||
return instruction;
|
||||
}
|
||||
|
||||
var newAncestorComponents = ancestorInstructions.concat([instruction]);
|
||||
|
||||
return this._recognize(candidate.remaining, newAncestorComponents)
|
||||
.then((childInstruction) => {
|
||||
if (isBlank(childInstruction)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// redirect instructions are already absolute
|
||||
if (childInstruction instanceof RedirectInstruction) {
|
||||
return childInstruction;
|
||||
}
|
||||
instruction.child = childInstruction;
|
||||
return instruction;
|
||||
});
|
||||
}
|
||||
|
||||
if (candidate instanceof RedirectMatch) {
|
||||
var instruction = this.generate(candidate.redirectTo, ancestorInstructions);
|
||||
return new RedirectInstruction(instruction.component, instruction.child,
|
||||
instruction.auxInstruction);
|
||||
}
|
||||
}));
|
||||
|
||||
if ((isBlank(parsedUrl) || parsedUrl.path == '') && possibleMatches.length == 0) {
|
||||
return PromiseWrapper.resolve(this.generateDefault(parentComponent));
|
||||
}
|
||||
|
||||
return PromiseWrapper.all(matchPromises).then(mostSpecific);
|
||||
}
|
||||
|
||||
private _completePrimaryRouteMatch(partialMatch: PathMatch): Promise<PrimaryInstruction> {
|
||||
var instruction = partialMatch.instruction;
|
||||
return instruction.resolveComponentType().then((componentType) => {
|
||||
this.configFromComponent(componentType);
|
||||
private _auxRoutesToUnresolved(auxRoutes: Url[],
|
||||
parentInstructions: Instruction[]): {[key: string]: Instruction} {
|
||||
var unresolvedAuxInstructions: {[key: string]: Instruction} = {};
|
||||
|
||||
if (instruction.terminal) {
|
||||
return new PrimaryInstruction(instruction, null, partialMatch.remainingAux);
|
||||
auxRoutes.forEach((auxUrl: Url) => {
|
||||
unresolvedAuxInstructions[auxUrl.path] = new UnresolvedInstruction(
|
||||
() => { return this._recognize(auxUrl, parentInstructions, true); });
|
||||
});
|
||||
|
||||
return unresolvedAuxInstructions;
|
||||
}
|
||||
|
||||
return this._recognizePrimaryRoute(partialMatch.remaining, componentType)
|
||||
.then((childInstruction) => {
|
||||
if (isBlank(childInstruction)) {
|
||||
return null;
|
||||
} else {
|
||||
return new PrimaryInstruction(instruction, childInstruction,
|
||||
partialMatch.remainingAux);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
private _completeAuxiliaryRouteMatches(instruction: PrimaryInstruction,
|
||||
parentComponent: any): Promise<Instruction> {
|
||||
if (isBlank(instruction)) {
|
||||
return _resolveToNull;
|
||||
}
|
||||
|
||||
var componentRecognizer = this._rules.get(parentComponent);
|
||||
var auxInstructions: {[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[], parentComponent: any, _aux = false): Instruction {
|
||||
generate(linkParams: any[], ancestorInstructions: Instruction[], _aux = false): Instruction {
|
||||
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 == '') {
|
||||
ancestorInstructions = [];
|
||||
} else if (first == '..') {
|
||||
// we already captured the first instance of "..", so we need to pop off an ancestor
|
||||
ancestorInstructions.pop();
|
||||
while (ListWrapper.first(rest) == '..') {
|
||||
rest = ListWrapper.slice(rest, 1);
|
||||
ancestorInstructions.pop();
|
||||
if (ancestorInstructions.length <= 0) {
|
||||
throw new BaseException(
|
||||
`Link "${ListWrapper.toJSON(linkParams)}" has too many "../" segments.`);
|
||||
}
|
||||
}
|
||||
} else if (first != '.') {
|
||||
let parentComponent = this._rootComponent;
|
||||
let grandparentComponent = null;
|
||||
if (ancestorInstructions.length > 1) {
|
||||
parentComponent =
|
||||
ancestorInstructions[ancestorInstructions.length - 1].component.componentType;
|
||||
grandparentComponent =
|
||||
ancestorInstructions[ancestorInstructions.length - 2].component.componentType;
|
||||
} else if (ancestorInstructions.length == 1) {
|
||||
parentComponent = ancestorInstructions[0].component.componentType;
|
||||
grandparentComponent = this._rootComponent;
|
||||
}
|
||||
|
||||
// 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) {
|
||||
ancestorInstructions.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);
|
||||
}
|
||||
|
||||
var generatedInstruction = this._generate(rest, ancestorInstructions, _aux);
|
||||
|
||||
for (var i = ancestorInstructions.length - 1; i >= 0; i--) {
|
||||
let ancestorInstruction = ancestorInstructions[i];
|
||||
generatedInstruction = ancestorInstruction.replaceChild(generatedInstruction);
|
||||
}
|
||||
|
||||
return generatedInstruction;
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
* Internal helper that does not make any assertions about the beginning of the link DSL
|
||||
*/
|
||||
private _generate(linkParams: any[], ancestorInstructions: Instruction[],
|
||||
_aux = false): Instruction {
|
||||
let parentComponent =
|
||||
ancestorInstructions.length > 0 ?
|
||||
ancestorInstructions[ancestorInstructions.length - 1].component.componentType :
|
||||
this._rootComponent;
|
||||
|
||||
|
||||
if (linkParams.length == 0) {
|
||||
return this.generateDefault(parentComponent);
|
||||
}
|
||||
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 == '..') {
|
||||
|
@ -216,7 +342,13 @@ export class RouteRegistry {
|
|||
let auxInstructions: {[key: string]: Instruction} = {};
|
||||
var nextSegment;
|
||||
while (linkIndex + 1 < linkParams.length && isArray(nextSegment = linkParams[linkIndex + 1])) {
|
||||
auxInstructions[nextSegment[0]] = this.generate(nextSegment, parentComponent, true);
|
||||
let auxParentInstruction = ancestorInstructions.length > 0 ?
|
||||
[ancestorInstructions[ancestorInstructions.length - 1]] :
|
||||
[];
|
||||
let auxInstruction = this._generate(nextSegment, auxParentInstruction, true);
|
||||
|
||||
// TODO: this will not work for aux routes with parameters or multiple segments
|
||||
auxInstructions[auxInstruction.component.urlPath] = auxInstruction;
|
||||
linkIndex += 1;
|
||||
}
|
||||
|
||||
|
@ -226,74 +358,107 @@ export class RouteRegistry {
|
|||
`Component "${getTypeNameForDebugging(parentComponent)}" has no route config.`);
|
||||
}
|
||||
|
||||
var componentInstruction = _aux ? componentRecognizer.generateAuxiliary(routeName, params) :
|
||||
componentRecognizer.generate(routeName, params);
|
||||
var routeRecognizer =
|
||||
(_aux ? componentRecognizer.auxNames : componentRecognizer.names).get(routeName);
|
||||
|
||||
if (isBlank(componentInstruction)) {
|
||||
if (!isPresent(routeRecognizer)) {
|
||||
throw new BaseException(
|
||||
`Component "${getTypeNameForDebugging(parentComponent)}" has no route named "${routeName}".`);
|
||||
}
|
||||
|
||||
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.`);
|
||||
if (!isPresent(routeRecognizer.handler.componentType)) {
|
||||
var compInstruction = routeRecognizer.generateComponentPathValues(params);
|
||||
return new UnresolvedInstruction(() => {
|
||||
return routeRecognizer.handler.resolveComponentType().then(
|
||||
(_) => { return this._generate(linkParams, ancestorInstructions, _aux); });
|
||||
}, compInstruction['urlPath'], compInstruction['urlParams']);
|
||||
}
|
||||
|
||||
return new Instruction(componentInstruction, childInstruction, auxInstructions);
|
||||
var componentInstruction = _aux ? componentRecognizer.generateAuxiliary(routeName, params) :
|
||||
componentRecognizer.generate(routeName, params);
|
||||
|
||||
|
||||
|
||||
var remaining = linkParams.slice(linkIndex + 1);
|
||||
|
||||
var instruction = new ResolvedInstruction(componentInstruction, null, auxInstructions);
|
||||
|
||||
// the component is sync
|
||||
if (isPresent(componentInstruction.componentType)) {
|
||||
let childInstruction: Instruction = null;
|
||||
if (linkIndex + 1 < linkParams.length) {
|
||||
let childAncestorComponents = ancestorInstructions.concat([instruction]);
|
||||
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.`);
|
||||
}
|
||||
}
|
||||
instruction.child = childInstruction;
|
||||
}
|
||||
|
||||
return instruction;
|
||||
}
|
||||
|
||||
public hasRoute(name: string, parentComponent: any): boolean {
|
||||
var componentRecognizer: RouteRecognizer = this._rules.get(parentComponent);
|
||||
var componentRecognizer: ComponentRecognizer = this._rules.get(parentComponent);
|
||||
if (isBlank(componentRecognizer)) {
|
||||
return false;
|
||||
}
|
||||
return componentRecognizer.hasRoute(name);
|
||||
}
|
||||
|
||||
// if the child includes a redirect like : "/" -> "/something",
|
||||
// we want to honor that redirection when creating the link
|
||||
private _generateRedirects(componentCursor: Type): Instruction {
|
||||
public generateDefault(componentCursor: Type): Instruction {
|
||||
if (isBlank(componentCursor)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var componentRecognizer = this._rules.get(componentCursor);
|
||||
if (isBlank(componentRecognizer)) {
|
||||
if (isBlank(componentRecognizer) || isBlank(componentRecognizer.defaultRoute)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let i = 0; i < componentRecognizer.redirects.length; i += 1) {
|
||||
let redirect = componentRecognizer.redirects[i];
|
||||
|
||||
// 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, {});
|
||||
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);
|
||||
}
|
||||
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: PrimaryInstruction[]): PrimaryInstruction {
|
||||
return ListWrapper.maximum(
|
||||
instructions, (instruction: PrimaryInstruction) => instruction.component.specificity);
|
||||
function mostSpecific(instructions: Instruction[]): Instruction {
|
||||
return ListWrapper.maximum(instructions, (instruction: Instruction) => instruction.specificity);
|
||||
}
|
||||
|
||||
function assertTerminalComponent(component, path) {
|
||||
|
|
|
@ -2,13 +2,12 @@ import {Promise, PromiseWrapper, EventEmitter, ObservableWrapper} from 'angular2
|
|||
import {Map, StringMapWrapper, MapWrapper, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {isBlank, isString, isPresent, Type, isArray} from 'angular2/src/facade/lang';
|
||||
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
|
||||
import {RouteRegistry} from './route_registry';
|
||||
import {Inject, Injectable} from 'angular2/core';
|
||||
|
||||
import {RouteRegistry, ROUTER_PRIMARY_COMPONENT} from './route_registry';
|
||||
import {
|
||||
ComponentInstruction,
|
||||
Instruction,
|
||||
stringifyInstruction,
|
||||
stringifyInstructionPath,
|
||||
stringifyInstructionQuery
|
||||
} from './instruction';
|
||||
import {RouterOutlet} from './router_outlet';
|
||||
import {Location} from './location';
|
||||
|
@ -126,6 +125,7 @@ export class Router {
|
|||
this._currentInstruction.component == instruction.component;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Dynamically update the routing configuration and trigger a navigation.
|
||||
*
|
||||
|
@ -144,6 +144,7 @@ export class Router {
|
|||
return this.renavigate();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Navigate based on the provided Route Link DSL. It's preferred to navigate with this method
|
||||
* over `navigateByUrl`.
|
||||
|
@ -212,7 +213,7 @@ export class Router {
|
|||
if (result) {
|
||||
return this.commit(instruction, _skipLocationChange)
|
||||
.then((_) => {
|
||||
this._emitNavigationFinish(stringifyInstruction(instruction));
|
||||
this._emitNavigationFinish(instruction.toRootUrl());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
@ -220,25 +221,22 @@ 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((_) => {
|
||||
instruction.component.reuse = false;
|
||||
|
||||
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); }
|
||||
|
@ -378,7 +376,20 @@ export class Router {
|
|||
* Given a URL, returns an instruction representing the component graph
|
||||
*/
|
||||
recognize(url: string): Promise<Instruction> {
|
||||
return this.registry.recognize(url, this.hostComponent);
|
||||
var ancestorComponents = this._getAncestorInstructions();
|
||||
return this.registry.recognize(url, ancestorComponents);
|
||||
}
|
||||
|
||||
private _getAncestorInstructions(): Instruction[] {
|
||||
var ancestorComponents = [];
|
||||
var ancestorRouter = this;
|
||||
while (isPresent(ancestorRouter.parent) &&
|
||||
isPresent(ancestorRouter.parent._currentInstruction)) {
|
||||
ancestorRouter = ancestorRouter.parent;
|
||||
ancestorComponents.unshift(ancestorRouter._currentInstruction);
|
||||
}
|
||||
|
||||
return ancestorComponents;
|
||||
}
|
||||
|
||||
|
||||
|
@ -399,80 +410,20 @@ export class Router {
|
|||
* app's base href.
|
||||
*/
|
||||
generate(linkParams: any[]): Instruction {
|
||||
let normalizedLinkParams = splitAndFlattenLinkParams(linkParams);
|
||||
|
||||
var first = ListWrapper.first(normalizedLinkParams);
|
||||
var rest = ListWrapper.slice(normalizedLinkParams, 1);
|
||||
|
||||
var router = this;
|
||||
|
||||
// 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;
|
||||
}
|
||||
|
||||
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;
|
||||
var ancestorInstructions = this._getAncestorInstructions();
|
||||
return this.registry.generate(linkParams, ancestorInstructions);
|
||||
}
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class RootRouter extends Router {
|
||||
/** @internal */
|
||||
_location: Location;
|
||||
/** @internal */
|
||||
_locationSub: Object;
|
||||
|
||||
constructor(registry: RouteRegistry, location: Location, primaryComponent: Type) {
|
||||
constructor(registry: RouteRegistry, location: Location,
|
||||
@Inject(ROUTER_PRIMARY_COMPONENT) primaryComponent: Type) {
|
||||
super(registry, null, primaryComponent);
|
||||
this._location = location;
|
||||
this._locationSub = this._location.subscribe(
|
||||
|
@ -482,8 +433,8 @@ export class RootRouter extends Router {
|
|||
}
|
||||
|
||||
commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> {
|
||||
var emitPath = stringifyInstructionPath(instruction);
|
||||
var emitQuery = stringifyInstructionQuery(instruction);
|
||||
var emitPath = instruction.toUrlPath();
|
||||
var emitQuery = instruction.toUrlQuery();
|
||||
if (emitPath.length > 0) {
|
||||
emitPath = '/' + emitPath;
|
||||
}
|
||||
|
@ -521,20 +472,6 @@ 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> {
|
||||
|
|
|
@ -3,7 +3,7 @@ import {isString} from 'angular2/src/facade/lang';
|
|||
|
||||
import {Router} from './router';
|
||||
import {Location} from './location';
|
||||
import {Instruction, stringifyInstruction} from './instruction';
|
||||
import {Instruction} 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 = stringifyInstruction(this._navigationInstruction);
|
||||
var navigationHref = this._navigationInstruction.toLinkUrl();
|
||||
this.visibleHref = this._location.prepareExternalUrl(navigationHref);
|
||||
}
|
||||
|
||||
|
|
|
@ -1,13 +1,19 @@
|
|||
import {RouteHandler} from './route_handler';
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
import {Type} from 'angular2/src/facade/lang';
|
||||
import {isPresent, Type} from 'angular2/src/facade/lang';
|
||||
|
||||
import {RouteHandler} from './route_handler';
|
||||
import {RouteData, BLANK_ROUTE_DATA} from './instruction';
|
||||
|
||||
|
||||
export class SyncRouteHandler implements RouteHandler {
|
||||
public data: RouteData;
|
||||
|
||||
/** @internal */
|
||||
_resolvedComponent: Promise<any> = null;
|
||||
|
||||
constructor(public componentType: Type, public data?: {[key: string]: any}) {
|
||||
constructor(public componentType: Type, 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; }
|
||||
|
|
|
@ -0,0 +1,216 @@
|
|||
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 {}
|
|
@ -0,0 +1,9 @@
|
|||
# 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`.
|
|
@ -0,0 +1,28 @@
|
|||
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); });
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,145 @@
|
|||
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, clickOnElement, getHref} from './util';
|
||||
|
||||
function getLinkElement(rtc: RootTestComponent) {
|
||||
return rtc.debugElement.componentViewChildren[0].nativeElement;
|
||||
}
|
||||
|
||||
var cmpInstanceCount;
|
||||
var childCmpInstanceCount;
|
||||
|
||||
export function main() {
|
||||
describe('auxiliary routes', () => {
|
||||
|
||||
var tcb: TestComponentBuilder;
|
||||
var fixture: 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) => {fixture = 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((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.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) => {fixture = 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((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement).toHaveText('main {hello} | aux {modal}');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should generate a link URL', inject([AsyncTestCompleter], (async) => {
|
||||
compile(
|
||||
tcb,
|
||||
`<a [router-link]="['/Hello', ['Modal']]">open modal</a> | main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Modal'})
|
||||
]))
|
||||
.then((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(getHref(getLinkElement(fixture))).toEqual('/hello(modal)');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should navigate from a link click',
|
||||
inject([AsyncTestCompleter, Location], (async, location) => {
|
||||
compile(
|
||||
tcb,
|
||||
`<a [router-link]="['/Hello', ['Modal']]">open modal</a> | main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Modal'})
|
||||
]))
|
||||
.then((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement)
|
||||
.toHaveText('open modal | main {} | aux {}');
|
||||
|
||||
rtr.subscribe((_) => {
|
||||
fixture.detectChanges();
|
||||
expect(fixture.debugElement.nativeElement)
|
||||
.toHaveText('open modal | main {hello} | aux {modal}');
|
||||
expect(location.urlChanges).toEqual(['/hello(modal)']);
|
||||
async.done();
|
||||
});
|
||||
|
||||
clickOnElement(getLinkElement(fixture));
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@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 {
|
||||
}
|
|
@ -29,27 +29,23 @@ import {
|
|||
Router,
|
||||
APP_BASE_HREF,
|
||||
ROUTER_DIRECTIVES,
|
||||
HashLocationStrategy
|
||||
LocationStrategy
|
||||
} from 'angular2/router';
|
||||
|
||||
import {LocationStrategy} from 'angular2/src/router/location_strategy';
|
||||
import {MockLocationStrategy} from 'angular2/src/mock/mock_location_strategy';
|
||||
import {ApplicationRef} from 'angular2/src/core/application_ref';
|
||||
import {MockApplicationRef} from 'angular2/src/mock/mock_application_ref';
|
||||
|
||||
export function main() {
|
||||
describe('router injectables', () => {
|
||||
beforeEachProviders(() => {
|
||||
return [
|
||||
describe('router bootstrap', () => {
|
||||
beforeEachProviders(() => [
|
||||
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
|
||||
describe('bootstrap functionality', () => {
|
||||
it('should bootstrap a simple app', inject([AsyncTestCompleter], (async) => {
|
||||
var fakeDoc = DOM.createHtmlDocument();
|
||||
var el = DOM.createElement('app-cmp', fakeDoc);
|
||||
|
@ -71,11 +67,9 @@ export function main() {
|
|||
});
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('broken app', () => {
|
||||
beforeEachProviders(
|
||||
() => { return [provide(ROUTER_PRIMARY_COMPONENT, {useValue: BrokenAppCmp})]; });
|
||||
beforeEachProviders(() => [provide(ROUTER_PRIMARY_COMPONENT, {useValue: BrokenAppCmp})]);
|
||||
|
||||
it('should rethrow exceptions from component constructors',
|
||||
inject([AsyncTestCompleter, TestComponentBuilder], (async, tcb: TestComponentBuilder) => {
|
||||
|
@ -91,8 +85,7 @@ export function main() {
|
|||
});
|
||||
|
||||
describe('back button app', () => {
|
||||
beforeEachProviders(
|
||||
() => { return [provide(ROUTER_PRIMARY_COMPONENT, {useValue: HierarchyAppCmp})]; });
|
||||
beforeEachProviders(() => [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) => {
|
||||
|
@ -184,7 +177,7 @@ export function main() {
|
|||
}));
|
||||
});
|
||||
});
|
||||
// TODO: add a test in which the child component has bindings
|
||||
|
||||
|
||||
describe('querystring params app', () => {
|
||||
beforeEachProviders(
|
||||
|
@ -243,20 +236,21 @@ export function main() {
|
|||
}
|
||||
|
||||
|
||||
@Component({selector: 'hello-cmp'})
|
||||
@View({template: 'hello'})
|
||||
@Component({selector: 'hello-cmp', template: 'hello'})
|
||||
class HelloCmp {
|
||||
public message: string;
|
||||
}
|
||||
|
||||
@Component({selector: 'hello2-cmp'})
|
||||
@View({template: 'hello2'})
|
||||
@Component({selector: 'hello2-cmp', template: 'hello2'})
|
||||
class Hello2Cmp {
|
||||
public greeting: string;
|
||||
}
|
||||
|
||||
@Component({selector: 'app-cmp'})
|
||||
@View({template: "outer { <router-outlet></router-outlet> }", directives: ROUTER_DIRECTIVES})
|
||||
@Component({
|
||||
selector: 'app-cmp',
|
||||
template: `outer { <router-outlet></router-outlet> }`,
|
||||
directives: ROUTER_DIRECTIVES
|
||||
})
|
||||
@RouteConfig([new Route({path: '/', component: HelloCmp})])
|
||||
class AppCmp {
|
||||
constructor(public router: Router, public location: LocationStrategy) {}
|
||||
|
@ -283,20 +277,29 @@ class AppWithViewChildren implements AfterViewInit {
|
|||
afterViewInit() { this.helloCmp.message = 'Ahoy'; }
|
||||
}
|
||||
|
||||
@Component({selector: 'parent-cmp'})
|
||||
@View({template: `parent { <router-outlet></router-outlet> }`, directives: ROUTER_DIRECTIVES})
|
||||
@Component({
|
||||
selector: 'parent-cmp',
|
||||
template: `parent { <router-outlet></router-outlet> }`,
|
||||
directives: ROUTER_DIRECTIVES
|
||||
})
|
||||
@RouteConfig([new Route({path: '/child', component: HelloCmp})])
|
||||
class ParentCmp {
|
||||
}
|
||||
|
||||
@Component({selector: 'super-parent-cmp'})
|
||||
@View({template: `super-parent { <router-outlet></router-outlet> }`, directives: ROUTER_DIRECTIVES})
|
||||
@Component({
|
||||
selector: 'super-parent-cmp',
|
||||
template: `super-parent { <router-outlet></router-outlet> }`,
|
||||
directives: ROUTER_DIRECTIVES
|
||||
})
|
||||
@RouteConfig([new Route({path: '/child', component: Hello2Cmp})])
|
||||
class SuperParentCmp {
|
||||
}
|
||||
|
||||
@Component({selector: 'app-cmp'})
|
||||
@View({template: `root { <router-outlet></router-outlet> }`, directives: ROUTER_DIRECTIVES})
|
||||
@Component({
|
||||
selector: 'app-cmp',
|
||||
template: `root { <router-outlet></router-outlet> }`,
|
||||
directives: ROUTER_DIRECTIVES
|
||||
})
|
||||
@RouteConfig([
|
||||
new Route({path: '/parent/...', component: ParentCmp}),
|
||||
new Route({path: '/super-parent/...', component: SuperParentCmp})
|
||||
|
@ -305,28 +308,32 @@ class HierarchyAppCmp {
|
|||
constructor(public router: Router, public location: LocationStrategy) {}
|
||||
}
|
||||
|
||||
@Component({selector: 'qs-cmp'})
|
||||
@View({template: "qParam = {{q}}"})
|
||||
@Component({selector: 'qs-cmp', template: `qParam = {{q}}`})
|
||||
class QSCmp {
|
||||
q: string;
|
||||
constructor(params: RouteParams) { this.q = params.get('q'); }
|
||||
}
|
||||
|
||||
@Component({selector: 'app-cmp'})
|
||||
@View({template: `<router-outlet></router-outlet>`, directives: ROUTER_DIRECTIVES})
|
||||
@Component({
|
||||
selector: 'app-cmp',
|
||||
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'})
|
||||
@View({template: "oh no"})
|
||||
@Component({selector: 'oops-cmp', template: "oh no"})
|
||||
class BrokenCmp {
|
||||
constructor() { throw new BaseException('oops!'); }
|
||||
}
|
||||
|
||||
@Component({selector: 'app-cmp'})
|
||||
@View({template: `outer { <router-outlet></router-outlet> }`, directives: ROUTER_DIRECTIVES})
|
||||
@Component({
|
||||
selector: 'app-cmp',
|
||||
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) {}
|
|
@ -0,0 +1,655 @@
|
|||
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;
|
||||
}
|
|
@ -0,0 +1,131 @@
|
|||
import {Component} from 'angular2/core';
|
||||
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 {
|
||||
}
|
|
@ -0,0 +1,431 @@
|
|||
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;
|
||||
}
|
|
@ -10,7 +10,7 @@ import {
|
|||
expect,
|
||||
iit,
|
||||
inject,
|
||||
beforeEachBindings,
|
||||
beforeEachProviders,
|
||||
it,
|
||||
xit
|
||||
} from 'angular2/testing_internal';
|
||||
|
@ -25,7 +25,6 @@ import {
|
|||
ObservableWrapper
|
||||
} from 'angular2/src/facade/async';
|
||||
|
||||
import {RootRouter} from 'angular2/src/router/router';
|
||||
import {Router, RouterOutlet, RouterLink, RouteParams} from 'angular2/router';
|
||||
import {
|
||||
RouteConfig,
|
||||
|
@ -35,9 +34,6 @@ 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,
|
||||
|
@ -47,7 +43,9 @@ import {
|
|||
} from 'angular2/src/router/interfaces';
|
||||
import {CanActivate} from 'angular2/src/router/lifecycle_annotations';
|
||||
import {ComponentInstruction} from 'angular2/src/router/instruction';
|
||||
import {DirectiveResolver} from 'angular2/src/core/linker/directive_resolver';
|
||||
|
||||
|
||||
import {TEST_ROUTER_PROVIDERS, RootCmp, compile} from './util';
|
||||
|
||||
var cmpInstanceCount;
|
||||
var log: string[];
|
||||
|
@ -61,17 +59,7 @@ export function main() {
|
|||
var fixture: ComponentFixture;
|
||||
var rtr;
|
||||
|
||||
beforeEachBindings(() => [
|
||||
RouteRegistry,
|
||||
DirectiveResolver,
|
||||
provide(Location, {useClass: SpyLocation}),
|
||||
provide(Router,
|
||||
{
|
||||
useFactory:
|
||||
(registry, location) => { return new RootRouter(registry, location, MyComp); },
|
||||
deps: [RouteRegistry, Location]
|
||||
})
|
||||
]);
|
||||
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
|
||||
|
||||
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
|
||||
tcb = tcBuilder;
|
||||
|
@ -81,17 +69,9 @@ 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()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/on-activate'))
|
||||
.then((_) => {
|
||||
|
@ -104,7 +84,8 @@ export function main() {
|
|||
|
||||
it('should wait for a parent component\'s onActivate hook to resolve before calling its child\'s',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => {
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
|
@ -126,7 +107,8 @@ export function main() {
|
|||
}));
|
||||
|
||||
it('should call the onDeactivate hook', inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/on-deactivate'))
|
||||
.then((_) => rtr.navigateByUrl('/a'))
|
||||
|
@ -140,7 +122,8 @@ export function main() {
|
|||
|
||||
it('should wait for a child component\'s onDeactivate hook to resolve before calling its parent\'s',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/parent-deactivate/child-deactivate'))
|
||||
.then((_) => {
|
||||
|
@ -165,7 +148,8 @@ export function main() {
|
|||
|
||||
it('should reuse a component when the canReuse hook returns true',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/on-reuse/1/a'))
|
||||
.then((_) => {
|
||||
|
@ -187,7 +171,8 @@ export function main() {
|
|||
|
||||
it('should not reuse a component when the canReuse hook returns false',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/never-reuse/1/a'))
|
||||
.then((_) => {
|
||||
|
@ -208,7 +193,8 @@ export function main() {
|
|||
|
||||
|
||||
it('should navigate when canActivate returns true', inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => {
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
|
@ -228,7 +214,8 @@ export function main() {
|
|||
|
||||
it('should not navigate when canActivate returns false',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => {
|
||||
ObservableWrapper.subscribe<string>(eventBus, (ev) => {
|
||||
|
@ -248,7 +235,8 @@ export function main() {
|
|||
|
||||
it('should navigate away when canDeactivate returns true',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/can-deactivate/a'))
|
||||
.then((_) => {
|
||||
|
@ -273,7 +261,8 @@ export function main() {
|
|||
|
||||
it('should not navigate away when canDeactivate returns false',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/can-deactivate/a'))
|
||||
.then((_) => {
|
||||
|
@ -299,7 +288,8 @@ export function main() {
|
|||
|
||||
it('should run activation and deactivation hooks in the correct order',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/activation-hooks/child'))
|
||||
.then((_) => {
|
||||
|
@ -325,7 +315,8 @@ export function main() {
|
|||
}));
|
||||
|
||||
it('should only run reuse hooks when reusing', inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/reuse-hooks/1'))
|
||||
.then((_) => {
|
||||
|
@ -352,7 +343,7 @@ export function main() {
|
|||
}));
|
||||
|
||||
it('should not run reuse hooks when not reusing', inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((_) => rtr.config([new Route({path: '/...', component: LifecycleCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/reuse-hooks/1'))
|
||||
.then((_) => {
|
||||
|
@ -383,23 +374,16 @@ export function main() {
|
|||
}
|
||||
|
||||
|
||||
@Component({selector: 'a-cmp'})
|
||||
@View({template: "A"})
|
||||
@Component({selector: 'a-cmp', template: "A"})
|
||||
class A {
|
||||
}
|
||||
|
||||
|
||||
@Component({selector: 'b-cmp'})
|
||||
@View({template: "B"})
|
||||
@Component({selector: 'b-cmp', 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');
|
||||
|
@ -407,16 +391,18 @@ function logHook(name: string, next: ComponentInstruction, prev: ComponentInstru
|
|||
ObservableWrapper.callEmit(eventBus, message);
|
||||
}
|
||||
|
||||
@Component({selector: 'activate-cmp'})
|
||||
@View({template: 'activate cmp'})
|
||||
@Component({selector: 'activate-cmp', template: 'activate cmp'})
|
||||
class ActivateCmp implements OnActivate {
|
||||
onActivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('activate', next, prev);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'parent-activate-cmp'})
|
||||
@View({template: `parent {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
|
||||
@Component({
|
||||
selector: 'parent-activate-cmp',
|
||||
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> {
|
||||
|
@ -426,16 +412,14 @@ class ParentActivateCmp implements OnActivate {
|
|||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'deactivate-cmp'})
|
||||
@View({template: 'deactivate cmp'})
|
||||
@Component({selector: 'deactivate-cmp', template: 'deactivate cmp'})
|
||||
class DeactivateCmp implements OnDeactivate {
|
||||
onDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
logHook('deactivate', next, prev);
|
||||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'deactivate-cmp'})
|
||||
@View({template: 'deactivate cmp'})
|
||||
@Component({selector: 'deactivate-cmp', template: 'deactivate cmp'})
|
||||
class WaitDeactivateCmp implements OnDeactivate {
|
||||
onDeactivate(next: ComponentInstruction, prev: ComponentInstruction): Promise<any> {
|
||||
completer = PromiseWrapper.completer();
|
||||
|
@ -444,8 +428,11 @@ class WaitDeactivateCmp implements OnDeactivate {
|
|||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'parent-deactivate-cmp'})
|
||||
@View({template: `parent {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
|
||||
@Component({
|
||||
selector: 'parent-deactivate-cmp',
|
||||
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) {
|
||||
|
@ -453,26 +440,37 @@ class ParentDeactivateCmp implements OnDeactivate {
|
|||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'reuse-cmp'})
|
||||
@View({template: `reuse {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
|
||||
@Component({
|
||||
selector: 'reuse-cmp',
|
||||
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'})
|
||||
@View({template: `reuse {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
|
||||
@Component({
|
||||
selector: 'never-reuse-cmp',
|
||||
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'})
|
||||
@View({template: `canActivate {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
|
||||
@Component({
|
||||
selector: 'can-activate-cmp',
|
||||
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 {
|
||||
|
@ -483,8 +481,11 @@ class CanActivateCmp {
|
|||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'can-deactivate-cmp'})
|
||||
@View({template: `canDeactivate {<router-outlet></router-outlet>}`, directives: [RouterOutlet]})
|
||||
@Component({
|
||||
selector: 'can-deactivate-cmp',
|
||||
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> {
|
||||
|
@ -494,8 +495,7 @@ class CanDeactivateCmp implements CanDeactivate {
|
|||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'all-hooks-child-cmp'})
|
||||
@View({template: `child`})
|
||||
@Component({selector: 'all-hooks-child-cmp', template: `child`})
|
||||
@CanActivate(AllHooksChildCmp.canActivate)
|
||||
class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate {
|
||||
canDeactivate(next: ComponentInstruction, prev: ComponentInstruction) {
|
||||
|
@ -517,11 +517,15 @@ class AllHooksChildCmp implements CanDeactivate, OnDeactivate, OnActivate {
|
|||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'all-hooks-parent-cmp'})
|
||||
@View({template: `<router-outlet></router-outlet>`, directives: [RouterOutlet]})
|
||||
@Component({
|
||||
selector: 'all-hooks-parent-cmp',
|
||||
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;
|
||||
|
@ -541,8 +545,7 @@ class AllHooksParentCmp implements CanDeactivate, OnDeactivate, OnActivate {
|
|||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'reuse-hooks-cmp'})
|
||||
@View({template: 'reuse hooks cmp'})
|
||||
@Component({selector: 'reuse-hooks-cmp', template: 'reuse hooks cmp'})
|
||||
@CanActivate(ReuseHooksCmp.canActivate)
|
||||
class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanDeactivate {
|
||||
canReuse(next: ComponentInstruction, prev: ComponentInstruction): Promise<any> {
|
||||
|
@ -574,8 +577,11 @@ class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanD
|
|||
}
|
||||
}
|
||||
|
||||
@Component({selector: 'lifecycle-cmp'})
|
||||
@View({template: `<router-outlet></router-outlet>`, directives: [RouterOutlet]})
|
||||
@Component({
|
||||
selector: 'lifecycle-cmp',
|
||||
template: `<router-outlet></router-outlet>`,
|
||||
directives: [RouterOutlet]
|
||||
})
|
||||
@RouteConfig([
|
||||
new Route({path: '/a', component: A}),
|
||||
new Route({path: '/on-activate', component: ActivateCmp}),
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
expect,
|
||||
iit,
|
||||
inject,
|
||||
beforeEachBindings,
|
||||
beforeEachProviders,
|
||||
it,
|
||||
xit
|
||||
} from 'angular2/testing_internal';
|
||||
|
@ -18,8 +18,7 @@ import {
|
|||
import {provide, Component, View, Injector, Inject} from 'angular2/core';
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
|
||||
import {RootRouter} from 'angular2/src/router/router';
|
||||
import {Router, RouterOutlet, RouterLink, RouteParams, RouteData} from 'angular2/router';
|
||||
import {Router, RouterOutlet, RouterLink, RouteParams, RouteData, Location} from 'angular2/router';
|
||||
import {
|
||||
RouteConfig,
|
||||
Route,
|
||||
|
@ -28,14 +27,10 @@ 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 {DirectiveResolver} from 'angular2/src/core/linker/directive_resolver';
|
||||
import {TEST_ROUTER_PROVIDERS, RootCmp, compile} from './util';
|
||||
|
||||
var cmpInstanceCount;
|
||||
var childCmpInstanceCount;
|
||||
var log: string[];
|
||||
|
||||
export function main() {
|
||||
describe('navigation', () => {
|
||||
|
@ -44,37 +39,18 @@ export function main() {
|
|||
var fixture: ComponentFixture;
|
||||
var rtr;
|
||||
|
||||
beforeEachBindings(() => [
|
||||
RouteRegistry,
|
||||
DirectiveResolver,
|
||||
provide(Location, {useClass: SpyLocation}),
|
||||
provide(Router,
|
||||
{
|
||||
useFactory:
|
||||
(registry, location) => { return new RootRouter(registry, location, MyComp); },
|
||||
deps: [RouteRegistry, Location]
|
||||
})
|
||||
]);
|
||||
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
|
||||
|
||||
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()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/test', component: HelloCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/test'))
|
||||
.then((_) => {
|
||||
|
@ -87,7 +63,8 @@ export function main() {
|
|||
|
||||
it('should navigate between components with different parameters',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/user/:name', component: UserCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/user/brian'))
|
||||
.then((_) => {
|
||||
|
@ -102,9 +79,9 @@ export function main() {
|
|||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should navigate to child routes', inject([AsyncTestCompleter], (async) => {
|
||||
compile('outer { <router-outlet></router-outlet> }')
|
||||
compile(tcb, 'outer { <router-outlet></router-outlet> }')
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/a/...', component: ParentCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/a/b'))
|
||||
.then((_) => {
|
||||
|
@ -116,7 +93,9 @@ export function main() {
|
|||
|
||||
it('should navigate to child routes that capture an empty path',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile('outer { <router-outlet></router-outlet> }')
|
||||
|
||||
compile(tcb, 'outer { <router-outlet></router-outlet> }')
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/a/...', component: ParentCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/a'))
|
||||
.then((_) => {
|
||||
|
@ -126,9 +105,9 @@ export function main() {
|
|||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should navigate to child routes of async routes', inject([AsyncTestCompleter], (async) => {
|
||||
compile('outer { <router-outlet></router-outlet> }')
|
||||
compile(tcb, 'outer { <router-outlet></router-outlet> }')
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new AsyncRoute({path: '/a/...', loader: parentLoader})]))
|
||||
.then((_) => rtr.navigateByUrl('/a/b'))
|
||||
.then((_) => {
|
||||
|
@ -138,26 +117,9 @@ 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()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/team/:id/...', component: TeamCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/team/angular/user/rado'))
|
||||
.then((_) => {
|
||||
|
@ -177,7 +139,8 @@ export function main() {
|
|||
|
||||
it('should not reuse children when parent components change',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([new Route({path: '/team/:id/...', component: TeamCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/team/angular/user/rado'))
|
||||
.then((_) => {
|
||||
|
@ -197,7 +160,8 @@ export function main() {
|
|||
}));
|
||||
|
||||
it('should inject route data into component', inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/route-data', component: RouteDataCmp, data: {isAdmin: true}})
|
||||
]))
|
||||
|
@ -211,10 +175,11 @@ export function main() {
|
|||
|
||||
it('should inject route data into component with AsyncRoute',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.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((_) => {
|
||||
|
@ -226,7 +191,8 @@ export function main() {
|
|||
|
||||
it('should inject empty object if the route has no data property',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
compile()
|
||||
compile(tcb)
|
||||
.then((rtc) => {fixture = rtc})
|
||||
.then((_) => rtr.config(
|
||||
[new Route({path: '/route-data-default', component: RouteDataCmp})]))
|
||||
.then((_) => rtr.navigateByUrl('/route-data-default'))
|
||||
|
@ -236,45 +202,28 @@ 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'})
|
||||
@View({template: "{{greeting}}"})
|
||||
@Component({selector: 'hello-cmp', 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'})
|
||||
@View({template: "{{myData}}"})
|
||||
@Component({selector: 'data-cmp', template: `{{myData}}`})
|
||||
class RouteDataCmp {
|
||||
myData: boolean;
|
||||
constructor(data: RouteData) { this.myData = data.get('isAdmin'); }
|
||||
}
|
||||
|
||||
@Component({selector: 'user-cmp'})
|
||||
@View({template: "hello {{user}}"})
|
||||
@Component({selector: 'user-cmp', template: `hello {{user}}`})
|
||||
class UserCmp {
|
||||
user: string;
|
||||
constructor(params: RouteParams) {
|
||||
|
@ -288,9 +237,9 @@ function parentLoader() {
|
|||
return PromiseWrapper.resolve(ParentCmp);
|
||||
}
|
||||
|
||||
@Component({selector: 'parent-cmp'})
|
||||
@View({
|
||||
template: "inner { <router-outlet></router-outlet> }",
|
||||
@Component({
|
||||
selector: 'parent-cmp',
|
||||
template: `inner { <router-outlet></router-outlet> }`,
|
||||
directives: [RouterOutlet],
|
||||
})
|
||||
@RouteConfig([
|
||||
|
@ -298,13 +247,12 @@ function parentLoader() {
|
|||
new Route({path: '/', component: HelloCmp}),
|
||||
])
|
||||
class ParentCmp {
|
||||
constructor() {}
|
||||
}
|
||||
|
||||
|
||||
@Component({selector: 'team-cmp'})
|
||||
@View({
|
||||
template: "team {{id}} { <router-outlet></router-outlet> }",
|
||||
@Component({
|
||||
selector: 'team-cmp',
|
||||
template: `team {{id}} { <router-outlet></router-outlet> }`,
|
||||
directives: [RouterOutlet],
|
||||
})
|
||||
@RouteConfig([new Route({path: '/user/:name', component: UserCmp})])
|
||||
|
@ -315,27 +263,3 @@ 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 {
|
||||
}
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
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();
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
|
@ -9,7 +9,7 @@ import {
|
|||
expect,
|
||||
iit,
|
||||
inject,
|
||||
beforeEachBindings,
|
||||
beforeEachProviders,
|
||||
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, DirectiveResolver} from 'angular2/core';
|
||||
import {provide, Component, View, DirectiveResolver} from 'angular2/core';
|
||||
|
||||
import {SpyLocation} from 'angular2/src/mock/location_mock';
|
||||
import {
|
||||
|
@ -35,7 +35,8 @@ import {
|
|||
Route,
|
||||
RouteParams,
|
||||
RouteConfig,
|
||||
ROUTER_DIRECTIVES
|
||||
ROUTER_DIRECTIVES,
|
||||
ROUTER_PRIMARY_COMPONENT
|
||||
} from 'angular2/router';
|
||||
import {RootRouter} from 'angular2/src/router/router';
|
||||
|
||||
|
@ -47,16 +48,12 @@ export function main() {
|
|||
var fixture: ComponentFixture;
|
||||
var router, location;
|
||||
|
||||
beforeEachBindings(() => [
|
||||
beforeEachProviders(() => [
|
||||
RouteRegistry,
|
||||
DirectiveResolver,
|
||||
provide(Location, {useClass: SpyLocation}),
|
||||
provide(Router,
|
||||
{
|
||||
useFactory:
|
||||
(registry, location) => { return new RootRouter(registry, location, MyComp); },
|
||||
deps: [RouteRegistry, Location]
|
||||
})
|
||||
provide(ROUTER_PRIMARY_COMPONENT, {useValue: MyComp}),
|
||||
provide(Router, {useClass: RootRouter})
|
||||
]);
|
||||
|
||||
beforeEach(inject([TestComponentBuilder, Router, Location], (tcBuilder, rtr, loc) => {
|
||||
|
@ -240,8 +237,8 @@ export function main() {
|
|||
.then((_) => router.config([new Route({path: '/...', component: AuxLinkCmp})]))
|
||||
.then((_) => router.navigateByUrl('/'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(DOM.getAttribute(rootTC.debugElement.componentViewChildren[1]
|
||||
fixture.detectChanges();
|
||||
expect(DOM.getAttribute(fixture.debugElement.componentViewChildren[1]
|
||||
.componentViewChildren[0]
|
||||
.nativeElement,
|
||||
'href'))
|
||||
|
@ -386,10 +383,7 @@ 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'); }
|
||||
|
@ -425,17 +419,11 @@ 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 {
|
||||
}
|
||||
|
||||
|
@ -455,7 +443,6 @@ function parentCmpLoader() {
|
|||
new Route({path: '/better-grandchild', component: Hello2Cmp, name: 'BetterGrandchild'})
|
||||
])
|
||||
class ParentCmp {
|
||||
constructor(public router: Router) {}
|
||||
}
|
||||
|
||||
@Component({
|
||||
|
|
|
@ -0,0 +1,24 @@
|
|||
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); });
|
||||
|
||||
});
|
||||
});
|
||||
}
|
|
@ -0,0 +1,133 @@
|
|||
import {provide, Provider, Component, View} 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, ROUTER_PRIMARY_COMPONENT} 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_PRIMARY_COMPONENT, {useValue: RootCmp}),
|
||||
provide(Router, {useClass: RootRouter})
|
||||
];
|
||||
|
||||
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);
|
||||
}
|
|
@ -12,100 +12,82 @@ 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#', mockRouteHandler))
|
||||
expect(() => new PathRecognizer('/hi#'))
|
||||
.toThrowError(`Path "/hi#" should not include "#". Use "HashLocationStrategy" instead.`);
|
||||
expect(() => new PathRecognizer('hi?', mockRouteHandler))
|
||||
expect(() => new PathRecognizer('hi?'))
|
||||
.toThrowError(`Path "hi?" contains "?" which is not allowed in a route config.`);
|
||||
expect(() => new PathRecognizer('hi;', mockRouteHandler))
|
||||
expect(() => new PathRecognizer('hi;'))
|
||||
.toThrowError(`Path "hi;" contains ";" which is not allowed in a route config.`);
|
||||
expect(() => new PathRecognizer('hi=', mockRouteHandler))
|
||||
expect(() => new PathRecognizer('hi='))
|
||||
.toThrowError(`Path "hi=" contains "=" which is not allowed in a route config.`);
|
||||
expect(() => new PathRecognizer('hi(', mockRouteHandler))
|
||||
expect(() => new PathRecognizer('hi('))
|
||||
.toThrowError(`Path "hi(" contains "(" which is not allowed in a route config.`);
|
||||
expect(() => new PathRecognizer('hi)', mockRouteHandler))
|
||||
expect(() => new PathRecognizer('hi)'))
|
||||
.toThrowError(`Path "hi)" contains ")" which is not allowed in a route config.`);
|
||||
expect(() => new PathRecognizer('hi//there', mockRouteHandler))
|
||||
expect(() => new PathRecognizer('hi//there'))
|
||||
.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', mockRouteHandler);
|
||||
var rec = new PathRecognizer('/hello/there');
|
||||
var url = parser.parse('/hello/there?name=igor');
|
||||
var match = rec.recognize(url);
|
||||
expect(match.instruction.params).toEqual({'name': 'igor'});
|
||||
expect(match['allParams']).toEqual({'name': 'igor'});
|
||||
});
|
||||
|
||||
it('should return a combined map of parameters with the param expected in the URL path',
|
||||
() => {
|
||||
var rec = new PathRecognizer('/hello/:name', mockRouteHandler);
|
||||
var rec = new PathRecognizer('/hello/:name');
|
||||
var url = parser.parse('/hello/paul?topic=success');
|
||||
var match = rec.recognize(url);
|
||||
expect(match.instruction.params).toEqual({'name': 'paul', 'topic': 'success'});
|
||||
expect(match['allParams']).toEqual({'name': 'paul', 'topic': 'success'});
|
||||
});
|
||||
});
|
||||
|
||||
describe('matrix params', () => {
|
||||
it('should be parsed along with dynamic paths', () => {
|
||||
var rec = new PathRecognizer('/hello/:id', mockRouteHandler);
|
||||
var rec = new PathRecognizer('/hello/:id');
|
||||
var url = new Url('hello', new Url('matias', null, null, {'key': 'value'}));
|
||||
var match = rec.recognize(url);
|
||||
expect(match.instruction.params).toEqual({'id': 'matias', 'key': 'value'});
|
||||
expect(match['allParams']).toEqual({'id': 'matias', 'key': 'value'});
|
||||
});
|
||||
|
||||
it('should be parsed on a static path', () => {
|
||||
var rec = new PathRecognizer('/person', mockRouteHandler);
|
||||
var rec = new PathRecognizer('/person');
|
||||
var url = new Url('person', null, null, {'name': 'dave'});
|
||||
var match = rec.recognize(url);
|
||||
expect(match.instruction.params).toEqual({'name': 'dave'});
|
||||
expect(match['allParams']).toEqual({'name': 'dave'});
|
||||
});
|
||||
|
||||
it('should be ignored on a wildcard segment', () => {
|
||||
var rec = new PathRecognizer('/wild/*everything', mockRouteHandler);
|
||||
var rec = new PathRecognizer('/wild/*everything');
|
||||
var url = parser.parse('/wild/super;variable=value');
|
||||
var match = rec.recognize(url);
|
||||
expect(match.instruction.params).toEqual({'everything': 'super;variable=value'});
|
||||
expect(match['allParams']).toEqual({'everything': 'super;variable=value'});
|
||||
});
|
||||
|
||||
it('should set matrix param values to true when no value is present', () => {
|
||||
var rec = new PathRecognizer('/path', mockRouteHandler);
|
||||
var rec = new PathRecognizer('/path');
|
||||
var url = new Url('path', null, null, {'one': true, 'two': true, 'three': '3'});
|
||||
var match = rec.recognize(url);
|
||||
expect(match.instruction.params).toEqual({'one': true, 'two': true, 'three': '3'});
|
||||
expect(match['allParams']).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', mockRouteHandler);
|
||||
var rec = new PathRecognizer('/one/two/three');
|
||||
|
||||
var three = new Url('three', null, null, {'c': '3'});
|
||||
var two = new Url('two', three, null, {'b': '2'});
|
||||
var one = new Url('one', two, null, {'a': '1'});
|
||||
|
||||
var match = rec.recognize(one);
|
||||
expect(match.instruction.params).toEqual({'c': '3'});
|
||||
expect(match['allParams']).toEqual({'c': '3'});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -214,7 +214,10 @@ class HelloCmp {
|
|||
|
||||
@Component({selector: 'app-cmp'})
|
||||
@View({template: `root { <router-outlet></router-outlet> }`, directives: ROUTER_DIRECTIVES})
|
||||
@RouteConfig([{path: '/before', redirectTo: '/after'}, {path: '/after', component: HelloCmp}])
|
||||
@RouteConfig([
|
||||
{path: '/before', redirectTo: ['Hello']},
|
||||
{path: '/after', component: HelloCmp, name: 'Hello'}
|
||||
])
|
||||
class RedirectAppCmp {
|
||||
constructor(public router: Router, public location: LocationStrategy) {}
|
||||
}
|
||||
|
|
|
@ -1,185 +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 {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 {}
|
|
@ -11,7 +11,7 @@ import {
|
|||
} from 'angular2/testing_internal';
|
||||
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
import {Type} from 'angular2/src/facade/lang';
|
||||
import {Type, IS_DART} from 'angular2/src/facade/lang';
|
||||
|
||||
import {RouteRegistry} from 'angular2/src/router/route_registry';
|
||||
import {
|
||||
|
@ -21,20 +21,19 @@ 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;
|
||||
|
||||
beforeEach(() => { registry = new RouteRegistry(); });
|
||||
beforeEach(() => { registry = new RouteRegistry(RootHostCmp); });
|
||||
|
||||
it('should match the full URL', inject([AsyncTestCompleter], (async) => {
|
||||
registry.config(RootHostCmp, new Route({path: '/', component: DummyCmpA}));
|
||||
registry.config(RootHostCmp, new Route({path: '/test', component: DummyCmpB}));
|
||||
|
||||
registry.recognize('/test', RootHostCmp)
|
||||
registry.recognize('/test', [])
|
||||
.then((instruction) => {
|
||||
expect(instruction.component.componentType).toBe(DummyCmpB);
|
||||
async.done();
|
||||
|
@ -45,28 +44,35 @@ export function main() {
|
|||
registry.config(RootHostCmp,
|
||||
new Route({path: '/first/...', component: DummyParentCmp, name: 'FirstCmp'}));
|
||||
|
||||
expect(stringifyInstruction(registry.generate(['FirstCmp', 'SecondCmp'], RootHostCmp)))
|
||||
var instr = registry.generate(['FirstCmp', 'SecondCmp'], []);
|
||||
expect(stringifyInstruction(instr)).toEqual('first/second');
|
||||
|
||||
expect(stringifyInstruction(registry.generate(['SecondCmp'], [instr])))
|
||||
.toEqual('first/second');
|
||||
expect(stringifyInstruction(registry.generate(['SecondCmp'], DummyParentCmp)))
|
||||
.toEqual('second');
|
||||
});
|
||||
|
||||
xit('should generate URLs that account for redirects', () => {
|
||||
registry.config(
|
||||
RootHostCmp,
|
||||
new Route({path: '/first/...', component: DummyParentRedirectCmp, name: 'FirstCmp'}));
|
||||
|
||||
expect(stringifyInstruction(registry.generate(['FirstCmp'], RootHostCmp)))
|
||||
expect(stringifyInstruction(registry.generate(['./SecondCmp'], [instr])))
|
||||
.toEqual('first/second');
|
||||
});
|
||||
|
||||
xit('should generate URLs in a hierarchy of redirects', () => {
|
||||
it('should generate URLs that account for default routes', () => {
|
||||
registry.config(
|
||||
RootHostCmp,
|
||||
new Route({path: '/first/...', component: DummyMultipleRedirectCmp, name: 'FirstCmp'}));
|
||||
new Route({path: '/first/...', component: ParentWithDefaultRouteCmp, name: 'FirstCmp'}));
|
||||
|
||||
expect(stringifyInstruction(registry.generate(['FirstCmp'], RootHostCmp)))
|
||||
.toEqual('first/second/third');
|
||||
var instruction = registry.generate(['FirstCmp'], []);
|
||||
|
||||
expect(instruction.toLinkUrl()).toEqual('first');
|
||||
expect(instruction.toRootUrl()).toEqual('first/second');
|
||||
});
|
||||
|
||||
it('should generate URLs in a hierarchy of default routes', () => {
|
||||
registry.config(
|
||||
RootHostCmp,
|
||||
new Route({path: '/first/...', component: MultipleDefaultCmp, name: 'FirstCmp'}));
|
||||
|
||||
var instruction = registry.generate(['FirstCmp'], []);
|
||||
|
||||
expect(instruction.toLinkUrl()).toEqual('first');
|
||||
expect(instruction.toRootUrl()).toEqual('first/second/third');
|
||||
});
|
||||
|
||||
it('should generate URLs with params', () => {
|
||||
|
@ -74,14 +80,14 @@ export function main() {
|
|||
RootHostCmp,
|
||||
new Route({path: '/first/:param/...', component: DummyParentParamCmp, name: 'FirstCmp'}));
|
||||
|
||||
var url = stringifyInstruction(registry.generate(
|
||||
['FirstCmp', {param: 'one'}, 'SecondCmp', {param: 'two'}], RootHostCmp));
|
||||
var url = stringifyInstruction(
|
||||
registry.generate(['FirstCmp', {param: 'one'}, 'SecondCmp', {param: 'two'}], []));
|
||||
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'], []);
|
||||
expect(instruction.component.params).toEqual({});
|
||||
});
|
||||
|
||||
|
@ -91,20 +97,20 @@ export function main() {
|
|||
RootHostCmp,
|
||||
new AsyncRoute({path: '/first/...', loader: asyncParentLoader, name: 'FirstCmp'}));
|
||||
|
||||
expect(() => registry.generate(['FirstCmp', 'SecondCmp'], RootHostCmp))
|
||||
.toThrowError('Could not find route named "SecondCmp".');
|
||||
var instruction = registry.generate(['FirstCmp', 'SecondCmp'], []);
|
||||
|
||||
registry.recognize('/first/second', RootHostCmp)
|
||||
expect(stringifyInstruction(instruction)).toEqual('first');
|
||||
|
||||
registry.recognize('/first/second', [])
|
||||
.then((_) => {
|
||||
expect(
|
||||
stringifyInstruction(registry.generate(['FirstCmp', 'SecondCmp'], RootHostCmp)))
|
||||
.toEqual('first/second');
|
||||
var instruction = registry.generate(['FirstCmp', 'SecondCmp'], []);
|
||||
expect(stringifyInstruction(instruction)).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'], []))
|
||||
.toThrowError('Component "RootHostCmp" has no route config.');
|
||||
});
|
||||
|
||||
|
@ -113,7 +119,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']], [])))
|
||||
.toEqual('primary(aux)');
|
||||
});
|
||||
|
||||
|
@ -121,7 +127,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', [])
|
||||
.then((instruction) => {
|
||||
expect(instruction.component.componentType).toBe(DummyCmpA);
|
||||
async.done();
|
||||
|
@ -132,7 +138,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', [])
|
||||
.then((instruction) => {
|
||||
expect(instruction.component.componentType).toBe(DummyCmpA);
|
||||
async.done();
|
||||
|
@ -143,7 +149,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', [])
|
||||
.then((instruction) => {
|
||||
expect(instruction.component.componentType).toBe(DummyCmpA);
|
||||
async.done();
|
||||
|
@ -154,7 +160,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', [])
|
||||
.then((instruction) => {
|
||||
expect(instruction.component.componentType).toBe(DummyCmpA);
|
||||
async.done();
|
||||
|
@ -168,7 +174,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', [])
|
||||
.then((instruction) => {
|
||||
expect(instruction.component.componentType).toBe(DummyCmpB);
|
||||
async.done();
|
||||
|
@ -178,7 +184,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', [])
|
||||
.then((instruction) => {
|
||||
expect(instruction.component.componentType).toBe(DummyParentCmp);
|
||||
expect(instruction.child.component.componentType).toBe(DummyCmpB);
|
||||
|
@ -190,12 +196,15 @@ export function main() {
|
|||
inject([AsyncTestCompleter], (async) => {
|
||||
registry.config(RootHostCmp, new Route({path: '/first/...', component: DummyAsyncCmp}));
|
||||
|
||||
registry.recognize('/first/second', RootHostCmp)
|
||||
registry.recognize('/first/second', [])
|
||||
.then((instruction) => {
|
||||
expect(instruction.component.componentType).toBe(DummyAsyncCmp);
|
||||
expect(instruction.child.component.componentType).toBe(DummyCmpB);
|
||||
|
||||
instruction.child.resolveComponent().then((childComponentInstruction) => {
|
||||
expect(childComponentInstruction.componentType).toBe(DummyCmpB);
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should match the URL using an async parent component',
|
||||
|
@ -203,12 +212,15 @@ export function main() {
|
|||
registry.config(RootHostCmp,
|
||||
new AsyncRoute({path: '/first/...', loader: asyncParentLoader}));
|
||||
|
||||
registry.recognize('/first/second', RootHostCmp)
|
||||
registry.recognize('/first/second', [])
|
||||
.then((instruction) => {
|
||||
expect(instruction.component.componentType).toBe(DummyParentCmp);
|
||||
expect(instruction.child.component.componentType).toBe(DummyCmpB);
|
||||
|
||||
instruction.child.resolveComponent().then((childType) => {
|
||||
expect(childType.componentType).toBe(DummyCmpB);
|
||||
async.done();
|
||||
});
|
||||
});
|
||||
}));
|
||||
|
||||
it('should throw when a parent config is missing the `...` suffix any of its children add routes',
|
||||
|
@ -242,15 +254,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 or async instruction.');
|
||||
expect(() => { registry.generate(['First'], []); })
|
||||
.toThrowError('Link "["First"]" does not resolve to a terminal 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', [])
|
||||
.then((instruction) => {
|
||||
expect(instruction.component.componentType).toBe(DummyParentCmp);
|
||||
expect(instruction.component.params).toEqual({'comments': 'all'});
|
||||
|
@ -276,13 +288,18 @@ export function main() {
|
|||
sort: 'asc',
|
||||
}
|
||||
],
|
||||
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);
|
||||
}
|
||||
|
@ -300,26 +317,22 @@ class DummyAsyncCmp {
|
|||
class DummyCmpA {}
|
||||
class DummyCmpB {}
|
||||
|
||||
@RouteConfig([
|
||||
new Redirect({path: '/', redirectTo: '/third'}),
|
||||
new Route({path: '/third', component: DummyCmpB, name: 'ThirdCmp'})
|
||||
])
|
||||
class DummyRedirectCmp {
|
||||
@RouteConfig(
|
||||
[new Route({path: '/third', component: DummyCmpB, name: 'ThirdCmp', useAsDefault: true})])
|
||||
class DefaultRouteCmp {
|
||||
}
|
||||
|
||||
|
||||
@RouteConfig([
|
||||
new Redirect({path: '/', redirectTo: '/second'}),
|
||||
new Route({path: '/second/...', component: DummyRedirectCmp, name: 'SecondCmp'})
|
||||
new Route(
|
||||
{path: '/second/...', component: DefaultRouteCmp, name: 'SecondCmp', useAsDefault: true})
|
||||
])
|
||||
class DummyMultipleRedirectCmp {
|
||||
class MultipleDefaultCmp {
|
||||
}
|
||||
|
||||
@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', useAsDefault: true})])
|
||||
class ParentWithDefaultRouteCmp {
|
||||
}
|
||||
|
||||
@RouteConfig([new Route({path: '/second', component: DummyCmpB, name: 'SecondCmp'})])
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
expect,
|
||||
iit,
|
||||
inject,
|
||||
beforeEachBindings,
|
||||
beforeEachProviders,
|
||||
it,
|
||||
xit,
|
||||
TestComponentBuilder
|
||||
|
@ -27,24 +27,20 @@ import {
|
|||
RouterOutlet,
|
||||
Route,
|
||||
RouteParams,
|
||||
Instruction,
|
||||
ComponentInstruction
|
||||
} from 'angular2/router';
|
||||
|
||||
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
|
||||
import {ComponentInstruction_} from 'angular2/src/router/instruction';
|
||||
import {PathRecognizer} from 'angular2/src/router/path_recognizer';
|
||||
import {SyncRouteHandler} from 'angular2/src/router/sync_route_handler';
|
||||
import {ResolvedInstruction} from 'angular2/src/router/instruction';
|
||||
|
||||
let dummyPathRecognizer = new PathRecognizer('', new SyncRouteHandler(null));
|
||||
let dummyInstruction =
|
||||
new Instruction(new ComponentInstruction_('detail', [], dummyPathRecognizer), null, {});
|
||||
new ResolvedInstruction(new ComponentInstruction('detail', [], null, null, true, 0), null, {});
|
||||
|
||||
export function main() {
|
||||
describe('router-link directive', function() {
|
||||
var tcb: TestComponentBuilder;
|
||||
|
||||
beforeEachBindings(() => [
|
||||
beforeEachProviders(() => [
|
||||
provide(Location, {useValue: makeDummyLocation()}),
|
||||
provide(Router, {useValue: makeDummyRouter()})
|
||||
]);
|
||||
|
@ -106,11 +102,6 @@ export function main() {
|
|||
});
|
||||
}
|
||||
|
||||
@Component({selector: 'my-comp'})
|
||||
class MyComp {
|
||||
name;
|
||||
}
|
||||
|
||||
@Component({selector: 'user-cmp'})
|
||||
@View({template: "hello {{user}}"})
|
||||
class UserCmp {
|
||||
|
|
|
@ -8,7 +8,7 @@ import {
|
|||
expect,
|
||||
inject,
|
||||
beforeEach,
|
||||
beforeEachBindings
|
||||
beforeEachProviders
|
||||
} from 'angular2/testing_internal';
|
||||
import {SpyRouterOutlet} from './spies';
|
||||
import {Type} from 'angular2/src/facade/lang';
|
||||
|
@ -18,9 +18,8 @@ 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 {RouteRegistry, ROUTER_PRIMARY_COMPONENT} from 'angular2/src/router/route_registry';
|
||||
import {RouteConfig, AsyncRoute, Route} from 'angular2/src/router/route_config_decorator';
|
||||
import {DirectiveResolver} from 'angular2/src/core/linker/directive_resolver';
|
||||
|
||||
|
@ -30,16 +29,12 @@ export function main() {
|
|||
describe('Router', () => {
|
||||
var router, location;
|
||||
|
||||
beforeEachBindings(() => [
|
||||
beforeEachProviders(() => [
|
||||
RouteRegistry,
|
||||
DirectiveResolver,
|
||||
provide(Location, {useClass: SpyLocation}),
|
||||
provide(Router,
|
||||
{
|
||||
useFactory:
|
||||
(registry, location) => { return new RootRouter(registry, location, AppCmp); },
|
||||
deps: [RouteRegistry, Location]
|
||||
})
|
||||
provide(ROUTER_PRIMARY_COMPONENT, {useValue: AppCmp}),
|
||||
provide(Router, {useClass: RootRouter})
|
||||
]);
|
||||
|
||||
|
||||
|
@ -225,6 +220,11 @@ export function main() {
|
|||
});
|
||||
}
|
||||
|
||||
|
||||
function stringifyInstruction(instruction): string {
|
||||
return instruction.toRootUrl();
|
||||
}
|
||||
|
||||
function loader(): Promise<Type> {
|
||||
return PromiseWrapper.resolve(DummyComponent);
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ module.exports = function makeNodeTree(projects, destinationPath) {
|
|||
|
||||
// we call browser's bootstrap
|
||||
'angular2/test/router/route_config_spec.ts',
|
||||
'angular2/test/router/integration/router_integration_spec.ts',
|
||||
'angular2/test/router/integration/bootstrap_spec.ts',
|
||||
|
||||
// we check the public api by importing angular2/angular2
|
||||
'angular2/test/symbol_inspector/**/*.ts',
|
||||
|
|
Loading…
Reference in New Issue