diff --git a/modules/angular1_router/build.js b/modules/angular1_router/build.js
index ddaacbaa67..5d5065bd86 100644
--- a/modules/angular1_router/build.js
+++ b/modules/angular1_router/build.js
@@ -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') {
diff --git a/modules/angular1_router/lib/facades.es5 b/modules/angular1_router/lib/facades.es5
index f60f3b986d..b82d198056 100644
--- a/modules/angular1_router/lib/facades.es5
+++ b/modules/angular1_router/lib/facades.es5
@@ -173,6 +173,10 @@ var StringMapWrapper = {
var List = Array;
var ListWrapper = {
+ clear: function (l) {
+ l.length = 0;
+ },
+
create: function () {
return [];
},
diff --git a/modules/angular1_router/src/module_template.js b/modules/angular1_router/src/module_template.js
index aeed65ea3e..6712ca77c8 100644
--- a/modules/angular1_router/src/module_template.js
+++ b/modules/angular1_router/src/module_template.js
@@ -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) {
diff --git a/modules/angular1_router/src/ng_route_shim.js b/modules/angular1_router/src/ng_route_shim.js
index 0d3f245b28..5ba7aa4fe7 100644
--- a/modules/angular1_router/src/ng_route_shim.js
+++ b/modules/angular1_router/src/ng_route_shim.js
@@ -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;
diff --git a/modules/angular1_router/test/integration/navigation_spec.js b/modules/angular1_router/test/integration/navigation_spec.js
index 1a82f197c6..76b9490bab 100644
--- a/modules/angular1_router/test/integration/navigation_spec.js
+++ b/modules/angular1_router/test/integration/navigation_spec.js
@@ -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: '
',
$routeConfig: [
@@ -152,8 +151,8 @@ describe('navigation', function () {
compile('');
$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: '',
$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('');
diff --git a/modules/angular1_router/test/integration/shim_spec.js b/modules/angular1_router/test/integration/shim_spec.js
index f074bd50fd..e2edffe552 100644
--- a/modules/angular1_router/test/integration/shim_spec.js
+++ b/modules/angular1_router/test/integration/shim_spec.js
@@ -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();
diff --git a/modules/angular2/router.ts b/modules/angular2/router.ts
index 3bc054c9b7..b329aa8a0d 100644
--- a/modules/angular2/router.ts
+++ b/modules/angular2/router.ts
@@ -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
diff --git a/modules/angular2/src/router/async_route_handler.ts b/modules/angular2/src/router/async_route_handler.ts
index 6e22218344..a739352151 100644
--- a/modules/angular2/src/router/async_route_handler.ts
+++ b/modules/angular2/src/router/async_route_handler.ts
@@ -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 = 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 {
if (isPresent(this._resolvedComponent)) {
diff --git a/modules/angular2/src/router/component_recognizer.ts b/modules/angular2/src/router/component_recognizer.ts
new file mode 100644
index 0000000000..56730566fd
--- /dev/null
+++ b/modules/angular2/src/router/component_recognizer.ts
@@ -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();
+
+ // map from name to recognizer
+ auxNames = new Map();
+
+ // map from starting path to recognizer
+ auxRoutes = new Map();
+
+ // 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[] {
+ var solutions = [];
+
+ this.matchers.forEach((routeRecognizer: AbstractRecognizer) => {
+ var pathMatch = routeRecognizer.recognize(urlParse);
+
+ if (isPresent(pathMatch)) {
+ solutions.push(pathMatch);
+ }
+ });
+
+ return solutions;
+ }
+
+ recognizeAuxiliary(urlParse: Url): Promise[] {
+ 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 {
+ 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);
+ }
+}
diff --git a/modules/angular2/src/router/instruction.ts b/modules/angular2/src/router/instruction.ts
index 760394c65e..13e4e9f192 100644
--- a/modules/angular2/src/router/instruction.ts
+++ b/modules/angular2/src/router/instruction.ts
@@ -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,74 +103,184 @@ 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;
+
+ /**
+ * 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.
- */
-export class PrimaryInstruction {
- constructor(public component: ComponentInstruction, public child: PrimaryInstruction,
- public auxUrls: Url[]) {}
-}
-
-export function stringifyInstruction(instruction: Instruction): string {
- return stringifyInstructionPath(instruction) + stringifyInstructionQuery(instruction);
-}
-
-export function stringifyInstructionPath(instruction: Instruction): string {
- return instruction.component.urlPath + stringifyAux(instruction) +
- stringifyPrimaryPrefixed(instruction.child);
-}
-
-export function stringifyInstructionQuery(instruction: Instruction): string {
- return instruction.component.urlParams.length > 0 ?
- ('?' + instruction.component.urlParams.join('&')) :
- '';
-}
-
-function stringifyPrimaryPrefixed(instruction: Instruction): string {
- var primary = stringifyPrimary(instruction);
- if (primary.length > 0) {
- primary = '/' + primary;
+ /**
+ * If the final URL for the instruction is ``
+ */
+ toUrlPath(): string {
+ return this.urlPath + this._stringifyAux() +
+ (isPresent(this.child) ? this.child._toNonRootUrl() : '');
}
- return primary;
-}
-function stringifyPrimary(instruction: Instruction): string {
- if (isBlank(instruction)) {
+ // default instructions override these
+ toLinkUrl(): string {
+ return this.urlPath + this._stringifyAux() +
+ (isPresent(this.child) ? this.child._toLinkUrl() : '');
+ }
+
+ // this is the non-root version (called recursively)
+ /** @internal */
+ _toLinkUrl(): string {
+ return this._stringifyPathMatrixAuxPrefixed() +
+ (isPresent(this.child) ? this.child._toLinkUrl() : '');
+ }
+
+ /** @internal */
+ _stringifyPathMatrixAuxPrefixed(): string {
+ var primary = this._stringifyPathMatrixAux();
+ if (primary.length > 0) {
+ primary = '/' + primary;
+ }
+ return primary;
+ }
+
+ /** @internal */
+ _stringifyMatrixParams(): string {
+ return this.urlParams.length > 0 ? (';' + this.component.urlParams.join(';')) : '';
+ }
+
+ /** @internal */
+ _stringifyPathMatrixAux(): string {
+ if (isBlank(this.component)) {
+ return '';
+ }
+ return this.urlPath + this._stringifyMatrixParams() + this._stringifyAux();
+ }
+
+ /** @internal */
+ _stringifyAux(): string {
+ var routes = [];
+ StringMapWrapper.forEach(this.auxInstruction, (auxInstruction, _) => {
+ routes.push(auxInstruction._stringifyPathMatrixAux());
+ });
+ if (routes.length > 0) {
+ return '(' + routes.join('//') + ')';
+ }
return '';
}
- var params = instruction.component.urlParams.length > 0 ?
- (';' + instruction.component.urlParams.join(';')) :
- '';
- return instruction.component.urlPath + params + stringifyAux(instruction) +
- stringifyPrimaryPrefixed(instruction.child);
}
-function stringifyAux(instruction: Instruction): string {
- var routes = [];
- StringMapWrapper.forEach(instruction.auxInstruction, (auxInstruction, _) => {
- routes.push(stringifyPrimary(auxInstruction));
- });
- if (routes.length > 0) {
- return '(' + routes.join('//') + ')';
+
+/**
+ * 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 {
+ 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 {
+ 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, 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 {
+ 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);
}
- return '';
}
@@ -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;
-
- /**
- * 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 { return this._recognizer.handler.resolveComponentType(); }
- get specificity() { return this._recognizer.specificity; }
- get terminal() { return this._recognizer.terminal; }
- get routeData(): RouteData { return this._routeData; }
}
diff --git a/modules/angular2/src/router/path_recognizer.ts b/modules/angular2/src/router/path_recognizer.ts
index 01e84e0cc0..1e03e1271d 100644
--- a/modules/angular2/src/router/path_recognizer.ts
+++ b/modules/angular2/src/router/path_recognizer.ts
@@ -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 = new Map();
-
- // 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};
}
}
diff --git a/modules/angular2/src/router/route_config_impl.ts b/modules/angular2/src/router/route_config_impl.ts
index b56fb0103f..b52de354df 100644
--- a/modules/angular2/src/router/route_config_impl.ts
+++ b/modules/angular2/src/router/route_config_impl.ts
@@ -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;
}
diff --git a/modules/angular2/src/router/route_config_nomalizer.dart b/modules/angular2/src/router/route_config_nomalizer.dart
index 2d3005295b..6fe053e62d 100644
--- a/modules/angular2/src/router/route_config_nomalizer.dart
+++ b/modules/angular2/src/router/route_config_nomalizer.dart
@@ -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;
}
diff --git a/modules/angular2/src/router/route_config_nomalizer.ts b/modules/angular2/src/router/route_config_nomalizer.ts
index 8d0725dbab..16518fa79f 100644
--- a/modules/angular2/src/router/route_config_nomalizer.ts
+++ b/modules/angular2/src/router/route_config_nomalizer.ts
@@ -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 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:config.component, name: config.name});
@@ -36,11 +57,17 @@ export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
return new Route({
path: config.path,
component: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.`);
diff --git a/modules/angular2/src/router/route_definition.dart b/modules/angular2/src/router/route_definition.dart
index e5d0a22539..79c8b5672b 100644
--- a/modules/angular2/src/router/route_definition.dart
+++ b/modules/angular2/src/router/route_definition.dart
@@ -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});
}
diff --git a/modules/angular2/src/router/route_definition.ts b/modules/angular2/src/router/route_definition.ts
index ee1266143d..7d38690ee1 100644
--- a/modules/angular2/src/router/route_definition.ts
+++ b/modules/angular2/src/router/route_definition.ts
@@ -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 {
diff --git a/modules/angular2/src/router/route_handler.ts b/modules/angular2/src/router/route_handler.ts
index 54ca3b2ef0..5971267ee8 100644
--- a/modules/angular2/src/router/route_handler.ts
+++ b/modules/angular2/src/router/route_handler.ts
@@ -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;
- data?: {[key: string]: any};
+ data: RouteData;
}
diff --git a/modules/angular2/src/router/route_recognizer.ts b/modules/angular2/src/router/route_recognizer.ts
index 6511fadda7..fb0b8973e9 100644
--- a/modules/angular2/src/router/route_recognizer.ts
+++ b/modules/angular2/src/router/route_recognizer.ts
@@ -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();
+export abstract class RouteMatch {}
- // map from name to recognizer
- auxNames = new Map();
-
- // map from starting path to recognizer
- auxRoutes = new Map();
-
- // 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;
+ 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;
- }
- }
-
- return urlParse;
- }
-
- recognizeAuxiliary(urlParse: Url): PathMatch {
- var pathRecognizer = this.auxRoutes.get(urlParse.path);
- if (isBlank(pathRecognizer)) {
- return null;
- }
- return pathRecognizer.recognize(urlParse);
- }
-
- hasRoute(name: string): boolean { return this.names.has(name); }
-
- generate(name: string, params: any): ComponentInstruction {
- var pathRecognizer: PathRecognizer = this.names.get(name);
- if (isBlank(pathRecognizer)) {
- return null;
- }
- return pathRecognizer.generate(params);
- }
-
- generateAuxiliary(name: string, params: any): ComponentInstruction {
- var pathRecognizer: PathRecognizer = this.auxNames.get(name);
- if (isBlank(pathRecognizer)) {
- return null;
- }
- return pathRecognizer.generate(params);
+export class PathMatch extends RouteMatch {
+ constructor(public instruction: ComponentInstruction, public remaining: Url,
+ public remainingAux: Url[]) {
+ super();
}
}
-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('/');
+export class RedirectMatch extends RouteMatch {
+ constructor(public redirectTo: any[], public specificity) { super(); }
+}
+
+export class RedirectRecognizer implements AbstractRecognizer {
+ private _pathRecognizer: PathRecognizer;
+ public hash: string;
+
+ constructor(public path: string, public redirectTo: any[]) {
+ this._pathRecognizer = new PathRecognizer(path);
+ this.hash = this._pathRecognizer.hash;
}
/**
* Returns `null` or a `ParsedUrl` representing the new path to match
*/
- redirect(urlParse: Url): Url {
- for (var i = 0; i < this.segments.length; i += 1) {
- if (isBlank(urlParse)) {
- return null;
- }
- let segment = this.segments[i];
- if (segment != urlParse.path) {
- return null;
- }
- urlParse = urlParse.child;
+ recognize(beginningSegment: Url): Promise {
+ var match = null;
+ if (isPresent(this._pathRecognizer.recognize(beginningSegment))) {
+ match = new RedirectMatch(this.redirectTo, this._pathRecognizer.specificity);
}
+ 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 = new Map();
+ 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 {
+ 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;
}
}
diff --git a/modules/angular2/src/router/route_registry.ts b/modules/angular2/src/router/route_registry.ts
index cb57d6408b..7ea7c41daa 100644
--- a/modules/angular2/src/router/route_registry.ts
+++ b/modules/angular2/src/router/route_registry.ts
@@ -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();
+ private _rules = new Map();
+
+ 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 {
+ recognize(url: string, ancestorInstructions: Instruction[]): Promise {
var parsedUrl = parser.parse(url);
- return this._recognize(parsedUrl, parentComponent);
+ return this._recognize(parsedUrl, ancestorInstructions);
}
- private _recognize(parsedUrl: Url, parentComponent): Promise {
- return this._recognizePrimaryRoute(parsedUrl, parentComponent)
- .then((instruction: PrimaryInstruction) =>
- this._completeAuxiliaryRouteMatches(instruction, parentComponent));
- }
- private _recognizePrimaryRoute(parsedUrl: Url, parentComponent): Promise {
+ /**
+ * Recognizes all parent-child routes, but creates unresolved auxiliary routes
+ */
+
+ private _recognize(parsedUrl: Url, ancestorInstructions: Instruction[],
+ _aux = false): Promise {
+ 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[] =
+ _aux ? componentRecognizer.recognizeAuxiliary(parsedUrl) :
+ componentRecognizer.recognize(parsedUrl);
- var matchPromises =
- possibleMatches.map(candidate => this._completePrimaryRouteMatch(candidate));
+ var matchPromises: Promise[] = possibleMatches.map(
+ (candidate: Promise) => 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 {
- 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);
- }
-
- return this._recognizePrimaryRoute(partialMatch.remaining, componentType)
- .then((childInstruction) => {
- if (isBlank(childInstruction)) {
- return null;
- } else {
- return new PrimaryInstruction(instruction, childInstruction,
- partialMatch.remainingAux);
- }
- });
+ auxRoutes.forEach((auxUrl: Url) => {
+ unresolvedAuxInstructions[auxUrl.path] = new UnresolvedInstruction(
+ () => { return this._recognize(auxUrl, parentInstructions, true); });
});
+
+ return unresolvedAuxInstructions;
}
- private _completeAuxiliaryRouteMatches(instruction: PrimaryInstruction,
- parentComponent: any): Promise {
- 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, {});
- }
- return null;
+ 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 new DefaultInstruction(componentInstruction, defaultChild);
}
- return null;
+ return new UnresolvedInstruction(() => {
+ return componentRecognizer.defaultRoute.handler.resolveComponentType().then(
+ () => this.generateDefault(componentCursor));
+ });
}
}
+/*
+ * 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) {
diff --git a/modules/angular2/src/router/router.ts b/modules/angular2/src/router/router.ts
index 5eb4a31ec9..137ac7db36 100644
--- a/modules/angular2/src/router/router.ts
+++ b/modules/angular2/src/router/router.ts
@@ -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 {
- var unsettledInstructions: Array> = [];
- 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 instruction.resolveComponent().then((_) => {
+ instruction.component.reuse = false;
+
+ var unsettledInstructions: Array> = [];
+
+ if (isPresent(instruction.child)) {
+ unsettledInstructions.push(this._settleInstruction(instruction.child));
+ }
+
+ StringMapWrapper.forEach(instruction.auxInstruction, (instruction, _) => {
+ unsettledInstructions.push(this._settleInstruction(instruction));
+ });
+ return PromiseWrapper.all(unsettledInstructions);
});
- 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 {
- 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 {
- 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 {
diff --git a/modules/angular2/src/router/router_link.ts b/modules/angular2/src/router/router_link.ts
index 6ea1e5d6bb..6d380f4ffd 100644
--- a/modules/angular2/src/router/router_link.ts
+++ b/modules/angular2/src/router/router_link.ts
@@ -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);
}
diff --git a/modules/angular2/src/router/sync_route_handler.ts b/modules/angular2/src/router/sync_route_handler.ts
index 5ad7f0aa8d..1b951a098a 100644
--- a/modules/angular2/src/router/sync_route_handler.ts
+++ b/modules/angular2/src/router/sync_route_handler.ts
@@ -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 = 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 { return this._resolvedComponent; }
diff --git a/modules/angular2/test/router/component_recognizer_spec.ts b/modules/angular2/test/router/component_recognizer_spec.ts
new file mode 100644
index 0000000000..5924bf5b73
--- /dev/null
+++ b/modules/angular2/test/router/component_recognizer_spec.ts
@@ -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 {
+ 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 {}
diff --git a/modules/angular2/test/router/integration/README.md b/modules/angular2/test/router/integration/README.md
new file mode 100644
index 0000000000..157d7423d2
--- /dev/null
+++ b/modules/angular2/test/router/integration/README.md
@@ -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`.
\ No newline at end of file
diff --git a/modules/angular2/test/router/integration/async_route_spec.ts b/modules/angular2/test/router/integration/async_route_spec.ts
new file mode 100644
index 0000000000..40b182bfec
--- /dev/null
+++ b/modules/angular2/test/router/integration/async_route_spec.ts
@@ -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); });
+ });
+ });
+}
diff --git a/modules/angular2/test/router/integration/auxiliary_route_spec.ts b/modules/angular2/test/router/integration/auxiliary_route_spec.ts
new file mode 100644
index 0000000000..14615bc466
--- /dev/null
+++ b/modules/angular2/test/router/integration/auxiliary_route_spec.ts
@@ -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 {} | aux {}`)
+ .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 {} | aux {}`)
+ .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,
+ `open modal | main {} | aux {}`)
+ .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,
+ `open modal | main {} | aux {}`)
+ .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 {} | ' +
+ 'aux {}',
+ directives: [ROUTER_DIRECTIVES],
+})
+@RouteConfig([
+ new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
+ new AuxRoute({path: '/modal', component: ModalCmp, name: 'Aux'})
+])
+class AuxCmp {
+}
diff --git a/modules/angular2/test/router/integration/router_integration_spec.ts b/modules/angular2/test/router/integration/bootstrap_spec.ts
similarity index 78%
rename from modules/angular2/test/router/integration/router_integration_spec.ts
rename to modules/angular2/test/router/integration/bootstrap_spec.ts
index 1f0c0c04bb..22fac7e942 100644
--- a/modules/angular2/test/router/integration/router_integration_spec.ts
+++ b/modules/angular2/test/router/integration/bootstrap_spec.ts
@@ -29,53 +29,47 @@ 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 [
- ROUTER_PROVIDERS,
- provide(LocationStrategy, {useClass: MockLocationStrategy}),
- provide(ApplicationRef, {useClass: MockApplicationRef})
- ];
- });
+ 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);
- DOM.appendChild(fakeDoc.body, el);
+ it('should bootstrap a simple app', inject([AsyncTestCompleter], (async) => {
+ var fakeDoc = DOM.createHtmlDocument();
+ var el = DOM.createElement('app-cmp', fakeDoc);
+ DOM.appendChild(fakeDoc.body, el);
- bootstrap(AppCmp,
- [
- ROUTER_PROVIDERS,
- provide(ROUTER_PRIMARY_COMPONENT, {useValue: AppCmp}),
- provide(LocationStrategy, {useClass: MockLocationStrategy}),
- provide(DOCUMENT, {useValue: fakeDoc})
- ])
- .then((applicationRef) => {
- var router = applicationRef.hostComponent.router;
- router.subscribe((_) => {
- expect(el).toHaveText('outer { hello }');
- expect(applicationRef.hostComponent.location.path()).toEqual('');
- async.done();
- });
+ bootstrap(AppCmp,
+ [
+ ROUTER_PROVIDERS,
+ provide(ROUTER_PRIMARY_COMPONENT, {useValue: AppCmp}),
+ provide(LocationStrategy, {useClass: MockLocationStrategy}),
+ provide(DOCUMENT, {useValue: fakeDoc})
+ ])
+ .then((applicationRef) => {
+ var router = applicationRef.hostComponent.router;
+ router.subscribe((_) => {
+ expect(el).toHaveText('outer { hello }');
+ expect(applicationRef.hostComponent.location.path()).toEqual('');
+ async.done();
});
- }));
- });
+ });
+ }));
describe('broken app', () => {
- beforeEachProviders(
- () => { 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 { }", directives: ROUTER_DIRECTIVES})
+@Component({
+ selector: 'app-cmp',
+ template: `outer { }`,
+ 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 { }`, directives: ROUTER_DIRECTIVES})
+@Component({
+ selector: 'parent-cmp',
+ template: `parent { }`,
+ directives: ROUTER_DIRECTIVES
+})
@RouteConfig([new Route({path: '/child', component: HelloCmp})])
class ParentCmp {
}
-@Component({selector: 'super-parent-cmp'})
-@View({template: `super-parent { }`, directives: ROUTER_DIRECTIVES})
+@Component({
+ selector: 'super-parent-cmp',
+ template: `super-parent { }`,
+ directives: ROUTER_DIRECTIVES
+})
@RouteConfig([new Route({path: '/child', component: Hello2Cmp})])
class SuperParentCmp {
}
-@Component({selector: 'app-cmp'})
-@View({template: `root { }`, directives: ROUTER_DIRECTIVES})
+@Component({
+ selector: 'app-cmp',
+ template: `root { }`,
+ 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: ``, directives: ROUTER_DIRECTIVES})
+@Component({
+ selector: 'app-cmp',
+ template: ``,
+ 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 { }`, directives: ROUTER_DIRECTIVES})
+@Component({
+ selector: 'app-cmp',
+ template: `outer { }`,
+ directives: ROUTER_DIRECTIVES
+})
@RouteConfig([new Route({path: '/cause-error', component: BrokenCmp})])
class BrokenAppCmp {
constructor(public router: Router, public location: LocationStrategy) {}
diff --git a/modules/angular2/test/router/integration/impl/async_route_spec_impl.ts b/modules/angular2/test/router/integration/impl/async_route_spec_impl.ts
new file mode 100644
index 0000000000..583e4ecef8
--- /dev/null
+++ b/modules/angular2/test/router/integration/impl/async_route_spec_impl.ts
@@ -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, `go to hello | `)
+ .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, `go to hello | `)
+ .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, `greet naomi | `)
+ .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, `greet naomi | `)
+ .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 { }`)
+ .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 { }`)
+ .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, `nav to child | outer { }`)
+ .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, `nav to child | outer { }`)
+ .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 { }`)
+ .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 { }`)
+ .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, `link to inner | outer { }`)
+ .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, `link to inner | outer { }`)
+ .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 { }`)
+ .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 { }`)
+ .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, `nav to child | outer { }`)
+ .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, `nav to child | outer { }`)
+ .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 { }`)
+ .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 { }`)
+ .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, `nav to child | outer { }`)
+ .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, `nav to child | outer { }`)
+ .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, `{ }`)
+ .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, `{ }`)
+ .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,
+ `nav to matias { }`)
+ .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,
+ `nav to matias { }`)
+ .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;
+}
diff --git a/modules/angular2/test/router/integration/impl/fixture_components.ts b/modules/angular2/test/router/integration/impl/fixture_components.ts
new file mode 100644
index 0000000000..075213bdf6
--- /dev/null
+++ b/modules/angular2/test/router/integration/impl/fixture_components.ts
@@ -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 { }`,
+ 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 { }`,
+ 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 { }`,
+ 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 { }`,
+ 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 { }`,
+ 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 { }`,
+ 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 {
+}
diff --git a/modules/angular2/test/router/integration/impl/sync_route_spec_impl.ts b/modules/angular2/test/router/integration/impl/sync_route_spec_impl.ts
new file mode 100644
index 0000000000..15fbc3514c
--- /dev/null
+++ b/modules/angular2/test/router/integration/impl/sync_route_spec_impl.ts
@@ -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, `go to hello | `)
+ .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, `go to hello | `)
+ .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, `greet naomi | `)
+ .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, `greet naomi | `)
+ .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 { }`)
+ .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 { }`)
+ .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, `nav to child | outer { }`)
+ .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, `nav to child | outer { }`)
+ .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, `{ }`)
+ .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, `{ }`)
+ .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,
+ `nav to matias { }`)
+ .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,
+ `nav to matias { }`)
+ .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 { }`)
+ .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 { }`)
+ .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, `link to inner | outer { }`)
+ .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, `link to inner | outer { }`)
+ .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;
+}
diff --git a/modules/angular2/test/router/integration/lifecycle_hook_spec.ts b/modules/angular2/test/router/integration/lifecycle_hook_spec.ts
index 57e5d4f533..5130f2c090 100644
--- a/modules/angular2/test/router/integration/lifecycle_hook_spec.ts
+++ b/modules/angular2/test/router/integration/lifecycle_hook_spec.ts
@@ -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 = "") {
- return tcb.overrideView(MyComp, new View({
- template: ('' + template + '
'),
- 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(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(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(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 {}`, directives: [RouterOutlet]})
+@Component({
+ selector: 'parent-activate-cmp',
+ template: `parent {}`,
+ directives: [RouterOutlet]
+})
@RouteConfig([new Route({path: '/child-activate', component: ActivateCmp})])
class ParentActivateCmp implements OnActivate {
onActivate(next: ComponentInstruction, prev: ComponentInstruction): Promise {
@@ -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 {
completer = PromiseWrapper.completer();
@@ -444,8 +428,11 @@ class WaitDeactivateCmp implements OnDeactivate {
}
}
-@Component({selector: 'parent-deactivate-cmp'})
-@View({template: `parent {}`, directives: [RouterOutlet]})
+@Component({
+ selector: 'parent-deactivate-cmp',
+ template: `parent {}`,
+ 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 {}`, directives: [RouterOutlet]})
+@Component({
+ selector: 'reuse-cmp',
+ template: `reuse {}`,
+ 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 {}`, directives: [RouterOutlet]})
+@Component({
+ selector: 'never-reuse-cmp',
+ template: `reuse {}`,
+ 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 {}`, directives: [RouterOutlet]})
+@Component({
+ selector: 'can-activate-cmp',
+ template: `canActivate {}`,
+ 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 {}`, directives: [RouterOutlet]})
+@Component({
+ selector: 'can-deactivate-cmp',
+ template: `canDeactivate {}`,
+ 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 {
@@ -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: ``, directives: [RouterOutlet]})
+@Component({
+ selector: 'all-hooks-parent-cmp',
+ template: ``,
+ 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 {
@@ -574,8 +577,11 @@ class ReuseHooksCmp implements OnActivate, OnReuse, OnDeactivate, CanReuse, CanD
}
}
-@Component({selector: 'lifecycle-cmp'})
-@View({template: ``, directives: [RouterOutlet]})
+@Component({
+ selector: 'lifecycle-cmp',
+ template: ``,
+ directives: [RouterOutlet]
+})
@RouteConfig([
new Route({path: '/a', component: A}),
new Route({path: '/on-activate', component: ActivateCmp}),
diff --git a/modules/angular2/test/router/integration/navigation_spec.ts b/modules/angular2/test/router/integration/navigation_spec.ts
index cb420974e6..8e649ec9c2 100644
--- a/modules/angular2/test/router/integration/navigation_spec.ts
+++ b/modules/angular2/test/router/integration/navigation_spec.ts
@@ -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 = "") {
- return tcb.overrideView(MyComp, new View({
- template: ('' + template + '
'),
- 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 { }')
+ compile(tcb, 'outer { }')
+ .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 { }')
+
+ compile(tcb, 'outer { }')
+ .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 { }')
+ compile(tcb, 'outer { }')
+ .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 { }",
+@Component({
+ selector: 'parent-cmp',
+ template: `inner { }`,
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}} { }",
+@Component({
+ selector: 'team-cmp',
+ template: `team {{id}} { }`,
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 {} | ' +
- 'aux {}',
- directives: [RouterOutlet],
-})
-@RouteConfig([
- new Route({path: '/hello', component: HelloCmp}),
- new AuxRoute({path: '/modal', component: ModalCmp}),
-])
-class AuxCmp {
-}
diff --git a/modules/angular2/test/router/integration/redirect_route_spec.ts b/modules/angular2/test/router/integration/redirect_route_spec.ts
new file mode 100644
index 0000000000..7f8bb28a92
--- /dev/null
+++ b/modules/angular2/test/router/integration/redirect_route_spec.ts
@@ -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();
+ });
+ }));
+ });
+}
diff --git a/modules/angular2/test/router/integration/router_link_spec.ts b/modules/angular2/test/router/integration/router_link_spec.ts
index 5b76fd538b..6a074c06b6 100644
--- a/modules/angular2/test/router/integration/router_link_spec.ts
+++ b/modules/angular2/test/router/integration/router_link_spec.ts
@@ -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({
diff --git a/modules/angular2/test/router/integration/sync_route_spec.ts b/modules/angular2/test/router/integration/sync_route_spec.ts
new file mode 100644
index 0000000000..12a4d339fe
--- /dev/null
+++ b/modules/angular2/test/router/integration/sync_route_spec.ts
@@ -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); });
+
+ });
+ });
+}
diff --git a/modules/angular2/test/router/integration/util.ts b/modules/angular2/test/router/integration/util.ts
new file mode 100644
index 0000000000..08abbf3438
--- /dev/null
+++ b/modules/angular2/test/router/integration/util.ts
@@ -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: ``,
+ directives: [ROUTER_DIRECTIVES]
+})
+export class RootCmp {
+ name: string;
+}
+
+export function compile(tcb: TestComponentBuilder,
+ template: string = "") {
+ return tcb.overrideTemplate(RootCmp, ('' + template + '
')).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);
+}
diff --git a/modules/angular2/test/router/path_recognizer_spec.ts b/modules/angular2/test/router/path_recognizer_spec.ts
index 979c21a7d0..08fddc1044 100644
--- a/modules/angular2/test/router/path_recognizer_spec.ts
+++ b/modules/angular2/test/router/path_recognizer_spec.ts
@@ -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'});
});
});
});
diff --git a/modules/angular2/test/router/route_config_spec.ts b/modules/angular2/test/router/route_config_spec.ts
index ac64143273..6094178ac4 100644
--- a/modules/angular2/test/router/route_config_spec.ts
+++ b/modules/angular2/test/router/route_config_spec.ts
@@ -214,7 +214,10 @@ class HelloCmp {
@Component({selector: 'app-cmp'})
@View({template: `root { }`, 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) {}
}
diff --git a/modules/angular2/test/router/route_recognizer_spec.ts b/modules/angular2/test/router/route_recognizer_spec.ts
deleted file mode 100644
index 99426fd461..0000000000
--- a/modules/angular2/test/router/route_recognizer_spec.ts
+++ /dev/null
@@ -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 {}
diff --git a/modules/angular2/test/router/route_registry_spec.ts b/modules/angular2/test/router/route_registry_spec.ts
index 3b906bb749..423a88d264 100644
--- a/modules/angular2/test/router/route_registry_spec.ts
+++ b/modules/angular2/test/router/route_registry_spec.ts
@@ -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,11 +196,14 @@ 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);
- async.done();
+
+ instruction.child.resolveComponent().then((childComponentInstruction) => {
+ expect(childComponentInstruction.componentType).toBe(DummyCmpB);
+ async.done();
+ });
});
}));
@@ -203,11 +212,14 @@ 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);
- async.done();
+
+ instruction.child.resolveComponent().then((childType) => {
+ expect(childType.componentType).toBe(DummyCmpB);
+ async.done();
+ });
});
}));
@@ -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'})])
diff --git a/modules/angular2/test/router/router_link_spec.ts b/modules/angular2/test/router/router_link_spec.ts
index 49c90696fc..46af69f37a 100644
--- a/modules/angular2/test/router/router_link_spec.ts
+++ b/modules/angular2/test/router/router_link_spec.ts
@@ -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 {
diff --git a/modules/angular2/test/router/router_spec.ts b/modules/angular2/test/router/router_spec.ts
index 1fb2a3fb11..af3190e623 100644
--- a/modules/angular2/test/router/router_spec.ts
+++ b/modules/angular2/test/router/router_spec.ts
@@ -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 {
return PromiseWrapper.resolve(DummyComponent);
}
diff --git a/tools/broccoli/trees/node_tree.ts b/tools/broccoli/trees/node_tree.ts
index da6e6647eb..29d911ddf2 100644
--- a/tools/broccoli/trees/node_tree.ts
+++ b/tools/broccoli/trees/node_tree.ts
@@ -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',