refactor(router): improve recognition and generation pipeline
This is a big change. @matsko also deserves much of the credit for the implementation.
Previously, `ComponentInstruction`s held all the state for async components.
Now, we introduce several subclasses for `Instruction` to describe each type of navigation.
BREAKING CHANGE:
Redirects now use the Link DSL syntax. Before:
```
@RouteConfig([
{ path: '/foo', redirectTo: '/bar' },
{ path: '/bar', component: BarCmp }
])
```
After:
```
@RouteConfig([
{ path: '/foo', redirectTo: ['Bar'] },
{ path: '/bar', component: BarCmp, name: 'Bar' }
])
```
BREAKING CHANGE:
This also introduces `useAsDefault` in the RouteConfig, which makes cases like lazy-loading
and encapsulating large routes with sub-routes easier.
Previously, you could use `redirectTo` like this to expand a URL like `/tab` to `/tab/posts`:
@RouteConfig([
{ path: '/tab', redirectTo: '/tab/users' }
{ path: '/tab', component: TabsCmp, name: 'Tab' }
])
AppCmp { ... }
Now the recommended way to handle this is case is to use `useAsDefault` like so:
```
@RouteConfig([
{ path: '/tab', component: TabsCmp, name: 'Tab' }
])
AppCmp { ... }
@RouteConfig([
{ path: '/posts', component: PostsCmp, useAsDefault: true, name: 'Posts' },
{ path: '/users', component: UsersCmp, name: 'Users' }
])
TabsCmp { ... }
```
In the above example, you can write just `['/Tab']` and the route `Users` is automatically selected as a child route.
Closes #4170
Closes #4490
Closes #4694
Closes #5200
Closes #5352
This commit is contained in:
Vendored
+3
-2
@@ -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',
|
||||
|
||||
@@ -173,6 +173,10 @@ var StringMapWrapper = {
|
||||
|
||||
var List = Array;
|
||||
var ListWrapper = {
|
||||
clear: function (l) {
|
||||
l.length = 0;
|
||||
},
|
||||
|
||||
create: function () {
|
||||
return [];
|
||||
},
|
||||
|
||||
+3
-1
@@ -31,7 +31,9 @@ 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;
|
||||
|
||||
+2
-2
@@ -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;
|
||||
|
||||
|
||||
+8
-10
@@ -113,8 +113,7 @@ describe('navigation', function () {
|
||||
});
|
||||
|
||||
|
||||
// TODO: fix this
|
||||
xit('should work with recursive nested outlets', function () {
|
||||
it('should work with recursive nested outlets', function () {
|
||||
registerComponent('recurCmp', {
|
||||
template: '<div>recur { <div ng-outlet></div> }</div>',
|
||||
$routeConfig: [
|
||||
@@ -152,8 +151,8 @@ describe('navigation', function () {
|
||||
compile('<div ng-outlet></div>');
|
||||
|
||||
$router.config([
|
||||
{ path: '/', redirectTo: '/user' },
|
||||
{ path: '/user', component: 'userCmp' }
|
||||
{ path: '/', redirectTo: ['/User'] },
|
||||
{ path: '/user', component: 'userCmp', name: 'User' }
|
||||
]);
|
||||
|
||||
$router.navigateByUrl('/');
|
||||
@@ -167,16 +166,15 @@ describe('navigation', function () {
|
||||
registerComponent('childRouter', {
|
||||
template: '<div>inner { <div ng-outlet></div> }</div>',
|
||||
$routeConfig: [
|
||||
{ path: '/old-child', redirectTo: '/new-child' },
|
||||
{ path: '/new-child', component: 'oneCmp'},
|
||||
{ path: '/old-child-two', redirectTo: '/new-child-two' },
|
||||
{ path: '/new-child-two', component: 'twoCmp'}
|
||||
{ path: '/new-child', component: 'oneCmp', name: 'NewChild'},
|
||||
{ path: '/new-child-two', component: 'twoCmp', name: 'NewChildTwo'}
|
||||
]
|
||||
});
|
||||
|
||||
$router.config([
|
||||
{ path: '/old-parent', redirectTo: '/new-parent' },
|
||||
{ path: '/new-parent/...', component: 'childRouter' }
|
||||
{ path: '/old-parent/old-child', redirectTo: ['/NewParent', 'NewChild'] },
|
||||
{ path: '/old-parent/old-child-two', redirectTo: ['/NewParent', 'NewChildTwo'] },
|
||||
{ path: '/new-parent/...', component: 'childRouter', name: 'NewParent' }
|
||||
]);
|
||||
|
||||
compile('<div ng-outlet></div>');
|
||||
|
||||
+4
-3
@@ -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();
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import {RouteHandler} from './route_handler';
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
import {isPresent, Type} from 'angular2/src/facade/lang';
|
||||
|
||||
import {RouteHandler} from './route_handler';
|
||||
import {RouteData, BLANK_ROUTE_DATA} from './instruction';
|
||||
|
||||
|
||||
export class AsyncRouteHandler implements RouteHandler {
|
||||
/** @internal */
|
||||
_resolvedComponent: Promise<any> = null;
|
||||
componentType: Type;
|
||||
public data: RouteData;
|
||||
|
||||
constructor(private _loader: Function, public data?: {[key: string]: any}) {}
|
||||
constructor(private _loader: Function, data: {[key: string]: any} = null) {
|
||||
this.data = isPresent(data) ? new RouteData(data) : BLANK_ROUTE_DATA;
|
||||
}
|
||||
|
||||
resolveComponentType(): Promise<any> {
|
||||
if (isPresent(this._resolvedComponent)) {
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
import {isBlank, isPresent} from 'angular2/src/facade/lang';
|
||||
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
|
||||
import {Map, MapWrapper, ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
|
||||
import {
|
||||
AbstractRecognizer,
|
||||
RouteRecognizer,
|
||||
RedirectRecognizer,
|
||||
RouteMatch
|
||||
} from './route_recognizer';
|
||||
import {Route, AsyncRoute, AuxRoute, Redirect, RouteDefinition} from './route_config_impl';
|
||||
import {AsyncRouteHandler} from './async_route_handler';
|
||||
import {SyncRouteHandler} from './sync_route_handler';
|
||||
import {Url} from './url_parser';
|
||||
import {ComponentInstruction} from './instruction';
|
||||
|
||||
|
||||
/**
|
||||
* `ComponentRecognizer` is responsible for recognizing routes for a single component.
|
||||
* It is consumed by `RouteRegistry`, which knows how to recognize an entire hierarchy of
|
||||
* components.
|
||||
*/
|
||||
export class ComponentRecognizer {
|
||||
names = new Map<string, RouteRecognizer>();
|
||||
|
||||
// map from name to recognizer
|
||||
auxNames = new Map<string, RouteRecognizer>();
|
||||
|
||||
// map from starting path to recognizer
|
||||
auxRoutes = new Map<string, RouteRecognizer>();
|
||||
|
||||
// TODO: optimize this into a trie
|
||||
matchers: AbstractRecognizer[] = [];
|
||||
|
||||
defaultRoute: RouteRecognizer = null;
|
||||
|
||||
/**
|
||||
* returns whether or not the config is terminal
|
||||
*/
|
||||
config(config: RouteDefinition): boolean {
|
||||
var handler;
|
||||
|
||||
if (isPresent(config.name) && config.name[0].toUpperCase() != config.name[0]) {
|
||||
var suggestedName = config.name[0].toUpperCase() + config.name.substring(1);
|
||||
throw new BaseException(
|
||||
`Route "${config.path}" with name "${config.name}" does not begin with an uppercase letter. Route names should be CamelCase like "${suggestedName}".`);
|
||||
}
|
||||
|
||||
if (config instanceof AuxRoute) {
|
||||
handler = new SyncRouteHandler(config.component, config.data);
|
||||
let path = config.path.startsWith('/') ? config.path.substring(1) : config.path;
|
||||
var recognizer = new RouteRecognizer(config.path, handler);
|
||||
this.auxRoutes.set(path, recognizer);
|
||||
if (isPresent(config.name)) {
|
||||
this.auxNames.set(config.name, recognizer);
|
||||
}
|
||||
return recognizer.terminal;
|
||||
}
|
||||
|
||||
var useAsDefault = false;
|
||||
|
||||
if (config instanceof Redirect) {
|
||||
let redirector = new RedirectRecognizer(config.path, config.redirectTo);
|
||||
this._assertNoHashCollision(redirector.hash, config.path);
|
||||
this.matchers.push(redirector);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (config instanceof Route) {
|
||||
handler = new SyncRouteHandler(config.component, config.data);
|
||||
useAsDefault = isPresent(config.useAsDefault) && config.useAsDefault;
|
||||
} else if (config instanceof AsyncRoute) {
|
||||
handler = new AsyncRouteHandler(config.loader, config.data);
|
||||
useAsDefault = isPresent(config.useAsDefault) && config.useAsDefault;
|
||||
}
|
||||
var recognizer = new RouteRecognizer(config.path, handler);
|
||||
|
||||
this._assertNoHashCollision(recognizer.hash, config.path);
|
||||
|
||||
if (useAsDefault) {
|
||||
if (isPresent(this.defaultRoute)) {
|
||||
throw new BaseException(`Only one route can be default`);
|
||||
}
|
||||
this.defaultRoute = recognizer;
|
||||
}
|
||||
|
||||
this.matchers.push(recognizer);
|
||||
if (isPresent(config.name)) {
|
||||
this.names.set(config.name, recognizer);
|
||||
}
|
||||
return recognizer.terminal;
|
||||
}
|
||||
|
||||
|
||||
private _assertNoHashCollision(hash: string, path) {
|
||||
this.matchers.forEach((matcher) => {
|
||||
if (hash == matcher.hash) {
|
||||
throw new BaseException(
|
||||
`Configuration '${path}' conflicts with existing route '${matcher.path}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a URL, returns a list of `RouteMatch`es, which are partial recognitions for some route.
|
||||
*/
|
||||
recognize(urlParse: Url): Promise<RouteMatch>[] {
|
||||
var solutions = [];
|
||||
|
||||
this.matchers.forEach((routeRecognizer: AbstractRecognizer) => {
|
||||
var pathMatch = routeRecognizer.recognize(urlParse);
|
||||
|
||||
if (isPresent(pathMatch)) {
|
||||
solutions.push(pathMatch);
|
||||
}
|
||||
});
|
||||
|
||||
return solutions;
|
||||
}
|
||||
|
||||
recognizeAuxiliary(urlParse: Url): Promise<RouteMatch>[] {
|
||||
var routeRecognizer: RouteRecognizer = this.auxRoutes.get(urlParse.path);
|
||||
if (isPresent(routeRecognizer)) {
|
||||
return [routeRecognizer.recognize(urlParse)];
|
||||
}
|
||||
|
||||
return [PromiseWrapper.resolve(null)];
|
||||
}
|
||||
|
||||
hasRoute(name: string): boolean { return this.names.has(name); }
|
||||
|
||||
componentLoaded(name: string): boolean {
|
||||
return this.hasRoute(name) && isPresent(this.names.get(name).handler.componentType);
|
||||
}
|
||||
|
||||
loadComponent(name: string): Promise<any> {
|
||||
return this.names.get(name).handler.resolveComponentType();
|
||||
}
|
||||
|
||||
generate(name: string, params: any): ComponentInstruction {
|
||||
var pathRecognizer: RouteRecognizer = this.names.get(name);
|
||||
if (isBlank(pathRecognizer)) {
|
||||
return null;
|
||||
}
|
||||
return pathRecognizer.generate(params);
|
||||
}
|
||||
|
||||
generateAuxiliary(name: string, params: any): ComponentInstruction {
|
||||
var pathRecognizer: RouteRecognizer = this.auxNames.get(name);
|
||||
if (isBlank(pathRecognizer)) {
|
||||
return null;
|
||||
}
|
||||
return pathRecognizer.generate(params);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,12 +7,9 @@ import {
|
||||
isBlank
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
|
||||
|
||||
import {Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {RouteHandler} from './route_handler';
|
||||
import {Url, RootUrl, serializeParams} from './url_parser';
|
||||
import {ComponentInstruction, ComponentInstruction_} from './instruction';
|
||||
|
||||
class TouchMap {
|
||||
map: {[key: string]: string} = {};
|
||||
@@ -33,7 +30,7 @@ class TouchMap {
|
||||
}
|
||||
|
||||
getUnused(): {[key: string]: any} {
|
||||
var unused: {[key: string]: any} = StringMapWrapper.create();
|
||||
var unused: {[key: string]: any} = {};
|
||||
var keys = StringMapWrapper.keys(this.keys);
|
||||
keys.forEach(key => unused[key] = StringMapWrapper.get(this.map, key));
|
||||
return unused;
|
||||
@@ -126,7 +123,6 @@ function parsePathString(route: string): {[key: string]: any} {
|
||||
results.push(new StarSegment(match[1]));
|
||||
} else if (segment == '...') {
|
||||
if (i < limit) {
|
||||
// TODO (matsko): setup a proper error here `
|
||||
throw new BaseException(`Unexpected "..." before the end of the path for "${route}".`);
|
||||
}
|
||||
results.push(new ContinuationSegment());
|
||||
@@ -175,23 +171,17 @@ function assertPath(path: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export class PathMatch {
|
||||
constructor(public instruction: ComponentInstruction, public remaining: Url,
|
||||
public remainingAux: Url[]) {}
|
||||
}
|
||||
|
||||
// represents something like '/foo/:bar'
|
||||
/**
|
||||
* Parses a URL string using a given matcher DSL, and generates URLs from param maps
|
||||
*/
|
||||
export class PathRecognizer {
|
||||
private _segments: Segment[];
|
||||
specificity: number;
|
||||
terminal: boolean = true;
|
||||
hash: string;
|
||||
private _cache: Map<string, ComponentInstruction> = new Map<string, ComponentInstruction>();
|
||||
|
||||
|
||||
// TODO: cache component instruction instances by params and by ParsedUrl instance
|
||||
|
||||
constructor(public path: string, public handler: RouteHandler) {
|
||||
constructor(public path: string) {
|
||||
assertPath(path);
|
||||
var parsed = parsePathString(path);
|
||||
|
||||
@@ -203,8 +193,7 @@ export class PathRecognizer {
|
||||
this.terminal = !(lastSegment instanceof ContinuationSegment);
|
||||
}
|
||||
|
||||
|
||||
recognize(beginningSegment: Url): PathMatch {
|
||||
recognize(beginningSegment: Url): {[key: string]: any} {
|
||||
var nextSegment = beginningSegment;
|
||||
var currentSegment: Url;
|
||||
var positionalParams = {};
|
||||
@@ -247,7 +236,6 @@ export class PathRecognizer {
|
||||
var urlPath = captured.join('/');
|
||||
|
||||
var auxiliary;
|
||||
var instruction: ComponentInstruction;
|
||||
var urlParams;
|
||||
var allParams;
|
||||
if (isPresent(currentSegment)) {
|
||||
@@ -267,12 +255,11 @@ export class PathRecognizer {
|
||||
auxiliary = [];
|
||||
urlParams = [];
|
||||
}
|
||||
instruction = this._getInstruction(urlPath, urlParams, this, allParams);
|
||||
return new PathMatch(instruction, nextSegment, auxiliary);
|
||||
return {urlPath, urlParams, allParams, auxiliary, nextSegment};
|
||||
}
|
||||
|
||||
|
||||
generate(params: {[key: string]: any}): ComponentInstruction {
|
||||
generate(params: {[key: string]: any}): {[key: string]: any} {
|
||||
var paramTokens = new TouchMap(params);
|
||||
|
||||
var path = [];
|
||||
@@ -288,18 +275,6 @@ export class PathRecognizer {
|
||||
var nonPositionalParams = paramTokens.getUnused();
|
||||
var urlParams = serializeParams(nonPositionalParams);
|
||||
|
||||
return this._getInstruction(urlPath, urlParams, this, params);
|
||||
}
|
||||
|
||||
private _getInstruction(urlPath: string, urlParams: string[], _recognizer: PathRecognizer,
|
||||
params: {[key: string]: any}): ComponentInstruction {
|
||||
var hashKey = urlPath + '?' + urlParams.join('?');
|
||||
if (this._cache.has(hashKey)) {
|
||||
return this._cache.get(hashKey);
|
||||
}
|
||||
var instruction = new ComponentInstruction_(urlPath, urlParams, _recognizer, params);
|
||||
this._cache.set(hashKey, instruction);
|
||||
|
||||
return instruction;
|
||||
return {urlPath, urlParams};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,8 @@ export class RouteConfig {
|
||||
* - `name` is an optional `CamelCase` string representing the name of the route.
|
||||
* - `data` is an optional property of any type representing arbitrary route metadata for the given
|
||||
* route. It is injectable via {@link RouteData}.
|
||||
* - `useAsDefault` is a boolean value. If `true`, the child route will be navigated to if no child
|
||||
* route is specified during the navigation.
|
||||
*
|
||||
* ### Example
|
||||
* ```
|
||||
@@ -38,16 +40,20 @@ export class Route implements RouteDefinition {
|
||||
path: string;
|
||||
component: Type;
|
||||
name: string;
|
||||
useAsDefault: boolean;
|
||||
// added next three properties to work around https://github.com/Microsoft/TypeScript/issues/4107
|
||||
aux: string = null;
|
||||
loader: Function = null;
|
||||
redirectTo: string = null;
|
||||
constructor({path, component, name,
|
||||
data}: {path: string, component: Type, name?: string, data?: {[key: string]: any}}) {
|
||||
redirectTo: any[] = null;
|
||||
constructor({path, component, name, data, useAsDefault}: {
|
||||
path: string,
|
||||
component: Type, name?: string, data?: {[key: string]: any}, useAsDefault?: boolean
|
||||
}) {
|
||||
this.path = path;
|
||||
this.component = component;
|
||||
this.name = name;
|
||||
this.data = data;
|
||||
this.useAsDefault = useAsDefault;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +86,8 @@ export class AuxRoute implements RouteDefinition {
|
||||
// added next three properties to work around https://github.com/Microsoft/TypeScript/issues/4107
|
||||
aux: string = null;
|
||||
loader: Function = null;
|
||||
redirectTo: string = null;
|
||||
redirectTo: any[] = null;
|
||||
useAsDefault: boolean = false;
|
||||
constructor({path, component, name}: {path: string, component: Type, name?: string}) {
|
||||
this.path = path;
|
||||
this.component = component;
|
||||
@@ -98,6 +105,8 @@ export class AuxRoute implements RouteDefinition {
|
||||
* - `name` is an optional `CamelCase` string representing the name of the route.
|
||||
* - `data` is an optional property of any type representing arbitrary route metadata for the given
|
||||
* route. It is injectable via {@link RouteData}.
|
||||
* - `useAsDefault` is a boolean value. If `true`, the child route will be navigated to if no child
|
||||
* route is specified during the navigation.
|
||||
*
|
||||
* ### Example
|
||||
* ```
|
||||
@@ -115,31 +124,37 @@ export class AsyncRoute implements RouteDefinition {
|
||||
path: string;
|
||||
loader: Function;
|
||||
name: string;
|
||||
useAsDefault: boolean;
|
||||
aux: string = null;
|
||||
constructor({path, loader, name, data}:
|
||||
{path: string, loader: Function, name?: string, data?: {[key: string]: any}}) {
|
||||
constructor({path, loader, name, data, useAsDefault}: {
|
||||
path: string,
|
||||
loader: Function, name?: string, data?: {[key: string]: any}, useAsDefault?: boolean
|
||||
}) {
|
||||
this.path = path;
|
||||
this.loader = loader;
|
||||
this.name = name;
|
||||
this.data = data;
|
||||
this.useAsDefault = useAsDefault;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `Redirect` is a type of {@link RouteDefinition} used to route a path to an asynchronously loaded
|
||||
* component.
|
||||
* `Redirect` is a type of {@link RouteDefinition} used to route a path to a canonical route.
|
||||
*
|
||||
* It has the following properties:
|
||||
* - `path` is a string that uses the route matcher DSL.
|
||||
* - `redirectTo` is a string representing the new URL to be matched against.
|
||||
* - `redirectTo` is an array representing the link DSL.
|
||||
*
|
||||
* Note that redirects **do not** affect how links are generated. For that, see the `useAsDefault`
|
||||
* option.
|
||||
*
|
||||
* ### Example
|
||||
* ```
|
||||
* import {RouteConfig} from 'angular2/router';
|
||||
*
|
||||
* @RouteConfig([
|
||||
* {path: '/', redirectTo: '/home'},
|
||||
* {path: '/home', component: HomeCmp}
|
||||
* {path: '/', redirectTo: ['/Home'] },
|
||||
* {path: '/home', component: HomeCmp, name: 'Home'}
|
||||
* ])
|
||||
* class MyApp {}
|
||||
* ```
|
||||
@@ -147,13 +162,14 @@ export class AsyncRoute implements RouteDefinition {
|
||||
@CONST()
|
||||
export class Redirect implements RouteDefinition {
|
||||
path: string;
|
||||
redirectTo: string;
|
||||
redirectTo: any[];
|
||||
name: string = null;
|
||||
// added next three properties to work around https://github.com/Microsoft/TypeScript/issues/4107
|
||||
loader: Function = null;
|
||||
data: any = null;
|
||||
aux: string = null;
|
||||
constructor({path, redirectTo}: {path: string, redirectTo: string}) {
|
||||
useAsDefault: boolean = false;
|
||||
constructor({path, redirectTo}: {path: string, redirectTo: any[]}) {
|
||||
this.path = path;
|
||||
this.redirectTo = redirectTo;
|
||||
}
|
||||
|
||||
@@ -1,9 +1,22 @@
|
||||
library angular2.src.router.route_config_normalizer;
|
||||
|
||||
import "route_config_decorator.dart";
|
||||
import "route_registry.dart";
|
||||
import "package:angular2/src/facade/exceptions.dart" show BaseException;
|
||||
|
||||
RouteDefinition normalizeRouteConfig(RouteDefinition config) {
|
||||
RouteDefinition normalizeRouteConfig(RouteDefinition config, RouteRegistry registry) {
|
||||
if (config is AsyncRoute) {
|
||||
|
||||
configRegistryAndReturnType(componentType) {
|
||||
registry.configFromComponent(componentType);
|
||||
return componentType;
|
||||
}
|
||||
|
||||
loader() {
|
||||
return config.loader().then(configRegistryAndReturnType);
|
||||
}
|
||||
return new AsyncRoute(path: config.path, loader: loader, name: config.name, data: config.data, useAsDefault: config.useAsDefault);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,14 +2,29 @@ import {AsyncRoute, AuxRoute, Route, Redirect, RouteDefinition} from './route_co
|
||||
import {ComponentDefinition} from './route_definition';
|
||||
import {isType, Type} from 'angular2/src/facade/lang';
|
||||
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
|
||||
import {RouteRegistry} from './route_registry';
|
||||
|
||||
|
||||
/**
|
||||
* Given a JS Object that represents... returns a corresponding Route, AsyncRoute, or Redirect
|
||||
* Given a JS Object that represents a route config, returns a corresponding Route, AsyncRoute,
|
||||
* AuxRoute or Redirect object.
|
||||
*
|
||||
* Also wraps an AsyncRoute's loader function to add the loaded component's route config to the
|
||||
* `RouteRegistry`.
|
||||
*/
|
||||
export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
||||
if (config instanceof Route || config instanceof Redirect || config instanceof AsyncRoute ||
|
||||
config instanceof AuxRoute) {
|
||||
export function normalizeRouteConfig(config: RouteDefinition,
|
||||
registry: RouteRegistry): RouteDefinition {
|
||||
if (config instanceof AsyncRoute) {
|
||||
var wrappedLoader = wrapLoaderToReconfigureRegistry(config.loader, registry);
|
||||
return new AsyncRoute({
|
||||
path: config.path,
|
||||
loader: wrappedLoader,
|
||||
name: config.name,
|
||||
data: config.data,
|
||||
useAsDefault: config.useAsDefault
|
||||
});
|
||||
}
|
||||
if (config instanceof Route || config instanceof Redirect || config instanceof AuxRoute) {
|
||||
return <RouteDefinition>config;
|
||||
}
|
||||
|
||||
@@ -24,7 +39,13 @@ export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
||||
config.name = config.as;
|
||||
}
|
||||
if (config.loader) {
|
||||
return new AsyncRoute({path: config.path, loader: config.loader, name: config.name});
|
||||
var wrappedLoader = wrapLoaderToReconfigureRegistry(config.loader, registry);
|
||||
return new AsyncRoute({
|
||||
path: config.path,
|
||||
loader: wrappedLoader,
|
||||
name: config.name,
|
||||
useAsDefault: config.useAsDefault
|
||||
});
|
||||
}
|
||||
if (config.aux) {
|
||||
return new AuxRoute({path: config.aux, component:<Type>config.component, name: config.name});
|
||||
@@ -36,11 +57,17 @@ export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
||||
return new Route({
|
||||
path: config.path,
|
||||
component:<Type>componentDefinitionObject.constructor,
|
||||
name: config.name
|
||||
name: config.name,
|
||||
data: config.data,
|
||||
useAsDefault: config.useAsDefault
|
||||
});
|
||||
} else if (componentDefinitionObject.type == 'loader') {
|
||||
return new AsyncRoute(
|
||||
{path: config.path, loader: componentDefinitionObject.loader, name: config.name});
|
||||
return new AsyncRoute({
|
||||
path: config.path,
|
||||
loader: componentDefinitionObject.loader,
|
||||
name: config.name,
|
||||
useAsDefault: config.useAsDefault
|
||||
});
|
||||
} else {
|
||||
throw new BaseException(
|
||||
`Invalid component type "${componentDefinitionObject.type}". Valid types are "constructor" and "loader".`);
|
||||
@@ -50,6 +77,8 @@ export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
||||
path: string;
|
||||
component: Type;
|
||||
name?: string;
|
||||
data?: {[key: string]: any};
|
||||
useAsDefault?: boolean;
|
||||
}>config);
|
||||
}
|
||||
|
||||
@@ -60,6 +89,16 @@ export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
function wrapLoaderToReconfigureRegistry(loader: Function, registry: RouteRegistry): Function {
|
||||
return () => {
|
||||
return loader().then((componentType) => {
|
||||
registry.configFromComponent(componentType);
|
||||
return componentType;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function assertComponentExists(component: Type, path: string): void {
|
||||
if (!isType(component)) {
|
||||
throw new BaseException(`Component for route "${path}" is not defined, or is not a class.`);
|
||||
|
||||
@@ -3,5 +3,6 @@ library angular2.src.router.route_definition;
|
||||
abstract class RouteDefinition {
|
||||
final String path;
|
||||
final String name;
|
||||
const RouteDefinition({this.path, this.name});
|
||||
final bool useAsDefault;
|
||||
const RouteDefinition({this.path, this.name, this.useAsDefault : false});
|
||||
}
|
||||
|
||||
@@ -16,10 +16,11 @@ export interface RouteDefinition {
|
||||
aux?: string;
|
||||
component?: Type | ComponentDefinition;
|
||||
loader?: Function;
|
||||
redirectTo?: string;
|
||||
redirectTo?: any[];
|
||||
as?: string;
|
||||
name?: string;
|
||||
data?: any;
|
||||
useAsDefault?: boolean;
|
||||
}
|
||||
|
||||
export interface ComponentDefinition {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
import {Type} from 'angular2/src/facade/lang';
|
||||
import {RouteData} from './instruction';
|
||||
|
||||
export interface RouteHandler {
|
||||
componentType: Type;
|
||||
resolveComponentType(): Promise<any>;
|
||||
data?: {[key: string]: any};
|
||||
data: RouteData;
|
||||
}
|
||||
|
||||
@@ -1,184 +1,119 @@
|
||||
import {
|
||||
RegExp,
|
||||
RegExpWrapper,
|
||||
isBlank,
|
||||
isPresent,
|
||||
isType,
|
||||
isStringMap,
|
||||
Type
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
|
||||
import {Map, MapWrapper, ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {isPresent, isBlank} from 'angular2/src/facade/lang';
|
||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||
import {PromiseWrapper, Promise} from 'angular2/src/facade/promise';
|
||||
import {Map} from 'angular2/src/facade/collection';
|
||||
|
||||
import {PathRecognizer, PathMatch} from './path_recognizer';
|
||||
import {Route, AsyncRoute, AuxRoute, Redirect, RouteDefinition} from './route_config_impl';
|
||||
import {AsyncRouteHandler} from './async_route_handler';
|
||||
import {SyncRouteHandler} from './sync_route_handler';
|
||||
import {RouteHandler} from './route_handler';
|
||||
import {Url} from './url_parser';
|
||||
import {ComponentInstruction} from './instruction';
|
||||
import {PathRecognizer} from './path_recognizer';
|
||||
|
||||
|
||||
/**
|
||||
* `RouteRecognizer` is responsible for recognizing routes for a single component.
|
||||
* It is consumed by `RouteRegistry`, which knows how to recognize an entire hierarchy of
|
||||
* components.
|
||||
*/
|
||||
export class RouteRecognizer {
|
||||
names = new Map<string, PathRecognizer>();
|
||||
export abstract class RouteMatch {}
|
||||
|
||||
// map from name to recognizer
|
||||
auxNames = new Map<string, PathRecognizer>();
|
||||
|
||||
// map from starting path to recognizer
|
||||
auxRoutes = new Map<string, PathRecognizer>();
|
||||
|
||||
// TODO: optimize this into a trie
|
||||
matchers: PathRecognizer[] = [];
|
||||
|
||||
// TODO: optimize this into a trie
|
||||
redirects: Redirector[] = [];
|
||||
|
||||
config(config: RouteDefinition): boolean {
|
||||
var handler;
|
||||
|
||||
if (isPresent(config.name) && config.name[0].toUpperCase() != config.name[0]) {
|
||||
var suggestedName = config.name[0].toUpperCase() + config.name.substring(1);
|
||||
throw new BaseException(
|
||||
`Route "${config.path}" with name "${config.name}" does not begin with an uppercase letter. Route names should be CamelCase like "${suggestedName}".`);
|
||||
}
|
||||
|
||||
if (config instanceof AuxRoute) {
|
||||
handler = new SyncRouteHandler(config.component, config.data);
|
||||
let path = config.path.startsWith('/') ? config.path.substring(1) : config.path;
|
||||
var recognizer = new PathRecognizer(config.path, handler);
|
||||
this.auxRoutes.set(path, recognizer);
|
||||
if (isPresent(config.name)) {
|
||||
this.auxNames.set(config.name, recognizer);
|
||||
}
|
||||
return recognizer.terminal;
|
||||
}
|
||||
|
||||
if (config instanceof Redirect) {
|
||||
this.redirects.push(new Redirector(config.path, config.redirectTo));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (config instanceof Route) {
|
||||
handler = new SyncRouteHandler(config.component, config.data);
|
||||
} else if (config instanceof AsyncRoute) {
|
||||
handler = new AsyncRouteHandler(config.loader, config.data);
|
||||
}
|
||||
var recognizer = new PathRecognizer(config.path, handler);
|
||||
|
||||
this.matchers.forEach((matcher) => {
|
||||
if (recognizer.hash == matcher.hash) {
|
||||
throw new BaseException(
|
||||
`Configuration '${config.path}' conflicts with existing route '${matcher.path}'`);
|
||||
}
|
||||
});
|
||||
|
||||
this.matchers.push(recognizer);
|
||||
if (isPresent(config.name)) {
|
||||
this.names.set(config.name, recognizer);
|
||||
}
|
||||
return recognizer.terminal;
|
||||
}
|
||||
export interface AbstractRecognizer {
|
||||
hash: string;
|
||||
path: string;
|
||||
recognize(beginningSegment: Url): Promise<RouteMatch>;
|
||||
generate(params: {[key: string]: any}): ComponentInstruction;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a URL, returns a list of `RouteMatch`es, which are partial recognitions for some route.
|
||||
*
|
||||
*/
|
||||
recognize(urlParse: Url): PathMatch[] {
|
||||
var solutions = [];
|
||||
|
||||
urlParse = this._redirect(urlParse);
|
||||
|
||||
this.matchers.forEach((pathRecognizer: PathRecognizer) => {
|
||||
var pathMatch = pathRecognizer.recognize(urlParse);
|
||||
|
||||
if (isPresent(pathMatch)) {
|
||||
solutions.push(pathMatch);
|
||||
}
|
||||
});
|
||||
|
||||
return solutions;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_redirect(urlParse: Url): Url {
|
||||
for (var i = 0; i < this.redirects.length; i += 1) {
|
||||
let redirector = this.redirects[i];
|
||||
var redirectedUrl = redirector.redirect(urlParse);
|
||||
if (isPresent(redirectedUrl)) {
|
||||
return redirectedUrl;
|
||||
}
|
||||
}
|
||||
|
||||
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<RouteMatch> {
|
||||
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);
|
||||
generate(params: {[key: string]: any}): ComponentInstruction {
|
||||
throw new BaseException(`Tried to generate a redirect.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// represents something like '/foo/:bar'
|
||||
export class RouteRecognizer implements AbstractRecognizer {
|
||||
specificity: number;
|
||||
terminal: boolean = true;
|
||||
hash: string;
|
||||
|
||||
private _cache: Map<string, ComponentInstruction> = new Map<string, ComponentInstruction>();
|
||||
private _pathRecognizer: PathRecognizer;
|
||||
|
||||
// TODO: cache component instruction instances by params and by ParsedUrl instance
|
||||
|
||||
constructor(public path: string, public handler: RouteHandler) {
|
||||
this._pathRecognizer = new PathRecognizer(path);
|
||||
this.specificity = this._pathRecognizer.specificity;
|
||||
this.hash = this._pathRecognizer.hash;
|
||||
this.terminal = this._pathRecognizer.terminal;
|
||||
}
|
||||
|
||||
recognize(beginningSegment: Url): Promise<RouteMatch> {
|
||||
var res = this._pathRecognizer.recognize(beginningSegment);
|
||||
if (isBlank(res)) {
|
||||
return null;
|
||||
}
|
||||
return urlParse;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -6,9 +6,6 @@ import {RouteRegistry} from './route_registry';
|
||||
import {
|
||||
ComponentInstruction,
|
||||
Instruction,
|
||||
stringifyInstruction,
|
||||
stringifyInstructionPath,
|
||||
stringifyInstructionQuery
|
||||
} from './instruction';
|
||||
import {RouterOutlet} from './router_outlet';
|
||||
import {Location} from './location';
|
||||
@@ -212,7 +209,7 @@ export class Router {
|
||||
if (result) {
|
||||
return this.commit(instruction, _skipLocationChange)
|
||||
.then((_) => {
|
||||
this._emitNavigationFinish(stringifyInstruction(instruction));
|
||||
this._emitNavigationFinish(instruction.toRootUrl());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@@ -220,25 +217,20 @@ export class Router {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO(btford): it'd be nice to remove this method as part of cleaning up the traversal logic
|
||||
// Since refactoring `Router.generate` to return an instruction rather than a string, it's not
|
||||
// guaranteed that the `componentType`s for the terminal async routes have been loaded by the time
|
||||
// we begin navigation. The method below simply traverses instructions and resolves any components
|
||||
// for which `componentType` is not present
|
||||
/** @internal */
|
||||
_settleInstruction(instruction: Instruction): Promise<any> {
|
||||
var unsettledInstructions: Array<Promise<any>> = [];
|
||||
if (isBlank(instruction.component.componentType)) {
|
||||
unsettledInstructions.push(instruction.component.resolveComponentType().then(
|
||||
(type: Type) => { this.registry.configFromComponent(type); }));
|
||||
}
|
||||
if (isPresent(instruction.child)) {
|
||||
unsettledInstructions.push(this._settleInstruction(instruction.child));
|
||||
}
|
||||
StringMapWrapper.forEach(instruction.auxInstruction, (instruction, _) => {
|
||||
unsettledInstructions.push(this._settleInstruction(instruction));
|
||||
return instruction.resolveComponent().then((_) => {
|
||||
var unsettledInstructions: Array<Promise<any>> = [];
|
||||
|
||||
if (isPresent(instruction.child)) {
|
||||
unsettledInstructions.push(this._settleInstruction(instruction.child));
|
||||
}
|
||||
|
||||
StringMapWrapper.forEach(instruction.auxInstruction, (instruction, _) => {
|
||||
unsettledInstructions.push(this._settleInstruction(instruction));
|
||||
});
|
||||
return PromiseWrapper.all(unsettledInstructions);
|
||||
});
|
||||
return PromiseWrapper.all(unsettledInstructions);
|
||||
}
|
||||
|
||||
private _emitNavigationFinish(url): void { ObservableWrapper.callEmit(this._subject, url); }
|
||||
@@ -378,7 +370,22 @@ export class Router {
|
||||
* Given a URL, returns an instruction representing the component graph
|
||||
*/
|
||||
recognize(url: string): Promise<Instruction> {
|
||||
return this.registry.recognize(url, this.hostComponent);
|
||||
var ancestorComponents = this._getAncestorComponents();
|
||||
return this.registry.recognize(url, ancestorComponents);
|
||||
}
|
||||
|
||||
/**
|
||||
* get all the host components for this and
|
||||
*/
|
||||
private _getAncestorComponents(): any[] {
|
||||
var ancestorComponents = [];
|
||||
var ancestorRouter = this;
|
||||
do {
|
||||
ancestorComponents.unshift(ancestorRouter.hostComponent);
|
||||
ancestorRouter = ancestorRouter.parent;
|
||||
} while (isPresent(ancestorRouter));
|
||||
|
||||
return ancestorComponents;
|
||||
}
|
||||
|
||||
|
||||
@@ -399,67 +406,27 @@ export class Router {
|
||||
* app's base href.
|
||||
*/
|
||||
generate(linkParams: any[]): Instruction {
|
||||
let normalizedLinkParams = splitAndFlattenLinkParams(linkParams);
|
||||
var ancestorComponents = this._getAncestorComponents();
|
||||
var startingNumberOfAncestors = ancestorComponents.length;
|
||||
|
||||
var first = ListWrapper.first(normalizedLinkParams);
|
||||
var rest = ListWrapper.slice(normalizedLinkParams, 1);
|
||||
var nextInstruction = this.registry.generate(linkParams, ancestorComponents);
|
||||
if (isBlank(nextInstruction)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var router = this;
|
||||
var parentInstructionsToClone = startingNumberOfAncestors - ancestorComponents.length;
|
||||
|
||||
// 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;
|
||||
var router = this.parent;
|
||||
for (var i = 0; i < parentInstructionsToClone; i++) {
|
||||
if (isBlank(router)) {
|
||||
break;
|
||||
}
|
||||
} 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);
|
||||
while (isPresent(router) && isPresent(router._currentInstruction)) {
|
||||
nextInstruction = router._currentInstruction.replaceChild(nextInstruction);
|
||||
router = router.parent;
|
||||
}
|
||||
|
||||
return nextInstruction;
|
||||
@@ -482,8 +449,8 @@ export class RootRouter extends Router {
|
||||
}
|
||||
|
||||
commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> {
|
||||
var emitPath = stringifyInstructionPath(instruction);
|
||||
var emitQuery = stringifyInstructionQuery(instruction);
|
||||
var emitPath = instruction.toUrlPath();
|
||||
var emitQuery = instruction.toUrlQuery();
|
||||
if (emitPath.length > 0) {
|
||||
emitPath = '/' + emitPath;
|
||||
}
|
||||
@@ -521,20 +488,6 @@ class ChildRouter extends Router {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Given: ['/a/b', {c: 2}]
|
||||
* Returns: ['', 'a', 'b', {c: 2}]
|
||||
*/
|
||||
function splitAndFlattenLinkParams(linkParams: any[]): any[] {
|
||||
return linkParams.reduce((accumulation: any[], item) => {
|
||||
if (isString(item)) {
|
||||
let strItem: string = item;
|
||||
return accumulation.concat(strItem.split('/'));
|
||||
}
|
||||
accumulation.push(item);
|
||||
return accumulation;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function canActivateOne(nextInstruction: Instruction,
|
||||
prevInstruction: Instruction): Promise<boolean> {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {isString} from 'angular2/src/facade/lang';
|
||||
|
||||
import {Router} from './router';
|
||||
import {Location} from './location';
|
||||
import {Instruction, stringifyInstruction} from './instruction';
|
||||
import {Instruction} from './instruction';
|
||||
|
||||
/**
|
||||
* The RouterLink directive lets you link to specific parts of your app.
|
||||
@@ -61,7 +61,7 @@ export class RouterLink {
|
||||
this._routeParams = changes;
|
||||
this._navigationInstruction = this._router.generate(this._routeParams);
|
||||
|
||||
var navigationHref = stringifyInstruction(this._navigationInstruction);
|
||||
var navigationHref = this._navigationInstruction.toLinkUrl();
|
||||
this.visibleHref = this._location.prepareExternalUrl(navigationHref);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,19 @@
|
||||
import {RouteHandler} from './route_handler';
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
import {Type} from 'angular2/src/facade/lang';
|
||||
import {isPresent, Type} from 'angular2/src/facade/lang';
|
||||
|
||||
import {RouteHandler} from './route_handler';
|
||||
import {RouteData, BLANK_ROUTE_DATA} from './instruction';
|
||||
|
||||
|
||||
export class SyncRouteHandler implements RouteHandler {
|
||||
public data: RouteData;
|
||||
|
||||
/** @internal */
|
||||
_resolvedComponent: Promise<any> = null;
|
||||
|
||||
constructor(public componentType: Type, public data?: {[key: string]: any}) {
|
||||
constructor(public componentType: Type, data?: {[key: string]: any}) {
|
||||
this._resolvedComponent = PromiseWrapper.resolve(componentType);
|
||||
this.data = isPresent(data) ? new RouteData(data) : BLANK_ROUTE_DATA;
|
||||
}
|
||||
|
||||
resolveComponentType(): Promise<any> { return this._resolvedComponent; }
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
import {
|
||||
AsyncTestCompleter,
|
||||
describe,
|
||||
it,
|
||||
iit,
|
||||
ddescribe,
|
||||
expect,
|
||||
inject,
|
||||
beforeEach,
|
||||
SpyObject
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {Map, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {RouteMatch, PathMatch, RedirectMatch} from 'angular2/src/router/route_recognizer';
|
||||
import {ComponentRecognizer} from 'angular2/src/router/component_recognizer';
|
||||
|
||||
import {Route, Redirect} from 'angular2/src/router/route_config_decorator';
|
||||
import {parser} from 'angular2/src/router/url_parser';
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/promise';
|
||||
|
||||
|
||||
export function main() {
|
||||
describe('ComponentRecognizer', () => {
|
||||
var recognizer: ComponentRecognizer;
|
||||
|
||||
beforeEach(() => { recognizer = new ComponentRecognizer(); });
|
||||
|
||||
|
||||
it('should recognize a static segment', inject([AsyncTestCompleter], (async) => {
|
||||
recognizer.config(new Route({path: '/test', component: DummyCmpA}));
|
||||
recognize(recognizer, '/test')
|
||||
.then((solutions: RouteMatch[]) => {
|
||||
expect(solutions.length).toBe(1);
|
||||
expect(getComponentType(solutions[0])).toEqual(DummyCmpA);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should recognize a single slash', inject([AsyncTestCompleter], (async) => {
|
||||
recognizer.config(new Route({path: '/', component: DummyCmpA}));
|
||||
recognize(recognizer, '/')
|
||||
.then((solutions: RouteMatch[]) => {
|
||||
expect(solutions.length).toBe(1);
|
||||
expect(getComponentType(solutions[0])).toEqual(DummyCmpA);
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should recognize a dynamic segment', inject([AsyncTestCompleter], (async) => {
|
||||
recognizer.config(new Route({path: '/user/:name', component: DummyCmpA}));
|
||||
recognize(recognizer, '/user/brian')
|
||||
.then((solutions: RouteMatch[]) => {
|
||||
expect(solutions.length).toBe(1);
|
||||
expect(getComponentType(solutions[0])).toEqual(DummyCmpA);
|
||||
expect(getParams(solutions[0])).toEqual({'name': 'brian'});
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should recognize a star segment', inject([AsyncTestCompleter], (async) => {
|
||||
recognizer.config(new Route({path: '/first/*rest', component: DummyCmpA}));
|
||||
recognize(recognizer, '/first/second/third')
|
||||
.then((solutions: RouteMatch[]) => {
|
||||
expect(solutions.length).toBe(1);
|
||||
expect(getComponentType(solutions[0])).toEqual(DummyCmpA);
|
||||
expect(getParams(solutions[0])).toEqual({'rest': 'second/third'});
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should throw when given two routes that start with the same static segment', () => {
|
||||
recognizer.config(new Route({path: '/hello', component: DummyCmpA}));
|
||||
expect(() => recognizer.config(new Route({path: '/hello', component: DummyCmpB})))
|
||||
.toThrowError('Configuration \'/hello\' conflicts with existing route \'/hello\'');
|
||||
});
|
||||
|
||||
|
||||
it('should throw when given two routes that have dynamic segments in the same order', () => {
|
||||
recognizer.config(new Route({path: '/hello/:person/how/:doyoudou', component: DummyCmpA}));
|
||||
expect(() => recognizer.config(
|
||||
new Route({path: '/hello/:friend/how/:areyou', component: DummyCmpA})))
|
||||
.toThrowError(
|
||||
'Configuration \'/hello/:friend/how/:areyou\' conflicts with existing route \'/hello/:person/how/:doyoudou\'');
|
||||
|
||||
expect(() => recognizer.config(
|
||||
new Redirect({path: '/hello/:pal/how/:goesit', redirectTo: ['/Foo']})))
|
||||
.toThrowError(
|
||||
'Configuration \'/hello/:pal/how/:goesit\' conflicts with existing route \'/hello/:person/how/:doyoudou\'');
|
||||
});
|
||||
|
||||
|
||||
it('should recognize redirects', inject([AsyncTestCompleter], (async) => {
|
||||
recognizer.config(new Route({path: '/b', component: DummyCmpA}));
|
||||
recognizer.config(new Redirect({path: '/a', redirectTo: ['B']}));
|
||||
recognize(recognizer, '/a')
|
||||
.then((solutions: RouteMatch[]) => {
|
||||
expect(solutions.length).toBe(1);
|
||||
var solution = solutions[0];
|
||||
expect(solution).toBeAnInstanceOf(RedirectMatch);
|
||||
if (solution instanceof RedirectMatch) {
|
||||
expect(solution.redirectTo).toEqual(['B']);
|
||||
}
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should generate URLs with params', () => {
|
||||
recognizer.config(new Route({path: '/app/user/:name', component: DummyCmpA, name: 'User'}));
|
||||
var instruction = recognizer.generate('User', {'name': 'misko'});
|
||||
expect(instruction.urlPath).toEqual('app/user/misko');
|
||||
});
|
||||
|
||||
|
||||
it('should generate URLs with numeric params', () => {
|
||||
recognizer.config(new Route({path: '/app/page/:number', component: DummyCmpA, name: 'Page'}));
|
||||
expect(recognizer.generate('Page', {'number': 42}).urlPath).toEqual('app/page/42');
|
||||
});
|
||||
|
||||
|
||||
it('should throw in the absence of required params URLs', () => {
|
||||
recognizer.config(new Route({path: 'app/user/:name', component: DummyCmpA, name: 'User'}));
|
||||
expect(() => recognizer.generate('User', {}))
|
||||
.toThrowError('Route generator for \'name\' was not included in parameters passed.');
|
||||
});
|
||||
|
||||
|
||||
it('should throw if the route alias is not TitleCase', () => {
|
||||
expect(() => recognizer.config(
|
||||
new Route({path: 'app/user/:name', component: DummyCmpA, name: 'user'})))
|
||||
.toThrowError(
|
||||
`Route "app/user/:name" with name "user" does not begin with an uppercase letter. Route names should be CamelCase like "User".`);
|
||||
});
|
||||
|
||||
|
||||
describe('params', () => {
|
||||
it('should recognize parameters within the URL path',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
recognizer.config(
|
||||
new Route({path: 'profile/:name', component: DummyCmpA, name: 'User'}));
|
||||
recognize(recognizer, '/profile/matsko?comments=all')
|
||||
.then((solutions: RouteMatch[]) => {
|
||||
expect(solutions.length).toBe(1);
|
||||
expect(getParams(solutions[0])).toEqual({'name': 'matsko', 'comments': 'all'});
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should generate and populate the given static-based route with querystring params',
|
||||
() => {
|
||||
recognizer.config(
|
||||
new Route({path: 'forum/featured', component: DummyCmpA, name: 'ForumPage'}));
|
||||
|
||||
var params = {'start': 10, 'end': 100};
|
||||
|
||||
var result = recognizer.generate('ForumPage', params);
|
||||
expect(result.urlPath).toEqual('forum/featured');
|
||||
expect(result.urlParams).toEqual(['start=10', 'end=100']);
|
||||
});
|
||||
|
||||
|
||||
it('should prefer positional params over query params',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
recognizer.config(
|
||||
new Route({path: 'profile/:name', component: DummyCmpA, name: 'User'}));
|
||||
recognize(recognizer, '/profile/yegor?name=igor')
|
||||
.then((solutions: RouteMatch[]) => {
|
||||
expect(solutions.length).toBe(1);
|
||||
expect(getParams(solutions[0])).toEqual({'name': 'yegor'});
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
|
||||
it('should ignore matrix params for the top-level component',
|
||||
inject([AsyncTestCompleter], (async) => {
|
||||
recognizer.config(
|
||||
new Route({path: '/home/:subject', component: DummyCmpA, name: 'User'}));
|
||||
recognize(recognizer, '/home;sort=asc/zero;one=1?two=2')
|
||||
.then((solutions: RouteMatch[]) => {
|
||||
expect(solutions.length).toBe(1);
|
||||
expect(getParams(solutions[0])).toEqual({'subject': 'zero', 'two': '2'});
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function recognize(recognizer: ComponentRecognizer, url: string): Promise<RouteMatch[]> {
|
||||
var parsedUrl = parser.parse(url);
|
||||
return PromiseWrapper.all(recognizer.recognize(parsedUrl));
|
||||
}
|
||||
|
||||
function getComponentType(routeMatch: RouteMatch): any {
|
||||
if (routeMatch instanceof PathMatch) {
|
||||
return routeMatch.instruction.componentType;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getParams(routeMatch: RouteMatch): any {
|
||||
if (routeMatch instanceof PathMatch) {
|
||||
return routeMatch.instruction.params;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
class DummyCmpA {}
|
||||
class DummyCmpB {}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Router integration tests
|
||||
|
||||
These tests only mock out `Location`, and otherwise use all the real parts of routing to ensure that
|
||||
various routing scenarios work as expected.
|
||||
|
||||
The Component Router in Angular 2 exposes only a handful of different options, but because they can
|
||||
be combined and nested in so many ways, it's difficult to rigorously test all the cases.
|
||||
|
||||
The address this problem, we introduce `describeRouter`, `describeWith`, and `describeWithout`.
|
||||
@@ -0,0 +1,28 @@
|
||||
import {
|
||||
describeRouter,
|
||||
ddescribeRouter,
|
||||
describeWith,
|
||||
describeWithout,
|
||||
describeWithAndWithout,
|
||||
itShouldRoute
|
||||
} from './util';
|
||||
|
||||
import {registerSpecs} from './impl/async_route_spec_impl';
|
||||
|
||||
export function main() {
|
||||
registerSpecs();
|
||||
|
||||
ddescribeRouter('async routes', () => {
|
||||
describeWithout('children', () => {
|
||||
describeWith('route data', itShouldRoute);
|
||||
describeWithAndWithout('params', itShouldRoute);
|
||||
});
|
||||
|
||||
describeWith('sync children',
|
||||
() => { describeWithAndWithout('default routes', itShouldRoute); });
|
||||
|
||||
describeWith('async children', () => {
|
||||
describeWithAndWithout('params', () => { describeWithout('default routes', itShouldRoute); });
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
import {
|
||||
RootTestComponent,
|
||||
AsyncTestCompleter,
|
||||
TestComponentBuilder,
|
||||
beforeEach,
|
||||
ddescribe,
|
||||
xdescribe,
|
||||
describe,
|
||||
el,
|
||||
expect,
|
||||
iit,
|
||||
inject,
|
||||
beforeEachProviders,
|
||||
it,
|
||||
xit
|
||||
} from 'angular2/testing_internal';
|
||||
|
||||
import {provide, Component, Injector, Inject} from 'angular2/core';
|
||||
|
||||
import {Router, ROUTER_DIRECTIVES, RouteParams, RouteData, Location} from 'angular2/router';
|
||||
import {RouteConfig, Route, AuxRoute, Redirect} from 'angular2/src/router/route_config_decorator';
|
||||
|
||||
import {TEST_ROUTER_PROVIDERS, RootCmp, compile} from './util';
|
||||
|
||||
var cmpInstanceCount;
|
||||
var childCmpInstanceCount;
|
||||
|
||||
export function main() {
|
||||
describe('auxiliary routes', () => {
|
||||
|
||||
var tcb: TestComponentBuilder;
|
||||
var rootTC: RootTestComponent;
|
||||
var rtr;
|
||||
|
||||
beforeEachProviders(() => TEST_ROUTER_PROVIDERS);
|
||||
|
||||
beforeEach(inject([TestComponentBuilder, Router], (tcBuilder, router) => {
|
||||
tcb = tcBuilder;
|
||||
rtr = router;
|
||||
childCmpInstanceCount = 0;
|
||||
cmpInstanceCount = 0;
|
||||
}));
|
||||
|
||||
it('should recognize and navigate from the URL', inject([AsyncTestCompleter], (async) => {
|
||||
compile(tcb, `main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
|
||||
.then((rtc) => {rootTC = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Aux'})
|
||||
]))
|
||||
.then((_) => rtr.navigateByUrl('/hello(modal)'))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.debugElement.nativeElement).toHaveText('main {hello} | aux {modal}');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
|
||||
it('should navigate via the link DSL', inject([AsyncTestCompleter], (async) => {
|
||||
compile(tcb, `main {<router-outlet></router-outlet>} | aux {<router-outlet name="modal"></router-outlet>}`)
|
||||
.then((rtc) => {rootTC = rtc})
|
||||
.then((_) => rtr.config([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Modal'})
|
||||
]))
|
||||
.then((_) => rtr.navigate(['/Hello', ['Modal']]))
|
||||
.then((_) => {
|
||||
rootTC.detectChanges();
|
||||
expect(rootTC.debugElement.nativeElement).toHaveText('main {hello} | aux {modal}');
|
||||
async.done();
|
||||
});
|
||||
}));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@Component({selector: 'hello-cmp', template: `{{greeting}}`})
|
||||
class HelloCmp {
|
||||
greeting: string;
|
||||
constructor() { this.greeting = 'hello'; }
|
||||
}
|
||||
|
||||
@Component({selector: 'modal-cmp', template: `modal`})
|
||||
class ModalCmp {
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'aux-cmp',
|
||||
template: 'main {<router-outlet></router-outlet>} | ' +
|
||||
'aux {<router-outlet name="modal"></router-outlet>}',
|
||||
directives: [ROUTER_DIRECTIVES],
|
||||
})
|
||||
@RouteConfig([
|
||||
new Route({path: '/hello', component: HelloCmp, name: 'Hello'}),
|
||||
new AuxRoute({path: '/modal', component: ModalCmp, name: 'Aux'})
|
||||
])
|
||||
class AuxCmp {
|
||||
}
|
||||
+62
-54
@@ -38,44 +38,39 @@ 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 +86,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 +178,7 @@ export function main() {
|
||||
}));
|
||||
});
|
||||
});
|
||||
// TODO: add a test in which the child component has bindings
|
||||
|
||||
|
||||
describe('querystring params app', () => {
|
||||
beforeEachProviders(
|
||||
@@ -243,20 +237,21 @@ export function main() {
|
||||
}
|
||||
|
||||
|
||||
@Component({selector: 'hello-cmp'})
|
||||
@View({template: 'hello'})
|
||||
@Component({selector: 'hello-cmp', template: 'hello'})
|
||||
class HelloCmp {
|
||||
public message: string;
|
||||
}
|
||||
|
||||
@Component({selector: 'hello2-cmp'})
|
||||
@View({template: 'hello2'})
|
||||
@Component({selector: 'hello2-cmp', template: 'hello2'})
|
||||
class Hello2Cmp {
|
||||
public greeting: string;
|
||||
}
|
||||
|
||||
@Component({selector: 'app-cmp'})
|
||||
@View({template: "outer { <router-outlet></router-outlet> }", directives: ROUTER_DIRECTIVES})
|
||||
@Component({
|
||||
selector: 'app-cmp',
|
||||
template: `outer { <router-outlet></router-outlet> }`,
|
||||
directives: ROUTER_DIRECTIVES
|
||||
})
|
||||
@RouteConfig([new Route({path: '/', component: HelloCmp})])
|
||||
class AppCmp {
|
||||
constructor(public router: Router, public location: LocationStrategy) {}
|
||||
@@ -283,20 +278,29 @@ class AppWithViewChildren implements AfterViewInit {
|
||||
afterViewInit() { this.helloCmp.message = 'Ahoy'; }
|
||||
}
|
||||
|
||||
@Component({selector: 'parent-cmp'})
|
||||
@View({template: `parent { <router-outlet></router-outlet> }`, directives: ROUTER_DIRECTIVES})
|
||||
@Component({
|
||||
selector: 'parent-cmp',
|
||||
template: `parent { <router-outlet></router-outlet> }`,
|
||||
directives: ROUTER_DIRECTIVES
|
||||
})
|
||||
@RouteConfig([new Route({path: '/child', component: HelloCmp})])
|
||||
class ParentCmp {
|
||||
}
|
||||
|
||||
@Component({selector: 'super-parent-cmp'})
|
||||
@View({template: `super-parent { <router-outlet></router-outlet> }`, directives: ROUTER_DIRECTIVES})
|
||||
@Component({
|
||||
selector: 'super-parent-cmp',
|
||||
template: `super-parent { <router-outlet></router-outlet> }`,
|
||||
directives: ROUTER_DIRECTIVES
|
||||
})
|
||||
@RouteConfig([new Route({path: '/child', component: Hello2Cmp})])
|
||||
class SuperParentCmp {
|
||||
}
|
||||
|
||||
@Component({selector: 'app-cmp'})
|
||||
@View({template: `root { <router-outlet></router-outlet> }`, directives: ROUTER_DIRECTIVES})
|
||||
@Component({
|
||||
selector: 'app-cmp',
|
||||
template: `root { <router-outlet></router-outlet> }`,
|
||||
directives: ROUTER_DIRECTIVES
|
||||
})
|
||||
@RouteConfig([
|
||||
new Route({path: '/parent/...', component: ParentCmp}),
|
||||
new Route({path: '/super-parent/...', component: SuperParentCmp})
|
||||
@@ -305,28 +309,32 @@ class HierarchyAppCmp {
|
||||
constructor(public router: Router, public location: LocationStrategy) {}
|
||||
}
|
||||
|
||||
@Component({selector: 'qs-cmp'})
|
||||
@View({template: "qParam = {{q}}"})
|
||||
@Component({selector: 'qs-cmp', template: `qParam = {{q}}`})
|
||||
class QSCmp {
|
||||
q: string;
|
||||
constructor(params: RouteParams) { this.q = params.get('q'); }
|
||||
}
|
||||
|
||||
@Component({selector: 'app-cmp'})
|
||||
@View({template: `<router-outlet></router-outlet>`, directives: ROUTER_DIRECTIVES})
|
||||
@Component({
|
||||
selector: 'app-cmp',
|
||||
template: `<router-outlet></router-outlet>`,
|
||||
directives: ROUTER_DIRECTIVES
|
||||
})
|
||||
@RouteConfig([new Route({path: '/qs', component: QSCmp})])
|
||||
class QueryStringAppCmp {
|
||||
constructor(public router: Router, public location: LocationStrategy) {}
|
||||
}
|
||||
|
||||
@Component({selector: 'oops-cmp'})
|
||||
@View({template: "oh no"})
|
||||
@Component({selector: 'oops-cmp', template: "oh no"})
|
||||
class BrokenCmp {
|
||||
constructor() { throw new BaseException('oops!'); }
|
||||
}
|
||||
|
||||
@Component({selector: 'app-cmp'})
|
||||
@View({template: `outer { <router-outlet></router-outlet> }`, directives: ROUTER_DIRECTIVES})
|
||||
@Component({
|
||||
selector: 'app-cmp',
|
||||
template: `outer { <router-outlet></router-outlet> }`,
|
||||
directives: ROUTER_DIRECTIVES
|
||||
})
|
||||
@RouteConfig([new Route({path: '/cause-error', component: BrokenCmp})])
|
||||
class BrokenAppCmp {
|
||||
constructor(public router: Router, public location: LocationStrategy) {}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,131 @@
|
||||
import {Component} from 'angular2/angular2';
|
||||
import {
|
||||
AsyncRoute,
|
||||
Route,
|
||||
Redirect,
|
||||
RouteConfig,
|
||||
RouteParams,
|
||||
RouteData,
|
||||
ROUTER_DIRECTIVES
|
||||
} from 'angular2/router';
|
||||
import {PromiseWrapper} from 'angular2/src/facade/async';
|
||||
|
||||
@Component({selector: 'hello-cmp', template: `{{greeting}}`})
|
||||
export class HelloCmp {
|
||||
greeting: string;
|
||||
constructor() { this.greeting = 'hello'; }
|
||||
}
|
||||
|
||||
export function helloCmpLoader() {
|
||||
return PromiseWrapper.resolve(HelloCmp);
|
||||
}
|
||||
|
||||
|
||||
@Component({selector: 'user-cmp', template: `hello {{user}}`})
|
||||
export class UserCmp {
|
||||
user: string;
|
||||
constructor(params: RouteParams) { this.user = params.get('name'); }
|
||||
}
|
||||
|
||||
export function userCmpLoader() {
|
||||
return PromiseWrapper.resolve(UserCmp);
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'parent-cmp',
|
||||
template: `inner { <router-outlet></router-outlet> }`,
|
||||
directives: [ROUTER_DIRECTIVES],
|
||||
})
|
||||
@RouteConfig([new Route({path: '/b', component: HelloCmp, name: 'Child'})])
|
||||
export class ParentCmp {
|
||||
}
|
||||
|
||||
export function parentCmpLoader() {
|
||||
return PromiseWrapper.resolve(ParentCmp);
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'parent-cmp',
|
||||
template: `inner { <router-outlet></router-outlet> }`,
|
||||
directives: [ROUTER_DIRECTIVES],
|
||||
})
|
||||
@RouteConfig([new AsyncRoute({path: '/b', loader: helloCmpLoader, name: 'Child'})])
|
||||
export class AsyncParentCmp {
|
||||
}
|
||||
|
||||
export function asyncParentCmpLoader() {
|
||||
return PromiseWrapper.resolve(AsyncParentCmp);
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'parent-cmp',
|
||||
template: `inner { <router-outlet></router-outlet> }`,
|
||||
directives: [ROUTER_DIRECTIVES],
|
||||
})
|
||||
@RouteConfig(
|
||||
[new AsyncRoute({path: '/b', loader: helloCmpLoader, name: 'Child', useAsDefault: true})])
|
||||
export class AsyncDefaultParentCmp {
|
||||
}
|
||||
|
||||
export function asyncDefaultParentCmpLoader() {
|
||||
return PromiseWrapper.resolve(AsyncDefaultParentCmp);
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'parent-cmp',
|
||||
template: `inner { <router-outlet></router-outlet> }`,
|
||||
directives: [ROUTER_DIRECTIVES],
|
||||
})
|
||||
@RouteConfig([new Route({path: '/b', component: HelloCmp, name: 'Child', useAsDefault: true})])
|
||||
export class ParentWithDefaultCmp {
|
||||
}
|
||||
|
||||
export function parentWithDefaultCmpLoader() {
|
||||
return PromiseWrapper.resolve(ParentWithDefaultCmp);
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'team-cmp',
|
||||
template: `team {{id}} | user { <router-outlet></router-outlet> }`,
|
||||
directives: [ROUTER_DIRECTIVES],
|
||||
})
|
||||
@RouteConfig([new Route({path: '/user/:name', component: UserCmp, name: 'User'})])
|
||||
export class TeamCmp {
|
||||
id: string;
|
||||
constructor(params: RouteParams) { this.id = params.get('id'); }
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'team-cmp',
|
||||
template: `team {{id}} | user { <router-outlet></router-outlet> }`,
|
||||
directives: [ROUTER_DIRECTIVES],
|
||||
})
|
||||
@RouteConfig([new AsyncRoute({path: '/user/:name', loader: userCmpLoader, name: 'User'})])
|
||||
export class AsyncTeamCmp {
|
||||
id: string;
|
||||
constructor(params: RouteParams) { this.id = params.get('id'); }
|
||||
}
|
||||
|
||||
export function asyncTeamLoader() {
|
||||
return PromiseWrapper.resolve(AsyncTeamCmp);
|
||||
}
|
||||
|
||||
|
||||
@Component({selector: 'data-cmp', template: `{{myData}}`})
|
||||
export class RouteDataCmp {
|
||||
myData: boolean;
|
||||
constructor(data: RouteData) { this.myData = data.get('isAdmin'); }
|
||||
}
|
||||
|
||||
export function asyncRouteDataCmp() {
|
||||
return PromiseWrapper.resolve(RouteDataCmp);
|
||||
}
|
||||
|
||||
@Component({selector: 'redirect-to-parent-cmp', template: 'redirect-to-parent'})
|
||||
@RouteConfig([new Redirect({path: '/child-redirect', redirectTo: ['../HelloSib']})])
|
||||
export class RedirectToParentCmp {
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user