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:
Brian Ford
2015-11-20 23:18:43 +00:00
parent 422a7b18f6
commit cf7292fcb1
41 changed files with 2968 additions and 1115 deletions
@@ -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
+9 -34
View File
@@ -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 {
+2 -1
View File
@@ -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;
}
+97 -162
View File
@@ -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
+44 -91
View File
@@ -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> {
+2 -2
View File
@@ -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; }