2015-05-15 02:05:57 -07:00
|
|
|
|
import {RouteRecognizer, RouteMatch} from './route_recognizer';
|
2015-05-29 14:58:41 -07:00
|
|
|
|
import {Instruction} from './instruction';
|
|
|
|
|
import {
|
|
|
|
|
List,
|
|
|
|
|
ListWrapper,
|
|
|
|
|
Map,
|
|
|
|
|
MapWrapper,
|
|
|
|
|
StringMap,
|
|
|
|
|
StringMapWrapper
|
|
|
|
|
} from 'angular2/src/facade/collection';
|
2015-05-21 13:59:14 -07:00
|
|
|
|
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
|
|
|
|
|
import {
|
|
|
|
|
isPresent,
|
|
|
|
|
isBlank,
|
|
|
|
|
isType,
|
2015-06-30 13:18:51 -07:00
|
|
|
|
isString,
|
2015-06-11 19:32:55 +02:00
|
|
|
|
isStringMap,
|
2015-05-21 13:59:14 -07:00
|
|
|
|
isFunction,
|
|
|
|
|
StringWrapper,
|
2015-07-13 15:29:14 -07:00
|
|
|
|
BaseException,
|
|
|
|
|
getTypeNameForDebugging
|
2015-05-21 13:59:14 -07:00
|
|
|
|
} from 'angular2/src/facade/lang';
|
2015-07-13 16:12:48 -07:00
|
|
|
|
import {RouteConfig, AsyncRoute, Route, Redirect, RouteDefinition} from './route_config_impl';
|
2015-04-17 09:59:56 -07:00
|
|
|
|
import {reflector} from 'angular2/src/reflection/reflection';
|
2015-06-29 10:22:00 +02:00
|
|
|
|
import {Injectable} from 'angular2/di';
|
2015-07-13 16:12:48 -07:00
|
|
|
|
import {normalizeRouteConfig} from './route_config_nomalizer';
|
2015-04-17 09:59:56 -07:00
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
/**
|
|
|
|
|
* The RouteRegistry holds route configurations for each component in an Angular app.
|
2015-05-29 14:58:41 -07:00
|
|
|
|
* It is responsible for creating Instructions from URLs, and generating URLs based on route and
|
|
|
|
|
* parameters.
|
2015-05-15 02:05:57 -07:00
|
|
|
|
*/
|
2015-06-29 10:22:00 +02:00
|
|
|
|
@Injectable()
|
2015-04-17 09:59:56 -07:00
|
|
|
|
export class RouteRegistry {
|
2015-06-30 13:18:51 -07:00
|
|
|
|
private _rules: Map<any, RouteRecognizer> = new Map();
|
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
/**
|
|
|
|
|
* Given a component and a configuration object, add the route to this registry
|
|
|
|
|
*/
|
2015-07-21 01:26:43 -07:00
|
|
|
|
config(parentComponent: any, config: RouteDefinition, isRootLevelRoute: boolean = false): void {
|
2015-07-13 16:12:48 -07:00
|
|
|
|
config = normalizeRouteConfig(config);
|
2015-05-01 15:50:12 -07:00
|
|
|
|
|
2015-06-17 16:21:40 -07:00
|
|
|
|
var recognizer: RouteRecognizer = this._rules.get(parentComponent);
|
2015-05-14 15:24:35 +02:00
|
|
|
|
|
|
|
|
|
if (isBlank(recognizer)) {
|
2015-07-21 01:26:43 -07:00
|
|
|
|
recognizer = new RouteRecognizer(isRootLevelRoute);
|
2015-06-17 16:21:40 -07:00
|
|
|
|
this._rules.set(parentComponent, recognizer);
|
2015-04-17 09:59:56 -07:00
|
|
|
|
}
|
|
|
|
|
|
2015-07-13 16:12:48 -07:00
|
|
|
|
var terminal = recognizer.config(config);
|
2015-05-21 13:59:14 -07:00
|
|
|
|
|
2015-07-13 16:12:48 -07:00
|
|
|
|
if (config instanceof Route) {
|
2015-06-17 11:57:38 -07:00
|
|
|
|
if (terminal) {
|
2015-07-13 16:12:48 -07:00
|
|
|
|
assertTerminalComponent(config.component, config.path);
|
2015-06-17 11:57:38 -07:00
|
|
|
|
} else {
|
2015-07-13 16:12:48 -07:00
|
|
|
|
this.configFromComponent(config.component);
|
2015-06-17 11:57:38 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
2015-04-17 09:59:56 -07:00
|
|
|
|
}
|
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
/**
|
|
|
|
|
* Reads the annotations of a component and configures the registry based on them
|
|
|
|
|
*/
|
2015-07-21 01:26:43 -07:00
|
|
|
|
configFromComponent(component: any, isRootComponent: boolean = false): void {
|
2015-04-17 09:59:56 -07:00
|
|
|
|
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.
|
2015-06-17 21:42:56 -07:00
|
|
|
|
if (this._rules.has(component)) {
|
2015-04-17 09:59:56 -07:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
var annotations = reflector.annotations(component);
|
|
|
|
|
if (isPresent(annotations)) {
|
2015-05-29 14:58:41 -07:00
|
|
|
|
for (var i = 0; i < annotations.length; i++) {
|
2015-04-17 09:59:56 -07:00
|
|
|
|
var annotation = annotations[i];
|
|
|
|
|
|
|
|
|
|
if (annotation instanceof RouteConfig) {
|
2015-07-21 01:26:43 -07:00
|
|
|
|
ListWrapper.forEach(annotation.configs,
|
|
|
|
|
(config) => this.config(component, config, isRootComponent));
|
2015-04-17 09:59:56 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
/**
|
|
|
|
|
* Given a URL and a parent component, return the most specific instruction for navigating
|
2015-07-01 16:34:37 -07:00
|
|
|
|
* the application into the state specified by the url
|
2015-05-15 02:05:57 -07:00
|
|
|
|
*/
|
2015-07-07 20:03:00 -07:00
|
|
|
|
recognize(url: string, parentComponent: any): Promise<Instruction> {
|
2015-06-17 16:21:40 -07:00
|
|
|
|
var componentRecognizer = this._rules.get(parentComponent);
|
2015-04-17 09:59:56 -07:00
|
|
|
|
if (isBlank(componentRecognizer)) {
|
2015-05-21 13:59:14 -07:00
|
|
|
|
return PromiseWrapper.resolve(null);
|
2015-04-17 09:59:56 -07:00
|
|
|
|
}
|
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
// Matches some beginning part of the given URL
|
|
|
|
|
var possibleMatches = componentRecognizer.recognize(url);
|
2015-05-21 13:59:14 -07:00
|
|
|
|
var matchPromises =
|
|
|
|
|
ListWrapper.map(possibleMatches, (candidate) => this._completeRouteMatch(candidate));
|
2015-05-15 02:05:57 -07:00
|
|
|
|
|
2015-05-21 13:59:14 -07:00
|
|
|
|
return PromiseWrapper.all(matchPromises)
|
2015-06-26 11:10:52 -07:00
|
|
|
|
.then((solutions: List<Instruction>) => {
|
2015-05-21 13:59:14 -07:00
|
|
|
|
// remove nulls
|
|
|
|
|
var fullSolutions = ListWrapper.filter(solutions, (solution) => isPresent(solution));
|
|
|
|
|
|
|
|
|
|
if (fullSolutions.length > 0) {
|
|
|
|
|
return mostSpecific(fullSolutions);
|
2015-05-12 14:53:13 -07:00
|
|
|
|
}
|
2015-05-21 13:59:14 -07:00
|
|
|
|
return null;
|
|
|
|
|
});
|
|
|
|
|
}
|
2015-04-17 09:59:56 -07:00
|
|
|
|
|
|
|
|
|
|
2015-06-30 13:18:51 -07:00
|
|
|
|
_completeRouteMatch(partialMatch: RouteMatch): Promise<Instruction> {
|
|
|
|
|
var recognizer = partialMatch.recognizer;
|
|
|
|
|
var handler = recognizer.handler;
|
|
|
|
|
return handler.resolveComponentType().then((componentType) => {
|
|
|
|
|
this.configFromComponent(componentType);
|
2015-05-14 15:38:16 +02:00
|
|
|
|
|
2015-06-30 13:18:51 -07:00
|
|
|
|
if (partialMatch.unmatchedUrl.length == 0) {
|
2015-07-07 15:44:29 -07:00
|
|
|
|
if (recognizer.terminal) {
|
2015-07-21 01:26:43 -07:00
|
|
|
|
return new Instruction(componentType, partialMatch.matchedUrl, recognizer, null,
|
|
|
|
|
partialMatch.params());
|
2015-07-07 15:44:29 -07:00
|
|
|
|
} else {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
2015-06-30 13:18:51 -07:00
|
|
|
|
}
|
2015-05-12 14:53:13 -07:00
|
|
|
|
|
2015-06-30 13:18:51 -07:00
|
|
|
|
return this.recognize(partialMatch.unmatchedUrl, componentType)
|
|
|
|
|
.then(childInstruction => {
|
|
|
|
|
if (isBlank(childInstruction)) {
|
|
|
|
|
return null;
|
|
|
|
|
} else {
|
|
|
|
|
return new Instruction(componentType, partialMatch.matchedUrl, recognizer,
|
|
|
|
|
childInstruction);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
2015-04-17 09:59:56 -07:00
|
|
|
|
}
|
|
|
|
|
|
2015-06-30 13:18:51 -07:00
|
|
|
|
/**
|
2015-07-06 17:41:15 -07:00
|
|
|
|
* Given a normalized list with component names and params like: `['user', {id: 3 }]`
|
2015-06-30 13:18:51 -07:00
|
|
|
|
* generates a url with a leading slash relative to the provided `parentComponent`.
|
|
|
|
|
*/
|
2015-07-07 20:03:00 -07:00
|
|
|
|
generate(linkParams: List<any>, parentComponent: any): string {
|
2015-07-06 17:41:15 -07:00
|
|
|
|
let url = '';
|
2015-06-30 13:18:51 -07:00
|
|
|
|
let componentCursor = parentComponent;
|
2015-07-06 17:41:15 -07:00
|
|
|
|
for (let i = 0; i < linkParams.length; i += 1) {
|
|
|
|
|
let segment = linkParams[i];
|
2015-07-13 15:29:14 -07:00
|
|
|
|
if (isBlank(componentCursor)) {
|
|
|
|
|
throw new BaseException(`Could not find route named "${segment}".`);
|
|
|
|
|
}
|
2015-06-30 13:18:51 -07:00
|
|
|
|
if (!isString(segment)) {
|
|
|
|
|
throw new BaseException(`Unexpected segment "${segment}" in link DSL. Expected a string.`);
|
2015-07-06 17:41:15 -07:00
|
|
|
|
} else if (segment == '' || segment == '.' || segment == '..') {
|
|
|
|
|
throw new BaseException(`"${segment}/" is only allowed at the beginning of a link DSL.`);
|
2015-06-30 13:18:51 -07:00
|
|
|
|
}
|
|
|
|
|
let params = null;
|
2015-07-06 17:41:15 -07:00
|
|
|
|
if (i + 1 < linkParams.length) {
|
|
|
|
|
let nextSegment = linkParams[i + 1];
|
2015-06-30 13:18:51 -07:00
|
|
|
|
if (isStringMap(nextSegment)) {
|
|
|
|
|
params = nextSegment;
|
|
|
|
|
i += 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var componentRecognizer = this._rules.get(componentCursor);
|
|
|
|
|
if (isBlank(componentRecognizer)) {
|
2015-07-15 11:10:39 -07:00
|
|
|
|
throw new BaseException(
|
|
|
|
|
`Component "${getTypeNameForDebugging(componentCursor)}" has no route config.`);
|
2015-06-30 13:18:51 -07:00
|
|
|
|
}
|
|
|
|
|
var response = componentRecognizer.generate(segment, params);
|
2015-07-13 15:29:14 -07:00
|
|
|
|
if (isBlank(response)) {
|
|
|
|
|
throw new BaseException(
|
|
|
|
|
`Component "${getTypeNameForDebugging(componentCursor)}" has no route named "${segment}".`);
|
|
|
|
|
}
|
2015-06-30 13:18:51 -07:00
|
|
|
|
url += response['url'];
|
|
|
|
|
componentCursor = response['nextComponent'];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return url;
|
2015-04-17 09:59:56 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-21 13:59:14 -07:00
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* Given a list of instructions, returns the most specific instruction
|
|
|
|
|
*/
|
|
|
|
|
function mostSpecific(instructions: List<Instruction>): Instruction {
|
|
|
|
|
var mostSpecificSolution = instructions[0];
|
|
|
|
|
for (var solutionIndex = 1; solutionIndex < instructions.length; solutionIndex++) {
|
|
|
|
|
var solution = instructions[solutionIndex];
|
|
|
|
|
if (solution.specificity > mostSpecificSolution.specificity) {
|
|
|
|
|
mostSpecificSolution = solution;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return mostSpecificSolution;
|
2015-04-29 15:47:12 -07:00
|
|
|
|
}
|
2015-06-17 11:57:38 -07:00
|
|
|
|
|
|
|
|
|
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.`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|