481 lines
16 KiB
TypeScript
481 lines
16 KiB
TypeScript
import {ListWrapper, Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
|
||
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
||
import {
|
||
isPresent,
|
||
isArray,
|
||
isBlank,
|
||
isType,
|
||
isString,
|
||
isStringMap,
|
||
Type,
|
||
getTypeNameForDebugging,
|
||
CONST_EXPR
|
||
} from 'angular2/src/facade/lang';
|
||
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
|
||
import {reflector} from 'angular2/src/core/reflection/reflection';
|
||
import {Injectable, Inject, OpaqueToken} from 'angular2/core';
|
||
|
||
import {
|
||
RouteConfig,
|
||
AsyncRoute,
|
||
Route,
|
||
AuxRoute,
|
||
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 {normalizeRouteConfig, assertComponentExists} from './route_config_nomalizer';
|
||
import {parser, Url, pathSegmentsToUrl} from './url_parser';
|
||
|
||
var _resolveToNull = PromiseWrapper.resolve(null);
|
||
|
||
|
||
|
||
/**
|
||
* Token used to bind the component with the top-level {@link RouteConfig}s for the
|
||
* application.
|
||
*
|
||
* ### Example ([live demo](http://plnkr.co/edit/iRUP8B5OUbxCWQ3AcIDm))
|
||
*
|
||
* ```
|
||
* import {Component} from 'angular2/angular2';
|
||
* import {
|
||
* ROUTER_DIRECTIVES,
|
||
* ROUTER_PROVIDERS,
|
||
* RouteConfig
|
||
* } from 'angular2/router';
|
||
*
|
||
* @Component({directives: [ROUTER_DIRECTIVES]})
|
||
* @RouteConfig([
|
||
* {...},
|
||
* ])
|
||
* class AppCmp {
|
||
* // ...
|
||
* }
|
||
*
|
||
* bootstrap(AppCmp, [ROUTER_PROVIDERS]);
|
||
* ```
|
||
*/
|
||
export const ROUTER_PRIMARY_COMPONENT: OpaqueToken =
|
||
CONST_EXPR(new OpaqueToken('RouterPrimaryComponent'));
|
||
|
||
|
||
/**
|
||
* The RouteRegistry holds route configurations for each component in an Angular app.
|
||
* It is responsible for creating Instructions from URLs, and generating URLs based on route and
|
||
* parameters.
|
||
*/
|
||
@Injectable()
|
||
export class RouteRegistry {
|
||
private _rules = new Map<any, ComponentRecognizer>();
|
||
|
||
constructor(@Inject(ROUTER_PRIMARY_COMPONENT) private _rootComponent: Type) {}
|
||
|
||
/**
|
||
* Given a component and a configuration object, add the route to this registry
|
||
*/
|
||
config(parentComponent: any, config: RouteDefinition): void {
|
||
config = normalizeRouteConfig(config, this);
|
||
|
||
// this is here because Dart type guard reasons
|
||
if (config instanceof Route) {
|
||
assertComponentExists(config.component, config.path);
|
||
} else if (config instanceof AuxRoute) {
|
||
assertComponentExists(config.component, config.path);
|
||
}
|
||
|
||
var recognizer: ComponentRecognizer = this._rules.get(parentComponent);
|
||
|
||
if (isBlank(recognizer)) {
|
||
recognizer = new ComponentRecognizer();
|
||
this._rules.set(parentComponent, recognizer);
|
||
}
|
||
|
||
var terminal = recognizer.config(config);
|
||
|
||
if (config instanceof Route) {
|
||
if (terminal) {
|
||
assertTerminalComponent(config.component, config.path);
|
||
} else {
|
||
this.configFromComponent(config.component);
|
||
}
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Reads the annotations of a component and configures the registry based on them
|
||
*/
|
||
configFromComponent(component: any): void {
|
||
if (!isType(component)) {
|
||
return;
|
||
}
|
||
|
||
// Don't read the annotations from a type more than once –
|
||
// this prevents an infinite loop if a component routes recursively.
|
||
if (this._rules.has(component)) {
|
||
return;
|
||
}
|
||
var annotations = reflector.annotations(component);
|
||
if (isPresent(annotations)) {
|
||
for (var i = 0; i < annotations.length; i++) {
|
||
var annotation = annotations[i];
|
||
|
||
if (annotation instanceof RouteConfig) {
|
||
let routeCfgs: RouteDefinition[] = annotation.configs;
|
||
routeCfgs.forEach(config => this.config(component, config));
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
/**
|
||
* 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, ancestorInstructions: Instruction[]): Promise<Instruction> {
|
||
var parsedUrl = parser.parse(url);
|
||
return this._recognize(parsedUrl, ancestorInstructions);
|
||
}
|
||
|
||
|
||
/**
|
||
* Recognizes all parent-child routes, but creates unresolved auxiliary routes
|
||
*/
|
||
|
||
private _recognize(parsedUrl: Url, ancestorInstructions: Instruction[],
|
||
_aux = false): Promise<Instruction> {
|
||
var parentComponent =
|
||
ancestorInstructions.length > 0 ?
|
||
ancestorInstructions[ancestorInstructions.length - 1].component.componentType :
|
||
this._rootComponent;
|
||
|
||
var componentRecognizer = this._rules.get(parentComponent);
|
||
if (isBlank(componentRecognizer)) {
|
||
return _resolveToNull;
|
||
}
|
||
|
||
// Matches some beginning part of the given URL
|
||
var possibleMatches: Promise<RouteMatch>[] =
|
||
_aux ? componentRecognizer.recognizeAuxiliary(parsedUrl) :
|
||
componentRecognizer.recognize(parsedUrl);
|
||
|
||
var matchPromises: Promise<Instruction>[] = possibleMatches.map(
|
||
(candidate: Promise<RouteMatch>) => candidate.then((candidate: RouteMatch) => {
|
||
|
||
if (candidate instanceof PathMatch) {
|
||
var auxParentInstructions =
|
||
ancestorInstructions.length > 0 ?
|
||
[ancestorInstructions[ancestorInstructions.length - 1]] :
|
||
[];
|
||
var auxInstructions =
|
||
this._auxRoutesToUnresolved(candidate.remainingAux, auxParentInstructions);
|
||
var instruction = new ResolvedInstruction(candidate.instruction, null, auxInstructions);
|
||
|
||
if (candidate.instruction.terminal) {
|
||
return instruction;
|
||
}
|
||
|
||
var newAncestorComponents = ancestorInstructions.concat([instruction]);
|
||
|
||
return this._recognize(candidate.remaining, newAncestorComponents)
|
||
.then((childInstruction) => {
|
||
if (isBlank(childInstruction)) {
|
||
return null;
|
||
}
|
||
|
||
// redirect instructions are already absolute
|
||
if (childInstruction instanceof RedirectInstruction) {
|
||
return childInstruction;
|
||
}
|
||
instruction.child = childInstruction;
|
||
return instruction;
|
||
});
|
||
}
|
||
|
||
if (candidate instanceof RedirectMatch) {
|
||
var instruction = this.generate(candidate.redirectTo, ancestorInstructions);
|
||
return new RedirectInstruction(instruction.component, instruction.child,
|
||
instruction.auxInstruction);
|
||
}
|
||
}));
|
||
|
||
if ((isBlank(parsedUrl) || parsedUrl.path == '') && possibleMatches.length == 0) {
|
||
return PromiseWrapper.resolve(this.generateDefault(parentComponent));
|
||
}
|
||
|
||
return PromiseWrapper.all(matchPromises).then(mostSpecific);
|
||
}
|
||
|
||
private _auxRoutesToUnresolved(auxRoutes: Url[],
|
||
parentInstructions: Instruction[]): {[key: string]: Instruction} {
|
||
var unresolvedAuxInstructions: {[key: string]: Instruction} = {};
|
||
|
||
auxRoutes.forEach((auxUrl: Url) => {
|
||
unresolvedAuxInstructions[auxUrl.path] = new UnresolvedInstruction(
|
||
() => { return this._recognize(auxUrl, parentInstructions, true); });
|
||
});
|
||
|
||
return unresolvedAuxInstructions;
|
||
}
|
||
|
||
|
||
/**
|
||
* 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[], ancestorInstructions: Instruction[], _aux = false): Instruction {
|
||
let normalizedLinkParams = splitAndFlattenLinkParams(linkParams);
|
||
|
||
var first = ListWrapper.first(normalizedLinkParams);
|
||
var rest = ListWrapper.slice(normalizedLinkParams, 1);
|
||
|
||
// The first segment should be either '.' (generate from parent) or '' (generate from root).
|
||
// When we normalize above, we strip all the slashes, './' becomes '.' and '/' becomes ''.
|
||
if (first == '') {
|
||
ancestorInstructions = [];
|
||
} else if (first == '..') {
|
||
// we already captured the first instance of "..", so we need to pop off an ancestor
|
||
ancestorInstructions.pop();
|
||
while (ListWrapper.first(rest) == '..') {
|
||
rest = ListWrapper.slice(rest, 1);
|
||
ancestorInstructions.pop();
|
||
if (ancestorInstructions.length <= 0) {
|
||
throw new BaseException(
|
||
`Link "${ListWrapper.toJSON(linkParams)}" has too many "../" segments.`);
|
||
}
|
||
}
|
||
} else if (first != '.') {
|
||
let parentComponent = this._rootComponent;
|
||
let grandparentComponent = null;
|
||
if (ancestorInstructions.length > 1) {
|
||
parentComponent =
|
||
ancestorInstructions[ancestorInstructions.length - 1].component.componentType;
|
||
grandparentComponent =
|
||
ancestorInstructions[ancestorInstructions.length - 2].component.componentType;
|
||
} else if (ancestorInstructions.length == 1) {
|
||
parentComponent = ancestorInstructions[0].component.componentType;
|
||
grandparentComponent = this._rootComponent;
|
||
}
|
||
|
||
// For a link with no leading `./`, `/`, or `../`, we look for a sibling and child.
|
||
// If both exist, we throw. Otherwise, we prefer whichever exists.
|
||
var childRouteExists = this.hasRoute(first, parentComponent);
|
||
var parentRouteExists =
|
||
isPresent(grandparentComponent) && this.hasRoute(first, grandparentComponent);
|
||
|
||
if (parentRouteExists && childRouteExists) {
|
||
let msg =
|
||
`Link "${ListWrapper.toJSON(linkParams)}" is ambiguous, use "./" or "../" to disambiguate.`;
|
||
throw new BaseException(msg);
|
||
}
|
||
if (parentRouteExists) {
|
||
ancestorInstructions.pop();
|
||
}
|
||
rest = linkParams;
|
||
}
|
||
|
||
if (rest[rest.length - 1] == '') {
|
||
rest.pop();
|
||
}
|
||
|
||
if (rest.length < 1) {
|
||
let msg = `Link "${ListWrapper.toJSON(linkParams)}" must include a route name.`;
|
||
throw new BaseException(msg);
|
||
}
|
||
|
||
var generatedInstruction = this._generate(rest, ancestorInstructions, _aux);
|
||
|
||
for (var i = ancestorInstructions.length - 1; i >= 0; i--) {
|
||
let ancestorInstruction = ancestorInstructions[i];
|
||
generatedInstruction = ancestorInstruction.replaceChild(generatedInstruction);
|
||
}
|
||
|
||
return generatedInstruction;
|
||
}
|
||
|
||
|
||
/*
|
||
* Internal helper that does not make any assertions about the beginning of the link DSL
|
||
*/
|
||
private _generate(linkParams: any[], ancestorInstructions: Instruction[],
|
||
_aux = false): Instruction {
|
||
let parentComponent =
|
||
ancestorInstructions.length > 0 ?
|
||
ancestorInstructions[ancestorInstructions.length - 1].component.componentType :
|
||
this._rootComponent;
|
||
|
||
|
||
if (linkParams.length == 0) {
|
||
return this.generateDefault(parentComponent);
|
||
}
|
||
let linkIndex = 0;
|
||
let routeName = linkParams[linkIndex];
|
||
|
||
if (!isString(routeName)) {
|
||
throw new BaseException(`Unexpected segment "${routeName}" in link DSL. Expected a string.`);
|
||
} else if (routeName == '' || routeName == '.' || routeName == '..') {
|
||
throw new BaseException(`"${routeName}/" is only allowed at the beginning of a link DSL.`);
|
||
}
|
||
|
||
let params = {};
|
||
if (linkIndex + 1 < linkParams.length) {
|
||
let nextSegment = linkParams[linkIndex + 1];
|
||
if (isStringMap(nextSegment) && !isArray(nextSegment)) {
|
||
params = nextSegment;
|
||
linkIndex += 1;
|
||
}
|
||
}
|
||
|
||
let auxInstructions: {[key: string]: Instruction} = {};
|
||
var nextSegment;
|
||
while (linkIndex + 1 < linkParams.length && isArray(nextSegment = linkParams[linkIndex + 1])) {
|
||
let auxParentInstruction = ancestorInstructions.length > 0 ?
|
||
[ancestorInstructions[ancestorInstructions.length - 1]] :
|
||
[];
|
||
let auxInstruction = this._generate(nextSegment, auxParentInstruction, true);
|
||
|
||
// TODO: this will not work for aux routes with parameters or multiple segments
|
||
auxInstructions[auxInstruction.component.urlPath] = auxInstruction;
|
||
linkIndex += 1;
|
||
}
|
||
|
||
var componentRecognizer = this._rules.get(parentComponent);
|
||
if (isBlank(componentRecognizer)) {
|
||
throw new BaseException(
|
||
`Component "${getTypeNameForDebugging(parentComponent)}" has no route config.`);
|
||
}
|
||
|
||
var routeRecognizer =
|
||
(_aux ? componentRecognizer.auxNames : componentRecognizer.names).get(routeName);
|
||
|
||
if (!isPresent(routeRecognizer)) {
|
||
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, ancestorInstructions, _aux); });
|
||
}, compInstruction['urlPath'], compInstruction['urlParams']);
|
||
}
|
||
|
||
var componentInstruction = _aux ? componentRecognizer.generateAuxiliary(routeName, params) :
|
||
componentRecognizer.generate(routeName, params);
|
||
|
||
|
||
|
||
var remaining = linkParams.slice(linkIndex + 1);
|
||
|
||
var instruction = new ResolvedInstruction(componentInstruction, null, auxInstructions);
|
||
|
||
// the component is sync
|
||
if (isPresent(componentInstruction.componentType)) {
|
||
let childInstruction: Instruction = null;
|
||
if (linkIndex + 1 < linkParams.length) {
|
||
let childAncestorComponents = ancestorInstructions.concat([instruction]);
|
||
childInstruction = this._generate(remaining, childAncestorComponents);
|
||
} else if (!componentInstruction.terminal) {
|
||
// ... look for defaults
|
||
childInstruction = this.generateDefault(componentInstruction.componentType);
|
||
|
||
if (isBlank(childInstruction)) {
|
||
throw new BaseException(
|
||
`Link "${ListWrapper.toJSON(linkParams)}" does not resolve to a terminal instruction.`);
|
||
}
|
||
}
|
||
instruction.child = childInstruction;
|
||
}
|
||
|
||
return instruction;
|
||
}
|
||
|
||
public hasRoute(name: string, parentComponent: any): boolean {
|
||
var componentRecognizer: ComponentRecognizer = this._rules.get(parentComponent);
|
||
if (isBlank(componentRecognizer)) {
|
||
return false;
|
||
}
|
||
return componentRecognizer.hasRoute(name);
|
||
}
|
||
|
||
public generateDefault(componentCursor: Type): Instruction {
|
||
if (isBlank(componentCursor)) {
|
||
return null;
|
||
}
|
||
|
||
var componentRecognizer = this._rules.get(componentCursor);
|
||
if (isBlank(componentRecognizer) || isBlank(componentRecognizer.defaultRoute)) {
|
||
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 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: Instruction[]): Instruction {
|
||
return ListWrapper.maximum(instructions, (instruction: Instruction) => instruction.specificity);
|
||
}
|
||
|
||
function assertTerminalComponent(component, path) {
|
||
if (!isType(component)) {
|
||
return;
|
||
}
|
||
|
||
var annotations = reflector.annotations(component);
|
||
if (isPresent(annotations)) {
|
||
for (var i = 0; i < annotations.length; i++) {
|
||
var annotation = annotations[i];
|
||
|
||
if (annotation instanceof RouteConfig) {
|
||
throw new BaseException(
|
||
`Child routes are not allowed for "${path}". Use "..." on the parent's route path.`);
|
||
}
|
||
}
|
||
}
|
||
}
|