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:
@ -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)) {
|
||||
|
157
modules/angular2/src/router/component_recognizer.ts
Normal file
157
modules/angular2/src/router/component_recognizer.ts
Normal file
@ -0,0 +1,157 @@
|
||||
import {isBlank, isPresent} from 'angular2/src/facade/lang';
|
||||
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
|
||||
import {Map, MapWrapper, ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
|
||||
import {
|
||||
AbstractRecognizer,
|
||||
RouteRecognizer,
|
||||
RedirectRecognizer,
|
||||
RouteMatch
|
||||
} from './route_recognizer';
|
||||
import {Route, AsyncRoute, AuxRoute, Redirect, RouteDefinition} from './route_config_impl';
|
||||
import {AsyncRouteHandler} from './async_route_handler';
|
||||
import {SyncRouteHandler} from './sync_route_handler';
|
||||
import {Url} from './url_parser';
|
||||
import {ComponentInstruction} from './instruction';
|
||||
|
||||
|
||||
/**
|
||||
* `ComponentRecognizer` is responsible for recognizing routes for a single component.
|
||||
* It is consumed by `RouteRegistry`, which knows how to recognize an entire hierarchy of
|
||||
* components.
|
||||
*/
|
||||
export class ComponentRecognizer {
|
||||
names = new Map<string, RouteRecognizer>();
|
||||
|
||||
// map from name to recognizer
|
||||
auxNames = new Map<string, RouteRecognizer>();
|
||||
|
||||
// map from starting path to recognizer
|
||||
auxRoutes = new Map<string, RouteRecognizer>();
|
||||
|
||||
// TODO: optimize this into a trie
|
||||
matchers: AbstractRecognizer[] = [];
|
||||
|
||||
defaultRoute: RouteRecognizer = null;
|
||||
|
||||
/**
|
||||
* returns whether or not the config is terminal
|
||||
*/
|
||||
config(config: RouteDefinition): boolean {
|
||||
var handler;
|
||||
|
||||
if (isPresent(config.name) && config.name[0].toUpperCase() != config.name[0]) {
|
||||
var suggestedName = config.name[0].toUpperCase() + config.name.substring(1);
|
||||
throw new BaseException(
|
||||
`Route "${config.path}" with name "${config.name}" does not begin with an uppercase letter. Route names should be CamelCase like "${suggestedName}".`);
|
||||
}
|
||||
|
||||
if (config instanceof AuxRoute) {
|
||||
handler = new SyncRouteHandler(config.component, config.data);
|
||||
let path = config.path.startsWith('/') ? config.path.substring(1) : config.path;
|
||||
var recognizer = new RouteRecognizer(config.path, handler);
|
||||
this.auxRoutes.set(path, recognizer);
|
||||
if (isPresent(config.name)) {
|
||||
this.auxNames.set(config.name, recognizer);
|
||||
}
|
||||
return recognizer.terminal;
|
||||
}
|
||||
|
||||
var useAsDefault = false;
|
||||
|
||||
if (config instanceof Redirect) {
|
||||
let redirector = new RedirectRecognizer(config.path, config.redirectTo);
|
||||
this._assertNoHashCollision(redirector.hash, config.path);
|
||||
this.matchers.push(redirector);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (config instanceof Route) {
|
||||
handler = new SyncRouteHandler(config.component, config.data);
|
||||
useAsDefault = isPresent(config.useAsDefault) && config.useAsDefault;
|
||||
} else if (config instanceof AsyncRoute) {
|
||||
handler = new AsyncRouteHandler(config.loader, config.data);
|
||||
useAsDefault = isPresent(config.useAsDefault) && config.useAsDefault;
|
||||
}
|
||||
var recognizer = new RouteRecognizer(config.path, handler);
|
||||
|
||||
this._assertNoHashCollision(recognizer.hash, config.path);
|
||||
|
||||
if (useAsDefault) {
|
||||
if (isPresent(this.defaultRoute)) {
|
||||
throw new BaseException(`Only one route can be default`);
|
||||
}
|
||||
this.defaultRoute = recognizer;
|
||||
}
|
||||
|
||||
this.matchers.push(recognizer);
|
||||
if (isPresent(config.name)) {
|
||||
this.names.set(config.name, recognizer);
|
||||
}
|
||||
return recognizer.terminal;
|
||||
}
|
||||
|
||||
|
||||
private _assertNoHashCollision(hash: string, path) {
|
||||
this.matchers.forEach((matcher) => {
|
||||
if (hash == matcher.hash) {
|
||||
throw new BaseException(
|
||||
`Configuration '${path}' conflicts with existing route '${matcher.path}'`);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a URL, returns a list of `RouteMatch`es, which are partial recognitions for some route.
|
||||
*/
|
||||
recognize(urlParse: Url): Promise<RouteMatch>[] {
|
||||
var solutions = [];
|
||||
|
||||
this.matchers.forEach((routeRecognizer: AbstractRecognizer) => {
|
||||
var pathMatch = routeRecognizer.recognize(urlParse);
|
||||
|
||||
if (isPresent(pathMatch)) {
|
||||
solutions.push(pathMatch);
|
||||
}
|
||||
});
|
||||
|
||||
return solutions;
|
||||
}
|
||||
|
||||
recognizeAuxiliary(urlParse: Url): Promise<RouteMatch>[] {
|
||||
var routeRecognizer: RouteRecognizer = this.auxRoutes.get(urlParse.path);
|
||||
if (isPresent(routeRecognizer)) {
|
||||
return [routeRecognizer.recognize(urlParse)];
|
||||
}
|
||||
|
||||
return [PromiseWrapper.resolve(null)];
|
||||
}
|
||||
|
||||
hasRoute(name: string): boolean { return this.names.has(name); }
|
||||
|
||||
componentLoaded(name: string): boolean {
|
||||
return this.hasRoute(name) && isPresent(this.names.get(name).handler.componentType);
|
||||
}
|
||||
|
||||
loadComponent(name: string): Promise<any> {
|
||||
return this.names.get(name).handler.resolveComponentType();
|
||||
}
|
||||
|
||||
generate(name: string, params: any): ComponentInstruction {
|
||||
var pathRecognizer: RouteRecognizer = this.names.get(name);
|
||||
if (isBlank(pathRecognizer)) {
|
||||
return null;
|
||||
}
|
||||
return pathRecognizer.generate(params);
|
||||
}
|
||||
|
||||
generateAuxiliary(name: string, params: any): ComponentInstruction {
|
||||
var pathRecognizer: RouteRecognizer = this.auxNames.get(name);
|
||||
if (isBlank(pathRecognizer)) {
|
||||
return null;
|
||||
}
|
||||
return pathRecognizer.generate(params);
|
||||
}
|
||||
}
|
@ -1,10 +1,7 @@
|
||||
import {Map, MapWrapper, StringMapWrapper, ListWrapper} from 'angular2/src/facade/collection';
|
||||
import {unimplemented} from 'angular2/src/facade/exceptions';
|
||||
import {isPresent, isBlank, normalizeBlank, Type, CONST_EXPR} from 'angular2/src/facade/lang';
|
||||
import {Promise} from 'angular2/src/facade/async';
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
|
||||
import {PathRecognizer} from './path_recognizer';
|
||||
import {Url} from './url_parser';
|
||||
|
||||
/**
|
||||
* `RouteParams` is an immutable map of parameters for the given route
|
||||
@ -77,7 +74,7 @@ export class RouteData {
|
||||
get(key: string): any { return normalizeBlank(StringMapWrapper.get(this.data, key)); }
|
||||
}
|
||||
|
||||
var BLANK_ROUTE_DATA = new RouteData();
|
||||
export var BLANK_ROUTE_DATA = new RouteData();
|
||||
|
||||
/**
|
||||
* `Instruction` is a tree of {@link ComponentInstruction}s with all the information needed
|
||||
@ -106,74 +103,184 @@ var BLANK_ROUTE_DATA = new RouteData();
|
||||
* bootstrap(AppCmp, ROUTER_PROVIDERS);
|
||||
* ```
|
||||
*/
|
||||
export class Instruction {
|
||||
constructor(public component: ComponentInstruction, public child: Instruction,
|
||||
public auxInstruction: {[key: string]: Instruction}) {}
|
||||
export abstract class Instruction {
|
||||
public component: ComponentInstruction;
|
||||
public child: Instruction;
|
||||
public auxInstruction: {[key: string]: Instruction} = {};
|
||||
|
||||
get urlPath(): string { return this.component.urlPath; }
|
||||
|
||||
get urlParams(): string[] { return this.component.urlParams; }
|
||||
|
||||
get specificity(): number {
|
||||
var total = 0;
|
||||
if (isPresent(this.component)) {
|
||||
total += this.component.specificity;
|
||||
}
|
||||
if (isPresent(this.child)) {
|
||||
total += this.child.specificity;
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
abstract resolveComponent(): Promise<ComponentInstruction>;
|
||||
|
||||
/**
|
||||
* converts the instruction into a URL string
|
||||
*/
|
||||
toRootUrl(): string { return this.toUrlPath() + this.toUrlQuery(); }
|
||||
|
||||
/** @internal */
|
||||
_toNonRootUrl(): string {
|
||||
return this._stringifyPathMatrixAuxPrefixed() +
|
||||
(isPresent(this.child) ? this.child._toNonRootUrl() : '');
|
||||
}
|
||||
|
||||
toUrlQuery(): string { return this.urlParams.length > 0 ? ('?' + this.urlParams.join('&')) : ''; }
|
||||
|
||||
/**
|
||||
* Returns a new instruction that shares the state of the existing instruction, but with
|
||||
* the given child {@link Instruction} replacing the existing child.
|
||||
*/
|
||||
replaceChild(child: Instruction): Instruction {
|
||||
return new Instruction(this.component, child, this.auxInstruction);
|
||||
return new ResolvedInstruction(this.component, child, this.auxInstruction);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a partially completed instruction during recognition that only has the
|
||||
* primary (non-aux) route instructions matched.
|
||||
*
|
||||
* `PrimaryInstruction` is an internal class used by `RouteRecognizer` while it's
|
||||
* figuring out where to navigate.
|
||||
*/
|
||||
export class PrimaryInstruction {
|
||||
constructor(public component: ComponentInstruction, public child: PrimaryInstruction,
|
||||
public auxUrls: Url[]) {}
|
||||
}
|
||||
|
||||
export function stringifyInstruction(instruction: Instruction): string {
|
||||
return stringifyInstructionPath(instruction) + stringifyInstructionQuery(instruction);
|
||||
}
|
||||
|
||||
export function stringifyInstructionPath(instruction: Instruction): string {
|
||||
return instruction.component.urlPath + stringifyAux(instruction) +
|
||||
stringifyPrimaryPrefixed(instruction.child);
|
||||
}
|
||||
|
||||
export function stringifyInstructionQuery(instruction: Instruction): string {
|
||||
return instruction.component.urlParams.length > 0 ?
|
||||
('?' + instruction.component.urlParams.join('&')) :
|
||||
'';
|
||||
}
|
||||
|
||||
function stringifyPrimaryPrefixed(instruction: Instruction): string {
|
||||
var primary = stringifyPrimary(instruction);
|
||||
if (primary.length > 0) {
|
||||
primary = '/' + primary;
|
||||
/**
|
||||
* If the final URL for the instruction is ``
|
||||
*/
|
||||
toUrlPath(): string {
|
||||
return this.urlPath + this._stringifyAux() +
|
||||
(isPresent(this.child) ? this.child._toNonRootUrl() : '');
|
||||
}
|
||||
return primary;
|
||||
}
|
||||
|
||||
function stringifyPrimary(instruction: Instruction): string {
|
||||
if (isBlank(instruction)) {
|
||||
// default instructions override these
|
||||
toLinkUrl(): string {
|
||||
return this.urlPath + this._stringifyAux() +
|
||||
(isPresent(this.child) ? this.child._toLinkUrl() : '');
|
||||
}
|
||||
|
||||
// this is the non-root version (called recursively)
|
||||
/** @internal */
|
||||
_toLinkUrl(): string {
|
||||
return this._stringifyPathMatrixAuxPrefixed() +
|
||||
(isPresent(this.child) ? this.child._toLinkUrl() : '');
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_stringifyPathMatrixAuxPrefixed(): string {
|
||||
var primary = this._stringifyPathMatrixAux();
|
||||
if (primary.length > 0) {
|
||||
primary = '/' + primary;
|
||||
}
|
||||
return primary;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_stringifyMatrixParams(): string {
|
||||
return this.urlParams.length > 0 ? (';' + this.component.urlParams.join(';')) : '';
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_stringifyPathMatrixAux(): string {
|
||||
if (isBlank(this.component)) {
|
||||
return '';
|
||||
}
|
||||
return this.urlPath + this._stringifyMatrixParams() + this._stringifyAux();
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_stringifyAux(): string {
|
||||
var routes = [];
|
||||
StringMapWrapper.forEach(this.auxInstruction, (auxInstruction, _) => {
|
||||
routes.push(auxInstruction._stringifyPathMatrixAux());
|
||||
});
|
||||
if (routes.length > 0) {
|
||||
return '(' + routes.join('//') + ')';
|
||||
}
|
||||
return '';
|
||||
}
|
||||
var params = instruction.component.urlParams.length > 0 ?
|
||||
(';' + instruction.component.urlParams.join(';')) :
|
||||
'';
|
||||
return instruction.component.urlPath + params + stringifyAux(instruction) +
|
||||
stringifyPrimaryPrefixed(instruction.child);
|
||||
}
|
||||
|
||||
function stringifyAux(instruction: Instruction): string {
|
||||
var routes = [];
|
||||
StringMapWrapper.forEach(instruction.auxInstruction, (auxInstruction, _) => {
|
||||
routes.push(stringifyPrimary(auxInstruction));
|
||||
});
|
||||
if (routes.length > 0) {
|
||||
return '(' + routes.join('//') + ')';
|
||||
|
||||
/**
|
||||
* a resolved instruction has an outlet instruction for itself, but maybe not for...
|
||||
*/
|
||||
export class ResolvedInstruction extends Instruction {
|
||||
constructor(public component: ComponentInstruction, public child: Instruction,
|
||||
public auxInstruction: {[key: string]: Instruction}) {
|
||||
super();
|
||||
}
|
||||
|
||||
resolveComponent(): Promise<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);
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
||||
@ -185,67 +292,18 @@ function stringifyAux(instruction: Instruction): string {
|
||||
* to route lifecycle hooks, like {@link CanActivate}.
|
||||
*
|
||||
* `ComponentInstruction`s are [https://en.wikipedia.org/wiki/Hash_consing](hash consed). You should
|
||||
* never construct one yourself with "new." Instead, rely on {@link Router/PathRecognizer} to
|
||||
* never construct one yourself with "new." Instead, rely on {@link Router/RouteRecognizer} to
|
||||
* construct `ComponentInstruction`s.
|
||||
*
|
||||
* You should not modify this object. It should be treated as immutable.
|
||||
*/
|
||||
export abstract class ComponentInstruction {
|
||||
export class ComponentInstruction {
|
||||
reuse: boolean = false;
|
||||
public urlPath: string;
|
||||
public urlParams: string[];
|
||||
public params: {[key: string]: any};
|
||||
public routeData: RouteData;
|
||||
|
||||
/**
|
||||
* Returns the component type of the represented route, or `null` if this instruction
|
||||
* hasn't been resolved.
|
||||
*/
|
||||
get componentType() { return unimplemented(); };
|
||||
|
||||
/**
|
||||
* Returns a promise that will resolve to component type of the represented route.
|
||||
* If this instruction references an {@link AsyncRoute}, the `loader` function of that route
|
||||
* will run.
|
||||
*/
|
||||
abstract resolveComponentType(): Promise<Type>;
|
||||
|
||||
/**
|
||||
* Returns the specificity of the route associated with this `Instruction`.
|
||||
*/
|
||||
get specificity() { return unimplemented(); };
|
||||
|
||||
/**
|
||||
* Returns `true` if the component type of this instruction has no child {@link RouteConfig},
|
||||
* or `false` if it does.
|
||||
*/
|
||||
get terminal() { return unimplemented(); };
|
||||
|
||||
/**
|
||||
* Returns the route data of the given route that was specified in the {@link RouteDefinition},
|
||||
* or an empty object if no route data was specified.
|
||||
*/
|
||||
get routeData(): RouteData { return unimplemented(); };
|
||||
}
|
||||
|
||||
export class ComponentInstruction_ extends ComponentInstruction {
|
||||
private _routeData: RouteData;
|
||||
|
||||
constructor(urlPath: string, urlParams: string[], private _recognizer: PathRecognizer,
|
||||
params: {[key: string]: any} = null) {
|
||||
super();
|
||||
this.urlPath = urlPath;
|
||||
this.urlParams = urlParams;
|
||||
this.params = params;
|
||||
if (isPresent(this._recognizer.handler.data)) {
|
||||
this._routeData = new RouteData(this._recognizer.handler.data);
|
||||
} else {
|
||||
this._routeData = BLANK_ROUTE_DATA;
|
||||
}
|
||||
constructor(public urlPath: string, public urlParams: string[], data: RouteData,
|
||||
public componentType, public terminal: boolean, public specificity: number,
|
||||
public params: {[key: string]: any} = null) {
|
||||
this.routeData = isPresent(data) ? data : BLANK_ROUTE_DATA;
|
||||
}
|
||||
|
||||
get componentType() { return this._recognizer.handler.componentType; }
|
||||
resolveComponentType(): Promise<Type> { return this._recognizer.handler.resolveComponentType(); }
|
||||
get specificity() { return this._recognizer.specificity; }
|
||||
get terminal() { return this._recognizer.terminal; }
|
||||
get routeData(): RouteData { return this._routeData; }
|
||||
}
|
||||
|
@ -7,12 +7,9 @@ import {
|
||||
isBlank
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
|
||||
|
||||
import {Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
|
||||
import {RouteHandler} from './route_handler';
|
||||
import {Url, RootUrl, serializeParams} from './url_parser';
|
||||
import {ComponentInstruction, ComponentInstruction_} from './instruction';
|
||||
|
||||
class TouchMap {
|
||||
map: {[key: string]: string} = {};
|
||||
@ -33,7 +30,7 @@ class TouchMap {
|
||||
}
|
||||
|
||||
getUnused(): {[key: string]: any} {
|
||||
var unused: {[key: string]: any} = StringMapWrapper.create();
|
||||
var unused: {[key: string]: any} = {};
|
||||
var keys = StringMapWrapper.keys(this.keys);
|
||||
keys.forEach(key => unused[key] = StringMapWrapper.get(this.map, key));
|
||||
return unused;
|
||||
@ -126,7 +123,6 @@ function parsePathString(route: string): {[key: string]: any} {
|
||||
results.push(new StarSegment(match[1]));
|
||||
} else if (segment == '...') {
|
||||
if (i < limit) {
|
||||
// TODO (matsko): setup a proper error here `
|
||||
throw new BaseException(`Unexpected "..." before the end of the path for "${route}".`);
|
||||
}
|
||||
results.push(new ContinuationSegment());
|
||||
@ -175,23 +171,17 @@ function assertPath(path: string) {
|
||||
}
|
||||
}
|
||||
|
||||
export class PathMatch {
|
||||
constructor(public instruction: ComponentInstruction, public remaining: Url,
|
||||
public remainingAux: Url[]) {}
|
||||
}
|
||||
|
||||
// represents something like '/foo/:bar'
|
||||
/**
|
||||
* Parses a URL string using a given matcher DSL, and generates URLs from param maps
|
||||
*/
|
||||
export class PathRecognizer {
|
||||
private _segments: Segment[];
|
||||
specificity: number;
|
||||
terminal: boolean = true;
|
||||
hash: string;
|
||||
private _cache: Map<string, ComponentInstruction> = new Map<string, ComponentInstruction>();
|
||||
|
||||
|
||||
// TODO: cache component instruction instances by params and by ParsedUrl instance
|
||||
|
||||
constructor(public path: string, public handler: RouteHandler) {
|
||||
constructor(public path: string) {
|
||||
assertPath(path);
|
||||
var parsed = parsePathString(path);
|
||||
|
||||
@ -203,8 +193,7 @@ export class PathRecognizer {
|
||||
this.terminal = !(lastSegment instanceof ContinuationSegment);
|
||||
}
|
||||
|
||||
|
||||
recognize(beginningSegment: Url): PathMatch {
|
||||
recognize(beginningSegment: Url): {[key: string]: any} {
|
||||
var nextSegment = beginningSegment;
|
||||
var currentSegment: Url;
|
||||
var positionalParams = {};
|
||||
@ -247,7 +236,6 @@ export class PathRecognizer {
|
||||
var urlPath = captured.join('/');
|
||||
|
||||
var auxiliary;
|
||||
var instruction: ComponentInstruction;
|
||||
var urlParams;
|
||||
var allParams;
|
||||
if (isPresent(currentSegment)) {
|
||||
@ -267,12 +255,11 @@ export class PathRecognizer {
|
||||
auxiliary = [];
|
||||
urlParams = [];
|
||||
}
|
||||
instruction = this._getInstruction(urlPath, urlParams, this, allParams);
|
||||
return new PathMatch(instruction, nextSegment, auxiliary);
|
||||
return {urlPath, urlParams, allParams, auxiliary, nextSegment};
|
||||
}
|
||||
|
||||
|
||||
generate(params: {[key: string]: any}): ComponentInstruction {
|
||||
generate(params: {[key: string]: any}): {[key: string]: any} {
|
||||
var paramTokens = new TouchMap(params);
|
||||
|
||||
var path = [];
|
||||
@ -288,18 +275,6 @@ export class PathRecognizer {
|
||||
var nonPositionalParams = paramTokens.getUnused();
|
||||
var urlParams = serializeParams(nonPositionalParams);
|
||||
|
||||
return this._getInstruction(urlPath, urlParams, this, params);
|
||||
}
|
||||
|
||||
private _getInstruction(urlPath: string, urlParams: string[], _recognizer: PathRecognizer,
|
||||
params: {[key: string]: any}): ComponentInstruction {
|
||||
var hashKey = urlPath + '?' + urlParams.join('?');
|
||||
if (this._cache.has(hashKey)) {
|
||||
return this._cache.get(hashKey);
|
||||
}
|
||||
var instruction = new ComponentInstruction_(urlPath, urlParams, _recognizer, params);
|
||||
this._cache.set(hashKey, instruction);
|
||||
|
||||
return instruction;
|
||||
return {urlPath, urlParams};
|
||||
}
|
||||
}
|
||||
|
@ -21,6 +21,8 @@ export class RouteConfig {
|
||||
* - `name` is an optional `CamelCase` string representing the name of the route.
|
||||
* - `data` is an optional property of any type representing arbitrary route metadata for the given
|
||||
* route. It is injectable via {@link RouteData}.
|
||||
* - `useAsDefault` is a boolean value. If `true`, the child route will be navigated to if no child
|
||||
* route is specified during the navigation.
|
||||
*
|
||||
* ### Example
|
||||
* ```
|
||||
@ -38,16 +40,20 @@ export class Route implements RouteDefinition {
|
||||
path: string;
|
||||
component: Type;
|
||||
name: string;
|
||||
useAsDefault: boolean;
|
||||
// added next three properties to work around https://github.com/Microsoft/TypeScript/issues/4107
|
||||
aux: string = null;
|
||||
loader: Function = null;
|
||||
redirectTo: string = null;
|
||||
constructor({path, component, name,
|
||||
data}: {path: string, component: Type, name?: string, data?: {[key: string]: any}}) {
|
||||
redirectTo: any[] = null;
|
||||
constructor({path, component, name, data, useAsDefault}: {
|
||||
path: string,
|
||||
component: Type, name?: string, data?: {[key: string]: any}, useAsDefault?: boolean
|
||||
}) {
|
||||
this.path = path;
|
||||
this.component = component;
|
||||
this.name = name;
|
||||
this.data = data;
|
||||
this.useAsDefault = useAsDefault;
|
||||
}
|
||||
}
|
||||
|
||||
@ -80,7 +86,8 @@ export class AuxRoute implements RouteDefinition {
|
||||
// added next three properties to work around https://github.com/Microsoft/TypeScript/issues/4107
|
||||
aux: string = null;
|
||||
loader: Function = null;
|
||||
redirectTo: string = null;
|
||||
redirectTo: any[] = null;
|
||||
useAsDefault: boolean = false;
|
||||
constructor({path, component, name}: {path: string, component: Type, name?: string}) {
|
||||
this.path = path;
|
||||
this.component = component;
|
||||
@ -98,6 +105,8 @@ export class AuxRoute implements RouteDefinition {
|
||||
* - `name` is an optional `CamelCase` string representing the name of the route.
|
||||
* - `data` is an optional property of any type representing arbitrary route metadata for the given
|
||||
* route. It is injectable via {@link RouteData}.
|
||||
* - `useAsDefault` is a boolean value. If `true`, the child route will be navigated to if no child
|
||||
* route is specified during the navigation.
|
||||
*
|
||||
* ### Example
|
||||
* ```
|
||||
@ -115,31 +124,37 @@ export class AsyncRoute implements RouteDefinition {
|
||||
path: string;
|
||||
loader: Function;
|
||||
name: string;
|
||||
useAsDefault: boolean;
|
||||
aux: string = null;
|
||||
constructor({path, loader, name, data}:
|
||||
{path: string, loader: Function, name?: string, data?: {[key: string]: any}}) {
|
||||
constructor({path, loader, name, data, useAsDefault}: {
|
||||
path: string,
|
||||
loader: Function, name?: string, data?: {[key: string]: any}, useAsDefault?: boolean
|
||||
}) {
|
||||
this.path = path;
|
||||
this.loader = loader;
|
||||
this.name = name;
|
||||
this.data = data;
|
||||
this.useAsDefault = useAsDefault;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* `Redirect` is a type of {@link RouteDefinition} used to route a path to an asynchronously loaded
|
||||
* component.
|
||||
* `Redirect` is a type of {@link RouteDefinition} used to route a path to a canonical route.
|
||||
*
|
||||
* It has the following properties:
|
||||
* - `path` is a string that uses the route matcher DSL.
|
||||
* - `redirectTo` is a string representing the new URL to be matched against.
|
||||
* - `redirectTo` is an array representing the link DSL.
|
||||
*
|
||||
* Note that redirects **do not** affect how links are generated. For that, see the `useAsDefault`
|
||||
* option.
|
||||
*
|
||||
* ### Example
|
||||
* ```
|
||||
* import {RouteConfig} from 'angular2/router';
|
||||
*
|
||||
* @RouteConfig([
|
||||
* {path: '/', redirectTo: '/home'},
|
||||
* {path: '/home', component: HomeCmp}
|
||||
* {path: '/', redirectTo: ['/Home'] },
|
||||
* {path: '/home', component: HomeCmp, name: 'Home'}
|
||||
* ])
|
||||
* class MyApp {}
|
||||
* ```
|
||||
@ -147,13 +162,14 @@ export class AsyncRoute implements RouteDefinition {
|
||||
@CONST()
|
||||
export class Redirect implements RouteDefinition {
|
||||
path: string;
|
||||
redirectTo: string;
|
||||
redirectTo: any[];
|
||||
name: string = null;
|
||||
// added next three properties to work around https://github.com/Microsoft/TypeScript/issues/4107
|
||||
loader: Function = null;
|
||||
data: any = null;
|
||||
aux: string = null;
|
||||
constructor({path, redirectTo}: {path: string, redirectTo: string}) {
|
||||
useAsDefault: boolean = false;
|
||||
constructor({path, redirectTo}: {path: string, redirectTo: any[]}) {
|
||||
this.path = path;
|
||||
this.redirectTo = redirectTo;
|
||||
}
|
||||
|
@ -1,9 +1,22 @@
|
||||
library angular2.src.router.route_config_normalizer;
|
||||
|
||||
import "route_config_decorator.dart";
|
||||
import "route_registry.dart";
|
||||
import "package:angular2/src/facade/exceptions.dart" show BaseException;
|
||||
|
||||
RouteDefinition normalizeRouteConfig(RouteDefinition config) {
|
||||
RouteDefinition normalizeRouteConfig(RouteDefinition config, RouteRegistry registry) {
|
||||
if (config is AsyncRoute) {
|
||||
|
||||
configRegistryAndReturnType(componentType) {
|
||||
registry.configFromComponent(componentType);
|
||||
return componentType;
|
||||
}
|
||||
|
||||
loader() {
|
||||
return config.loader().then(configRegistryAndReturnType);
|
||||
}
|
||||
return new AsyncRoute(path: config.path, loader: loader, name: config.name, data: config.data, useAsDefault: config.useAsDefault);
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
|
@ -2,14 +2,29 @@ import {AsyncRoute, AuxRoute, Route, Redirect, RouteDefinition} from './route_co
|
||||
import {ComponentDefinition} from './route_definition';
|
||||
import {isType, Type} from 'angular2/src/facade/lang';
|
||||
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
|
||||
import {RouteRegistry} from './route_registry';
|
||||
|
||||
|
||||
/**
|
||||
* Given a JS Object that represents... returns a corresponding Route, AsyncRoute, or Redirect
|
||||
* Given a JS Object that represents a route config, returns a corresponding Route, AsyncRoute,
|
||||
* AuxRoute or Redirect object.
|
||||
*
|
||||
* Also wraps an AsyncRoute's loader function to add the loaded component's route config to the
|
||||
* `RouteRegistry`.
|
||||
*/
|
||||
export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
||||
if (config instanceof Route || config instanceof Redirect || config instanceof AsyncRoute ||
|
||||
config instanceof AuxRoute) {
|
||||
export function normalizeRouteConfig(config: RouteDefinition,
|
||||
registry: RouteRegistry): RouteDefinition {
|
||||
if (config instanceof AsyncRoute) {
|
||||
var wrappedLoader = wrapLoaderToReconfigureRegistry(config.loader, registry);
|
||||
return new AsyncRoute({
|
||||
path: config.path,
|
||||
loader: wrappedLoader,
|
||||
name: config.name,
|
||||
data: config.data,
|
||||
useAsDefault: config.useAsDefault
|
||||
});
|
||||
}
|
||||
if (config instanceof Route || config instanceof Redirect || config instanceof AuxRoute) {
|
||||
return <RouteDefinition>config;
|
||||
}
|
||||
|
||||
@ -24,7 +39,13 @@ export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
||||
config.name = config.as;
|
||||
}
|
||||
if (config.loader) {
|
||||
return new AsyncRoute({path: config.path, loader: config.loader, name: config.name});
|
||||
var wrappedLoader = wrapLoaderToReconfigureRegistry(config.loader, registry);
|
||||
return new AsyncRoute({
|
||||
path: config.path,
|
||||
loader: wrappedLoader,
|
||||
name: config.name,
|
||||
useAsDefault: config.useAsDefault
|
||||
});
|
||||
}
|
||||
if (config.aux) {
|
||||
return new AuxRoute({path: config.aux, component:<Type>config.component, name: config.name});
|
||||
@ -36,11 +57,17 @@ export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
||||
return new Route({
|
||||
path: config.path,
|
||||
component:<Type>componentDefinitionObject.constructor,
|
||||
name: config.name
|
||||
name: config.name,
|
||||
data: config.data,
|
||||
useAsDefault: config.useAsDefault
|
||||
});
|
||||
} else if (componentDefinitionObject.type == 'loader') {
|
||||
return new AsyncRoute(
|
||||
{path: config.path, loader: componentDefinitionObject.loader, name: config.name});
|
||||
return new AsyncRoute({
|
||||
path: config.path,
|
||||
loader: componentDefinitionObject.loader,
|
||||
name: config.name,
|
||||
useAsDefault: config.useAsDefault
|
||||
});
|
||||
} else {
|
||||
throw new BaseException(
|
||||
`Invalid component type "${componentDefinitionObject.type}". Valid types are "constructor" and "loader".`);
|
||||
@ -50,6 +77,8 @@ export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
||||
path: string;
|
||||
component: Type;
|
||||
name?: string;
|
||||
data?: {[key: string]: any};
|
||||
useAsDefault?: boolean;
|
||||
}>config);
|
||||
}
|
||||
|
||||
@ -60,6 +89,16 @@ export function normalizeRouteConfig(config: RouteDefinition): RouteDefinition {
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
function wrapLoaderToReconfigureRegistry(loader: Function, registry: RouteRegistry): Function {
|
||||
return () => {
|
||||
return loader().then((componentType) => {
|
||||
registry.configFromComponent(componentType);
|
||||
return componentType;
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
export function assertComponentExists(component: Type, path: string): void {
|
||||
if (!isType(component)) {
|
||||
throw new BaseException(`Component for route "${path}" is not defined, or is not a class.`);
|
||||
|
@ -3,5 +3,6 @@ library angular2.src.router.route_definition;
|
||||
abstract class RouteDefinition {
|
||||
final String path;
|
||||
final String name;
|
||||
const RouteDefinition({this.path, this.name});
|
||||
final bool useAsDefault;
|
||||
const RouteDefinition({this.path, this.name, this.useAsDefault : false});
|
||||
}
|
||||
|
@ -16,10 +16,11 @@ export interface RouteDefinition {
|
||||
aux?: string;
|
||||
component?: Type | ComponentDefinition;
|
||||
loader?: Function;
|
||||
redirectTo?: string;
|
||||
redirectTo?: any[];
|
||||
as?: string;
|
||||
name?: string;
|
||||
data?: any;
|
||||
useAsDefault?: boolean;
|
||||
}
|
||||
|
||||
export interface ComponentDefinition {
|
||||
|
@ -1,8 +1,9 @@
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
import {Type} from 'angular2/src/facade/lang';
|
||||
import {RouteData} from './instruction';
|
||||
|
||||
export interface RouteHandler {
|
||||
componentType: Type;
|
||||
resolveComponentType(): Promise<any>;
|
||||
data?: {[key: string]: any};
|
||||
data: RouteData;
|
||||
}
|
||||
|
@ -1,184 +1,119 @@
|
||||
import {
|
||||
RegExp,
|
||||
RegExpWrapper,
|
||||
isBlank,
|
||||
isPresent,
|
||||
isType,
|
||||
isStringMap,
|
||||
Type
|
||||
} from 'angular2/src/facade/lang';
|
||||
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
|
||||
import {Map, MapWrapper, ListWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {isPresent, isBlank} from 'angular2/src/facade/lang';
|
||||
import {BaseException} from 'angular2/src/facade/exceptions';
|
||||
import {PromiseWrapper, Promise} from 'angular2/src/facade/promise';
|
||||
import {Map} from 'angular2/src/facade/collection';
|
||||
|
||||
import {PathRecognizer, PathMatch} from './path_recognizer';
|
||||
import {Route, AsyncRoute, AuxRoute, Redirect, RouteDefinition} from './route_config_impl';
|
||||
import {AsyncRouteHandler} from './async_route_handler';
|
||||
import {SyncRouteHandler} from './sync_route_handler';
|
||||
import {RouteHandler} from './route_handler';
|
||||
import {Url} from './url_parser';
|
||||
import {ComponentInstruction} from './instruction';
|
||||
import {PathRecognizer} from './path_recognizer';
|
||||
|
||||
|
||||
/**
|
||||
* `RouteRecognizer` is responsible for recognizing routes for a single component.
|
||||
* It is consumed by `RouteRegistry`, which knows how to recognize an entire hierarchy of
|
||||
* components.
|
||||
*/
|
||||
export class RouteRecognizer {
|
||||
names = new Map<string, PathRecognizer>();
|
||||
export abstract class RouteMatch {}
|
||||
|
||||
// map from name to recognizer
|
||||
auxNames = new Map<string, PathRecognizer>();
|
||||
|
||||
// map from starting path to recognizer
|
||||
auxRoutes = new Map<string, PathRecognizer>();
|
||||
|
||||
// TODO: optimize this into a trie
|
||||
matchers: PathRecognizer[] = [];
|
||||
|
||||
// TODO: optimize this into a trie
|
||||
redirects: Redirector[] = [];
|
||||
|
||||
config(config: RouteDefinition): boolean {
|
||||
var handler;
|
||||
|
||||
if (isPresent(config.name) && config.name[0].toUpperCase() != config.name[0]) {
|
||||
var suggestedName = config.name[0].toUpperCase() + config.name.substring(1);
|
||||
throw new BaseException(
|
||||
`Route "${config.path}" with name "${config.name}" does not begin with an uppercase letter. Route names should be CamelCase like "${suggestedName}".`);
|
||||
}
|
||||
|
||||
if (config instanceof AuxRoute) {
|
||||
handler = new SyncRouteHandler(config.component, config.data);
|
||||
let path = config.path.startsWith('/') ? config.path.substring(1) : config.path;
|
||||
var recognizer = new PathRecognizer(config.path, handler);
|
||||
this.auxRoutes.set(path, recognizer);
|
||||
if (isPresent(config.name)) {
|
||||
this.auxNames.set(config.name, recognizer);
|
||||
}
|
||||
return recognizer.terminal;
|
||||
}
|
||||
|
||||
if (config instanceof Redirect) {
|
||||
this.redirects.push(new Redirector(config.path, config.redirectTo));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (config instanceof Route) {
|
||||
handler = new SyncRouteHandler(config.component, config.data);
|
||||
} else if (config instanceof AsyncRoute) {
|
||||
handler = new AsyncRouteHandler(config.loader, config.data);
|
||||
}
|
||||
var recognizer = new PathRecognizer(config.path, handler);
|
||||
|
||||
this.matchers.forEach((matcher) => {
|
||||
if (recognizer.hash == matcher.hash) {
|
||||
throw new BaseException(
|
||||
`Configuration '${config.path}' conflicts with existing route '${matcher.path}'`);
|
||||
}
|
||||
});
|
||||
|
||||
this.matchers.push(recognizer);
|
||||
if (isPresent(config.name)) {
|
||||
this.names.set(config.name, recognizer);
|
||||
}
|
||||
return recognizer.terminal;
|
||||
}
|
||||
export interface AbstractRecognizer {
|
||||
hash: string;
|
||||
path: string;
|
||||
recognize(beginningSegment: Url): Promise<RouteMatch>;
|
||||
generate(params: {[key: string]: any}): ComponentInstruction;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Given a URL, returns a list of `RouteMatch`es, which are partial recognitions for some route.
|
||||
*
|
||||
*/
|
||||
recognize(urlParse: Url): PathMatch[] {
|
||||
var solutions = [];
|
||||
|
||||
urlParse = this._redirect(urlParse);
|
||||
|
||||
this.matchers.forEach((pathRecognizer: PathRecognizer) => {
|
||||
var pathMatch = pathRecognizer.recognize(urlParse);
|
||||
|
||||
if (isPresent(pathMatch)) {
|
||||
solutions.push(pathMatch);
|
||||
}
|
||||
});
|
||||
|
||||
return solutions;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
_redirect(urlParse: Url): Url {
|
||||
for (var i = 0; i < this.redirects.length; i += 1) {
|
||||
let redirector = this.redirects[i];
|
||||
var redirectedUrl = redirector.redirect(urlParse);
|
||||
if (isPresent(redirectedUrl)) {
|
||||
return redirectedUrl;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
return urlParse;
|
||||
generate(params: {[key: string]: any}): ComponentInstruction {
|
||||
throw new BaseException(`Tried to generate a redirect.`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// represents something like '/foo/:bar'
|
||||
export class RouteRecognizer implements AbstractRecognizer {
|
||||
specificity: number;
|
||||
terminal: boolean = true;
|
||||
hash: string;
|
||||
|
||||
private _cache: Map<string, ComponentInstruction> = new Map<string, ComponentInstruction>();
|
||||
private _pathRecognizer: PathRecognizer;
|
||||
|
||||
// TODO: cache component instruction instances by params and by ParsedUrl instance
|
||||
|
||||
constructor(public path: string, public handler: RouteHandler) {
|
||||
this._pathRecognizer = new PathRecognizer(path);
|
||||
this.specificity = this._pathRecognizer.specificity;
|
||||
this.hash = this._pathRecognizer.hash;
|
||||
this.terminal = this._pathRecognizer.terminal;
|
||||
}
|
||||
|
||||
recognize(beginningSegment: Url): Promise<RouteMatch> {
|
||||
var res = this._pathRecognizer.recognize(beginningSegment);
|
||||
if (isBlank(res)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return this.handler.resolveComponentType().then((_) => {
|
||||
var componentInstruction =
|
||||
this._getInstruction(res['urlPath'], res['urlParams'], res['allParams']);
|
||||
return new PathMatch(componentInstruction, res['nextSegment'], res['auxiliary']);
|
||||
});
|
||||
}
|
||||
|
||||
generate(params: {[key: string]: any}): ComponentInstruction {
|
||||
var generated = this._pathRecognizer.generate(params);
|
||||
var urlPath = generated['urlPath'];
|
||||
var urlParams = generated['urlParams'];
|
||||
return this._getInstruction(urlPath, urlParams, params);
|
||||
}
|
||||
|
||||
generateComponentPathValues(params: {[key: string]: any}): {[key: string]: any} {
|
||||
return this._pathRecognizer.generate(params);
|
||||
}
|
||||
|
||||
private _getInstruction(urlPath: string, urlParams: string[],
|
||||
params: {[key: string]: any}): ComponentInstruction {
|
||||
if (isBlank(this.handler.componentType)) {
|
||||
throw new BaseException(`Tried to get instruction before the type was loaded.`);
|
||||
}
|
||||
|
||||
var hashKey = urlPath + '?' + urlParams.join('?');
|
||||
if (this._cache.has(hashKey)) {
|
||||
return this._cache.get(hashKey);
|
||||
}
|
||||
var instruction =
|
||||
new ComponentInstruction(urlPath, urlParams, this.handler.data, this.handler.componentType,
|
||||
this.terminal, this.specificity, params);
|
||||
this._cache.set(hashKey, instruction);
|
||||
|
||||
return instruction;
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,3 @@
|
||||
import {PathMatch} from './path_recognizer';
|
||||
import {RouteRecognizer} from './route_recognizer';
|
||||
import {Instruction, ComponentInstruction, PrimaryInstruction} from './instruction';
|
||||
import {ListWrapper, Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
import {
|
||||
@ -16,6 +13,7 @@ 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,
|
||||
@ -24,7 +22,16 @@ import {
|
||||
Redirect,
|
||||
RouteDefinition
|
||||
} from './route_config_impl';
|
||||
import {reflector} from 'angular2/src/core/reflection/reflection';
|
||||
import {PathMatch, RedirectMatch, RouteMatch} from './route_recognizer';
|
||||
import {ComponentRecognizer} from './component_recognizer';
|
||||
import {
|
||||
Instruction,
|
||||
ResolvedInstruction,
|
||||
RedirectInstruction,
|
||||
UnresolvedInstruction,
|
||||
DefaultInstruction
|
||||
} from './instruction';
|
||||
|
||||
import {Injectable} from 'angular2/angular2';
|
||||
import {normalizeRouteConfig, assertComponentExists} from './route_config_nomalizer';
|
||||
import {parser, Url, pathSegmentsToUrl} from './url_parser';
|
||||
@ -38,13 +45,13 @@ var _resolveToNull = PromiseWrapper.resolve(null);
|
||||
*/
|
||||
@Injectable()
|
||||
export class RouteRegistry {
|
||||
private _rules = new Map<any, RouteRecognizer>();
|
||||
private _rules = new Map<any, ComponentRecognizer>();
|
||||
|
||||
/**
|
||||
* Given a component and a configuration object, add the route to this registry
|
||||
*/
|
||||
config(parentComponent: any, config: RouteDefinition): void {
|
||||
config = normalizeRouteConfig(config);
|
||||
config = normalizeRouteConfig(config, this);
|
||||
|
||||
// this is here because Dart type guard reasons
|
||||
if (config instanceof Route) {
|
||||
@ -53,10 +60,10 @@ export class RouteRegistry {
|
||||
assertComponentExists(config.component, config.path);
|
||||
}
|
||||
|
||||
var recognizer: RouteRecognizer = this._rules.get(parentComponent);
|
||||
var recognizer: ComponentRecognizer = this._rules.get(parentComponent);
|
||||
|
||||
if (isBlank(recognizer)) {
|
||||
recognizer = new RouteRecognizer();
|
||||
recognizer = new ComponentRecognizer();
|
||||
this._rules.set(parentComponent, recognizer);
|
||||
}
|
||||
|
||||
@ -102,102 +109,162 @@ export class RouteRegistry {
|
||||
* Given a URL and a parent component, return the most specific instruction for navigating
|
||||
* the application into the state specified by the url
|
||||
*/
|
||||
recognize(url: string, parentComponent: any): Promise<Instruction> {
|
||||
recognize(url: string, ancestorComponents: any[]): Promise<Instruction> {
|
||||
var parsedUrl = parser.parse(url);
|
||||
return this._recognize(parsedUrl, parentComponent);
|
||||
return this._recognize(parsedUrl, ancestorComponents);
|
||||
}
|
||||
|
||||
private _recognize(parsedUrl: Url, parentComponent): Promise<Instruction> {
|
||||
return this._recognizePrimaryRoute(parsedUrl, parentComponent)
|
||||
.then((instruction: PrimaryInstruction) =>
|
||||
this._completeAuxiliaryRouteMatches(instruction, parentComponent));
|
||||
}
|
||||
|
||||
private _recognizePrimaryRoute(parsedUrl: Url, parentComponent): Promise<PrimaryInstruction> {
|
||||
/**
|
||||
* Recognizes all parent-child routes, but creates unresolved auxiliary routes
|
||||
*/
|
||||
|
||||
private _recognize(parsedUrl: Url, ancestorComponents: any[],
|
||||
_aux = false): Promise<Instruction> {
|
||||
var parentComponent = ancestorComponents[ancestorComponents.length - 1];
|
||||
var componentRecognizer = this._rules.get(parentComponent);
|
||||
if (isBlank(componentRecognizer)) {
|
||||
return _resolveToNull;
|
||||
}
|
||||
|
||||
// Matches some beginning part of the given URL
|
||||
var possibleMatches = componentRecognizer.recognize(parsedUrl);
|
||||
var possibleMatches: Promise<RouteMatch>[] =
|
||||
_aux ? componentRecognizer.recognizeAuxiliary(parsedUrl) :
|
||||
componentRecognizer.recognize(parsedUrl);
|
||||
|
||||
var matchPromises =
|
||||
possibleMatches.map(candidate => this._completePrimaryRouteMatch(candidate));
|
||||
var matchPromises: Promise<Instruction>[] = possibleMatches.map(
|
||||
(candidate: Promise<RouteMatch>) => candidate.then((candidate: RouteMatch) => {
|
||||
|
||||
if (candidate instanceof PathMatch) {
|
||||
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));
|
||||
}
|
||||
|
||||
return PromiseWrapper.all(matchPromises).then(mostSpecific);
|
||||
}
|
||||
|
||||
private _completePrimaryRouteMatch(partialMatch: PathMatch): Promise<PrimaryInstruction> {
|
||||
var instruction = partialMatch.instruction;
|
||||
return instruction.resolveComponentType().then((componentType) => {
|
||||
this.configFromComponent(componentType);
|
||||
private _auxRoutesToUnresolved(auxRoutes: Url[], parentComponent): {[key: string]: Instruction} {
|
||||
var unresolvedAuxInstructions: {[key: string]: Instruction} = {};
|
||||
|
||||
if (instruction.terminal) {
|
||||
return new PrimaryInstruction(instruction, null, partialMatch.remainingAux);
|
||||
}
|
||||
|
||||
return this._recognizePrimaryRoute(partialMatch.remaining, componentType)
|
||||
.then((childInstruction) => {
|
||||
if (isBlank(childInstruction)) {
|
||||
return null;
|
||||
} else {
|
||||
return new PrimaryInstruction(instruction, childInstruction,
|
||||
partialMatch.remainingAux);
|
||||
}
|
||||
});
|
||||
auxRoutes.forEach((auxUrl: Url) => {
|
||||
unresolvedAuxInstructions[auxUrl.path] = new UnresolvedInstruction(
|
||||
() => { return this._recognize(auxUrl, [parentComponent], true); });
|
||||
});
|
||||
|
||||
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[], parentComponent: any, _aux = false): Instruction {
|
||||
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);
|
||||
}
|
||||
let linkIndex = 0;
|
||||
let routeName = linkParams[linkIndex];
|
||||
|
||||
// TODO: this is kind of odd but it makes existing assertions pass
|
||||
if (isBlank(parentComponent)) {
|
||||
throw new BaseException(`Could not find route named "${routeName}".`);
|
||||
}
|
||||
|
||||
if (!isString(routeName)) {
|
||||
throw new BaseException(`Unexpected segment "${routeName}" in link DSL. Expected a string.`);
|
||||
} else if (routeName == '' || routeName == '.' || routeName == '..') {
|
||||
@ -216,7 +283,10 @@ export class RouteRegistry {
|
||||
let auxInstructions: {[key: string]: Instruction} = {};
|
||||
var nextSegment;
|
||||
while (linkIndex + 1 < linkParams.length && isArray(nextSegment = linkParams[linkIndex + 1])) {
|
||||
auxInstructions[nextSegment[0]] = this.generate(nextSegment, parentComponent, true);
|
||||
let auxInstruction = this._generate(nextSegment, [parentComponent], true);
|
||||
|
||||
// TODO: this will not work for aux routes with parameters or multiple segments
|
||||
auxInstructions[auxInstruction.component.urlPath] = auxInstruction;
|
||||
linkIndex += 1;
|
||||
}
|
||||
|
||||
@ -226,74 +296,105 @@ export class RouteRegistry {
|
||||
`Component "${getTypeNameForDebugging(parentComponent)}" has no route config.`);
|
||||
}
|
||||
|
||||
var componentInstruction = _aux ? componentRecognizer.generateAuxiliary(routeName, params) :
|
||||
componentRecognizer.generate(routeName, params);
|
||||
var routeRecognizer =
|
||||
(_aux ? componentRecognizer.auxNames : componentRecognizer.names).get(routeName);
|
||||
|
||||
if (isBlank(componentInstruction)) {
|
||||
if (!isPresent(routeRecognizer)) {
|
||||
throw new BaseException(
|
||||
`Component "${getTypeNameForDebugging(parentComponent)}" has no route named "${routeName}".`);
|
||||
}
|
||||
|
||||
var childInstruction = null;
|
||||
if (linkIndex + 1 < linkParams.length) {
|
||||
var remaining = linkParams.slice(linkIndex + 1);
|
||||
childInstruction = this.generate(remaining, componentInstruction.componentType);
|
||||
} else if (!componentInstruction.terminal) {
|
||||
throw new BaseException(
|
||||
`Link "${ListWrapper.toJSON(linkParams)}" does not resolve to a terminal or async instruction.`);
|
||||
if (!isPresent(routeRecognizer.handler.componentType)) {
|
||||
var compInstruction = routeRecognizer.generateComponentPathValues(params);
|
||||
return new UnresolvedInstruction(() => {
|
||||
return routeRecognizer.handler.resolveComponentType().then(
|
||||
(_) => { return this._generate(linkParams, ancestorComponents, _aux); });
|
||||
}, compInstruction['urlPath'], compInstruction['urlParams']);
|
||||
}
|
||||
|
||||
return new Instruction(componentInstruction, childInstruction, auxInstructions);
|
||||
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);
|
||||
}
|
||||
|
||||
public hasRoute(name: string, parentComponent: any): boolean {
|
||||
var componentRecognizer: RouteRecognizer = this._rules.get(parentComponent);
|
||||
var componentRecognizer: ComponentRecognizer = this._rules.get(parentComponent);
|
||||
if (isBlank(componentRecognizer)) {
|
||||
return false;
|
||||
}
|
||||
return componentRecognizer.hasRoute(name);
|
||||
}
|
||||
|
||||
// if the child includes a redirect like : "/" -> "/something",
|
||||
// we want to honor that redirection when creating the link
|
||||
private _generateRedirects(componentCursor: Type): Instruction {
|
||||
public generateDefault(componentCursor: Type): Instruction {
|
||||
if (isBlank(componentCursor)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var componentRecognizer = this._rules.get(componentCursor);
|
||||
if (isBlank(componentRecognizer)) {
|
||||
if (isBlank(componentRecognizer) || isBlank(componentRecognizer.defaultRoute)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
for (let i = 0; i < componentRecognizer.redirects.length; i += 1) {
|
||||
let redirect = componentRecognizer.redirects[i];
|
||||
|
||||
// we only handle redirecting from an empty segment
|
||||
if (redirect.segments.length == 1 && redirect.segments[0] == '') {
|
||||
var toSegments = pathSegmentsToUrl(redirect.toSegments);
|
||||
var matches = componentRecognizer.recognize(toSegments);
|
||||
var primaryInstruction =
|
||||
ListWrapper.maximum(matches, (match: PathMatch) => match.instruction.specificity);
|
||||
|
||||
if (isPresent(primaryInstruction)) {
|
||||
var child = this._generateRedirects(primaryInstruction.instruction.componentType);
|
||||
return new Instruction(primaryInstruction.instruction, child, {});
|
||||
}
|
||||
return null;
|
||||
var defaultChild = null;
|
||||
if (isPresent(componentRecognizer.defaultRoute.handler.componentType)) {
|
||||
var componentInstruction = componentRecognizer.defaultRoute.generate({});
|
||||
if (!componentRecognizer.defaultRoute.terminal) {
|
||||
defaultChild = this.generateDefault(componentRecognizer.defaultRoute.handler.componentType);
|
||||
}
|
||||
return new DefaultInstruction(componentInstruction, defaultChild);
|
||||
}
|
||||
|
||||
return null;
|
||||
return new UnresolvedInstruction(() => {
|
||||
return componentRecognizer.defaultRoute.handler.resolveComponentType().then(
|
||||
() => this.generateDefault(componentCursor));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Given: ['/a/b', {c: 2}]
|
||||
* Returns: ['', 'a', 'b', {c: 2}]
|
||||
*/
|
||||
function splitAndFlattenLinkParams(linkParams: any[]): any[] {
|
||||
return linkParams.reduce((accumulation: any[], item) => {
|
||||
if (isString(item)) {
|
||||
let strItem: string = item;
|
||||
return accumulation.concat(strItem.split('/'));
|
||||
}
|
||||
accumulation.push(item);
|
||||
return accumulation;
|
||||
}, []);
|
||||
}
|
||||
|
||||
/*
|
||||
* Given a list of instructions, returns the most specific instruction
|
||||
*/
|
||||
function mostSpecific(instructions: PrimaryInstruction[]): PrimaryInstruction {
|
||||
return ListWrapper.maximum(
|
||||
instructions, (instruction: PrimaryInstruction) => instruction.component.specificity);
|
||||
function mostSpecific(instructions: Instruction[]): Instruction {
|
||||
return ListWrapper.maximum(instructions, (instruction: Instruction) => instruction.specificity);
|
||||
}
|
||||
|
||||
function assertTerminalComponent(component, path) {
|
||||
|
@ -6,9 +6,6 @@ import {RouteRegistry} from './route_registry';
|
||||
import {
|
||||
ComponentInstruction,
|
||||
Instruction,
|
||||
stringifyInstruction,
|
||||
stringifyInstructionPath,
|
||||
stringifyInstructionQuery
|
||||
} from './instruction';
|
||||
import {RouterOutlet} from './router_outlet';
|
||||
import {Location} from './location';
|
||||
@ -212,7 +209,7 @@ export class Router {
|
||||
if (result) {
|
||||
return this.commit(instruction, _skipLocationChange)
|
||||
.then((_) => {
|
||||
this._emitNavigationFinish(stringifyInstruction(instruction));
|
||||
this._emitNavigationFinish(instruction.toRootUrl());
|
||||
return true;
|
||||
});
|
||||
}
|
||||
@ -220,25 +217,20 @@ export class Router {
|
||||
});
|
||||
}
|
||||
|
||||
// TODO(btford): it'd be nice to remove this method as part of cleaning up the traversal logic
|
||||
// Since refactoring `Router.generate` to return an instruction rather than a string, it's not
|
||||
// guaranteed that the `componentType`s for the terminal async routes have been loaded by the time
|
||||
// we begin navigation. The method below simply traverses instructions and resolves any components
|
||||
// for which `componentType` is not present
|
||||
/** @internal */
|
||||
_settleInstruction(instruction: Instruction): Promise<any> {
|
||||
var unsettledInstructions: Array<Promise<any>> = [];
|
||||
if (isBlank(instruction.component.componentType)) {
|
||||
unsettledInstructions.push(instruction.component.resolveComponentType().then(
|
||||
(type: Type) => { this.registry.configFromComponent(type); }));
|
||||
}
|
||||
if (isPresent(instruction.child)) {
|
||||
unsettledInstructions.push(this._settleInstruction(instruction.child));
|
||||
}
|
||||
StringMapWrapper.forEach(instruction.auxInstruction, (instruction, _) => {
|
||||
unsettledInstructions.push(this._settleInstruction(instruction));
|
||||
return instruction.resolveComponent().then((_) => {
|
||||
var unsettledInstructions: Array<Promise<any>> = [];
|
||||
|
||||
if (isPresent(instruction.child)) {
|
||||
unsettledInstructions.push(this._settleInstruction(instruction.child));
|
||||
}
|
||||
|
||||
StringMapWrapper.forEach(instruction.auxInstruction, (instruction, _) => {
|
||||
unsettledInstructions.push(this._settleInstruction(instruction));
|
||||
});
|
||||
return PromiseWrapper.all(unsettledInstructions);
|
||||
});
|
||||
return PromiseWrapper.all(unsettledInstructions);
|
||||
}
|
||||
|
||||
private _emitNavigationFinish(url): void { ObservableWrapper.callEmit(this._subject, url); }
|
||||
@ -378,7 +370,22 @@ export class Router {
|
||||
* Given a URL, returns an instruction representing the component graph
|
||||
*/
|
||||
recognize(url: string): Promise<Instruction> {
|
||||
return this.registry.recognize(url, this.hostComponent);
|
||||
var ancestorComponents = this._getAncestorComponents();
|
||||
return this.registry.recognize(url, ancestorComponents);
|
||||
}
|
||||
|
||||
/**
|
||||
* get all the host components for this and
|
||||
*/
|
||||
private _getAncestorComponents(): any[] {
|
||||
var ancestorComponents = [];
|
||||
var ancestorRouter = this;
|
||||
do {
|
||||
ancestorComponents.unshift(ancestorRouter.hostComponent);
|
||||
ancestorRouter = ancestorRouter.parent;
|
||||
} while (isPresent(ancestorRouter));
|
||||
|
||||
return ancestorComponents;
|
||||
}
|
||||
|
||||
|
||||
@ -399,67 +406,27 @@ export class Router {
|
||||
* app's base href.
|
||||
*/
|
||||
generate(linkParams: any[]): Instruction {
|
||||
let normalizedLinkParams = splitAndFlattenLinkParams(linkParams);
|
||||
var ancestorComponents = this._getAncestorComponents();
|
||||
var startingNumberOfAncestors = ancestorComponents.length;
|
||||
|
||||
var first = ListWrapper.first(normalizedLinkParams);
|
||||
var rest = ListWrapper.slice(normalizedLinkParams, 1);
|
||||
var nextInstruction = this.registry.generate(linkParams, ancestorComponents);
|
||||
if (isBlank(nextInstruction)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
var router = this;
|
||||
var parentInstructionsToClone = startingNumberOfAncestors - ancestorComponents.length;
|
||||
|
||||
// The first segment should be either '.' (generate from parent) or '' (generate from root).
|
||||
// When we normalize above, we strip all the slashes, './' becomes '.' and '/' becomes ''.
|
||||
if (first == '') {
|
||||
while (isPresent(router.parent)) {
|
||||
router = router.parent;
|
||||
var router = this.parent;
|
||||
for (var i = 0; i < parentInstructionsToClone; i++) {
|
||||
if (isBlank(router)) {
|
||||
break;
|
||||
}
|
||||
} else if (first == '..') {
|
||||
router = router.parent;
|
||||
while (ListWrapper.first(rest) == '..') {
|
||||
rest = ListWrapper.slice(rest, 1);
|
||||
router = router.parent;
|
||||
if (isBlank(router)) {
|
||||
throw new BaseException(
|
||||
`Link "${ListWrapper.toJSON(linkParams)}" has too many "../" segments.`);
|
||||
}
|
||||
}
|
||||
} else if (first != '.') {
|
||||
// For a link with no leading `./`, `/`, or `../`, we look for a sibling and child.
|
||||
// If both exist, we throw. Otherwise, we prefer whichever exists.
|
||||
var childRouteExists = this.registry.hasRoute(first, this.hostComponent);
|
||||
var parentRouteExists =
|
||||
isPresent(this.parent) && this.registry.hasRoute(first, this.parent.hostComponent);
|
||||
|
||||
if (parentRouteExists && childRouteExists) {
|
||||
let msg =
|
||||
`Link "${ListWrapper.toJSON(linkParams)}" is ambiguous, use "./" or "../" to disambiguate.`;
|
||||
throw new BaseException(msg);
|
||||
}
|
||||
if (parentRouteExists) {
|
||||
router = this.parent;
|
||||
}
|
||||
rest = linkParams;
|
||||
}
|
||||
|
||||
if (rest[rest.length - 1] == '') {
|
||||
rest.pop();
|
||||
}
|
||||
|
||||
if (rest.length < 1) {
|
||||
let msg = `Link "${ListWrapper.toJSON(linkParams)}" must include a route name.`;
|
||||
throw new BaseException(msg);
|
||||
}
|
||||
|
||||
var nextInstruction = this.registry.generate(rest, router.hostComponent);
|
||||
|
||||
var url = [];
|
||||
var parent = router.parent;
|
||||
while (isPresent(parent)) {
|
||||
url.unshift(parent._currentInstruction);
|
||||
parent = parent.parent;
|
||||
}
|
||||
|
||||
while (url.length > 0) {
|
||||
nextInstruction = url.pop().replaceChild(nextInstruction);
|
||||
while (isPresent(router) && isPresent(router._currentInstruction)) {
|
||||
nextInstruction = router._currentInstruction.replaceChild(nextInstruction);
|
||||
router = router.parent;
|
||||
}
|
||||
|
||||
return nextInstruction;
|
||||
@ -482,8 +449,8 @@ export class RootRouter extends Router {
|
||||
}
|
||||
|
||||
commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> {
|
||||
var emitPath = stringifyInstructionPath(instruction);
|
||||
var emitQuery = stringifyInstructionQuery(instruction);
|
||||
var emitPath = instruction.toUrlPath();
|
||||
var emitQuery = instruction.toUrlQuery();
|
||||
if (emitPath.length > 0) {
|
||||
emitPath = '/' + emitPath;
|
||||
}
|
||||
@ -521,20 +488,6 @@ class ChildRouter extends Router {
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Given: ['/a/b', {c: 2}]
|
||||
* Returns: ['', 'a', 'b', {c: 2}]
|
||||
*/
|
||||
function splitAndFlattenLinkParams(linkParams: any[]): any[] {
|
||||
return linkParams.reduce((accumulation: any[], item) => {
|
||||
if (isString(item)) {
|
||||
let strItem: string = item;
|
||||
return accumulation.concat(strItem.split('/'));
|
||||
}
|
||||
accumulation.push(item);
|
||||
return accumulation;
|
||||
}, []);
|
||||
}
|
||||
|
||||
function canActivateOne(nextInstruction: Instruction,
|
||||
prevInstruction: Instruction): Promise<boolean> {
|
||||
|
@ -3,7 +3,7 @@ import {isString} from 'angular2/src/facade/lang';
|
||||
|
||||
import {Router} from './router';
|
||||
import {Location} from './location';
|
||||
import {Instruction, stringifyInstruction} from './instruction';
|
||||
import {Instruction} from './instruction';
|
||||
|
||||
/**
|
||||
* The RouterLink directive lets you link to specific parts of your app.
|
||||
@ -61,7 +61,7 @@ export class RouterLink {
|
||||
this._routeParams = changes;
|
||||
this._navigationInstruction = this._router.generate(this._routeParams);
|
||||
|
||||
var navigationHref = stringifyInstruction(this._navigationInstruction);
|
||||
var navigationHref = this._navigationInstruction.toLinkUrl();
|
||||
this.visibleHref = this._location.prepareExternalUrl(navigationHref);
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,19 @@
|
||||
import {RouteHandler} from './route_handler';
|
||||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||||
import {Type} from 'angular2/src/facade/lang';
|
||||
import {isPresent, Type} from 'angular2/src/facade/lang';
|
||||
|
||||
import {RouteHandler} from './route_handler';
|
||||
import {RouteData, BLANK_ROUTE_DATA} from './instruction';
|
||||
|
||||
|
||||
export class SyncRouteHandler implements RouteHandler {
|
||||
public data: RouteData;
|
||||
|
||||
/** @internal */
|
||||
_resolvedComponent: Promise<any> = null;
|
||||
|
||||
constructor(public componentType: Type, public data?: {[key: string]: any}) {
|
||||
constructor(public componentType: Type, data?: {[key: string]: any}) {
|
||||
this._resolvedComponent = PromiseWrapper.resolve(componentType);
|
||||
this.data = isPresent(data) ? new RouteData(data) : BLANK_ROUTE_DATA;
|
||||
}
|
||||
|
||||
resolveComponentType(): Promise<any> { return this._resolvedComponent; }
|
||||
|
Reference in New Issue
Block a user