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

This reverts commit cf7292fcb1.

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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