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-11 19:32:55 +02:00
|
|
|
|
isStringMap,
|
2015-05-21 13:59:14 -07:00
|
|
|
|
isFunction,
|
|
|
|
|
StringWrapper,
|
|
|
|
|
BaseException
|
|
|
|
|
} from 'angular2/src/facade/lang';
|
2015-05-04 15:39:14 -07:00
|
|
|
|
import {RouteConfig} from './route_config_impl';
|
2015-04-17 09:59:56 -07:00
|
|
|
|
import {reflector} from 'angular2/src/reflection/reflection';
|
|
|
|
|
|
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-04-17 09:59:56 -07:00
|
|
|
|
export class RouteRegistry {
|
2015-05-29 14:58:41 -07:00
|
|
|
|
_rules: Map<any, RouteRecognizer>;
|
2015-04-17 09:59:56 -07:00
|
|
|
|
|
2015-05-29 14:58:41 -07:00
|
|
|
|
constructor() { this._rules = MapWrapper.create(); }
|
2015-04-17 09:59:56 -07:00
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
/**
|
|
|
|
|
* Given a component and a configuration object, add the route to this registry
|
|
|
|
|
*/
|
2015-05-29 14:58:41 -07:00
|
|
|
|
config(parentComponent, config: StringMap<string, any>): void {
|
2015-05-21 13:59:14 -07:00
|
|
|
|
assertValidConfig(config);
|
2015-05-01 15:50:12 -07:00
|
|
|
|
|
2015-05-29 14:58:41 -07:00
|
|
|
|
var recognizer: RouteRecognizer = MapWrapper.get(this._rules, parentComponent);
|
2015-05-14 15:24:35 +02:00
|
|
|
|
|
|
|
|
|
if (isBlank(recognizer)) {
|
2015-04-17 09:59:56 -07:00
|
|
|
|
recognizer = new RouteRecognizer();
|
|
|
|
|
MapWrapper.set(this._rules, parentComponent, recognizer);
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-03 20:25:26 -07:00
|
|
|
|
if (StringMapWrapper.contains(config, 'redirectTo')) {
|
2015-05-15 02:05:57 -07:00
|
|
|
|
recognizer.addRedirect(config['path'], config['redirectTo']);
|
2015-05-03 20:25:26 -07:00
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-21 13:59:14 -07:00
|
|
|
|
config = StringMapWrapper.merge(
|
|
|
|
|
config, {'component': normalizeComponentDeclaration(config['component'])});
|
|
|
|
|
|
|
|
|
|
var component = config['component'];
|
|
|
|
|
this.configFromComponent(component);
|
2015-04-17 09:59:56 -07:00
|
|
|
|
|
2015-05-03 20:36:36 -07:00
|
|
|
|
recognizer.addConfig(config['path'], config, config['as']);
|
2015-04-17 09:59:56 -07:00
|
|
|
|
}
|
|
|
|
|
|
2015-05-21 13:59:14 -07:00
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
/**
|
|
|
|
|
* Reads the annotations of a component and configures the registry based on them
|
|
|
|
|
*/
|
2015-05-14 15:24:35 +02:00
|
|
|
|
configFromComponent(component): 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.
|
|
|
|
|
if (MapWrapper.contains(this._rules, component)) {
|
|
|
|
|
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-05-15 02:05:57 -07:00
|
|
|
|
ListWrapper.forEach(annotation.configs, (config) => this.config(component, config));
|
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
|
|
|
|
|
* the application into the state specified by the
|
|
|
|
|
*/
|
2015-05-21 13:59:14 -07:00
|
|
|
|
recognize(url: string, parentComponent): Promise<Instruction> {
|
2015-04-17 09:59:56 -07:00
|
|
|
|
var componentRecognizer = MapWrapper.get(this._rules, parentComponent);
|
|
|
|
|
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)
|
|
|
|
|
.then((solutions) => {
|
|
|
|
|
// 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-05-21 13:59:14 -07:00
|
|
|
|
_completeRouteMatch(candidate: RouteMatch): Promise<Instruction> {
|
|
|
|
|
return componentHandlerToComponentType(candidate.handler)
|
|
|
|
|
.then((componentType) => {
|
|
|
|
|
this.configFromComponent(componentType);
|
2015-05-14 15:38:16 +02:00
|
|
|
|
|
2015-05-21 13:59:14 -07:00
|
|
|
|
if (candidate.unmatchedUrl.length == 0) {
|
|
|
|
|
return new Instruction({
|
|
|
|
|
component: componentType,
|
|
|
|
|
params: candidate.params,
|
|
|
|
|
matchedUrl: candidate.matchedUrl,
|
|
|
|
|
parentSpecificity: candidate.specificity
|
|
|
|
|
});
|
|
|
|
|
}
|
2015-05-12 14:53:13 -07:00
|
|
|
|
|
2015-05-21 13:59:14 -07:00
|
|
|
|
return this.recognize(candidate.unmatchedUrl, componentType)
|
|
|
|
|
.then(childInstruction => {
|
|
|
|
|
if (isBlank(childInstruction)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
return new Instruction({
|
|
|
|
|
component: componentType,
|
|
|
|
|
child: childInstruction,
|
|
|
|
|
params: candidate.params,
|
|
|
|
|
matchedUrl: candidate.matchedUrl,
|
|
|
|
|
parentSpecificity: candidate.specificity
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
2015-04-17 09:59:56 -07:00
|
|
|
|
}
|
|
|
|
|
|
2015-05-29 14:58:41 -07:00
|
|
|
|
generate(name: string, params: StringMap<string, string>, hostComponent): string {
|
|
|
|
|
// TODO: implement for hierarchical routes
|
2015-05-01 05:53:38 -07:00
|
|
|
|
var componentRecognizer = MapWrapper.get(this._rules, hostComponent);
|
2015-05-14 15:24:35 +02:00
|
|
|
|
return isPresent(componentRecognizer) ? componentRecognizer.generate(name, params) : null;
|
2015-04-17 09:59:56 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-21 13:59:14 -07:00
|
|
|
|
|
|
|
|
|
/*
|
|
|
|
|
* A config should have a "path" property, and exactly one of:
|
|
|
|
|
* - `component`
|
|
|
|
|
* - `redirectTo`
|
|
|
|
|
*/
|
|
|
|
|
var ALLOWED_TARGETS = ['component', 'redirectTo'];
|
|
|
|
|
function assertValidConfig(config: StringMap<string, any>): void {
|
|
|
|
|
if (!StringMapWrapper.contains(config, 'path')) {
|
|
|
|
|
throw new BaseException(`Route config should contain a "path" property`);
|
|
|
|
|
}
|
|
|
|
|
var targets = 0;
|
|
|
|
|
ListWrapper.forEach(ALLOWED_TARGETS, (target) => {
|
|
|
|
|
if (StringMapWrapper.contains(config, target)) {
|
|
|
|
|
targets += 1;
|
|
|
|
|
}
|
2015-04-17 09:59:56 -07:00
|
|
|
|
});
|
2015-05-21 13:59:14 -07:00
|
|
|
|
if (targets != 1) {
|
|
|
|
|
throw new BaseException(
|
|
|
|
|
`Route config should contain exactly one 'component', or 'redirectTo' property`);
|
|
|
|
|
}
|
2015-04-17 09:59:56 -07:00
|
|
|
|
}
|
2015-04-29 15:47:12 -07:00
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
/*
|
2015-05-21 13:59:14 -07:00
|
|
|
|
* Returns a StringMap like: `{ 'constructor': SomeType, 'type': 'constructor' }`
|
2015-05-15 02:05:57 -07:00
|
|
|
|
*/
|
2015-05-21 13:59:14 -07:00
|
|
|
|
var VALID_COMPONENT_TYPES = ['constructor', 'loader'];
|
|
|
|
|
function normalizeComponentDeclaration(config: any): StringMap<string, any> {
|
|
|
|
|
if (isType(config)) {
|
|
|
|
|
return {'constructor': config, 'type': 'constructor'};
|
2015-06-11 19:32:55 +02:00
|
|
|
|
} else if (isStringMap(config)) {
|
2015-05-21 13:59:14 -07:00
|
|
|
|
if (isBlank(config['type'])) {
|
|
|
|
|
throw new BaseException(
|
|
|
|
|
`Component declaration when provided as a map should include a 'type' property`);
|
|
|
|
|
}
|
|
|
|
|
var componentType = config['type'];
|
|
|
|
|
if (!ListWrapper.contains(VALID_COMPONENT_TYPES, componentType)) {
|
|
|
|
|
throw new BaseException(`Invalid component type '${componentType}'`);
|
|
|
|
|
}
|
2015-05-15 02:05:57 -07:00
|
|
|
|
return config;
|
2015-05-21 13:59:14 -07:00
|
|
|
|
} else {
|
|
|
|
|
throw new BaseException(`Component declaration should be either a Map or a Type`);
|
2015-05-15 02:05:57 -07:00
|
|
|
|
}
|
2015-05-21 13:59:14 -07:00
|
|
|
|
}
|
2015-04-29 15:47:12 -07:00
|
|
|
|
|
2015-05-21 13:59:14 -07:00
|
|
|
|
function componentHandlerToComponentType(handler): Promise<any> {
|
|
|
|
|
var componentDeclaration = handler['component'], type = componentDeclaration['type'];
|
|
|
|
|
|
|
|
|
|
if (type == 'constructor') {
|
|
|
|
|
return PromiseWrapper.resolve(componentDeclaration['constructor']);
|
|
|
|
|
} else if (type == 'loader') {
|
|
|
|
|
var resolverFunction = componentDeclaration['loader'];
|
|
|
|
|
return resolverFunction();
|
|
|
|
|
} else {
|
|
|
|
|
throw new BaseException(`Cannot extract the component type from a '${type}' component`);
|
|
|
|
|
}
|
|
|
|
|
}
|
2015-04-29 15:47:12 -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
|
|
|
|
}
|