2015-05-15 02:05:57 -07:00
|
|
|
|
import {RouteRecognizer, RouteMatch} from './route_recognizer';
|
2015-04-17 09:59:56 -07:00
|
|
|
|
import {Instruction, noopInstruction} from './instruction';
|
|
|
|
|
import {List, ListWrapper, Map, MapWrapper, StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
|
2015-05-01 05:53:38 -07:00
|
|
|
|
import {isPresent, isBlank, isType, 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.
|
|
|
|
|
* It is responsible for creating Instructions from URLs, and generating URLs based on route and parameters.
|
|
|
|
|
*/
|
2015-04-17 09:59:56 -07:00
|
|
|
|
export class RouteRegistry {
|
|
|
|
|
_rules:Map<any, RouteRecognizer>;
|
|
|
|
|
|
|
|
|
|
constructor() {
|
|
|
|
|
this._rules = MapWrapper.create();
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
/**
|
|
|
|
|
* Given a component and a configuration object, add the route to this registry
|
|
|
|
|
*/
|
|
|
|
|
config(parentComponent, config:StringMap<string, any>): void {
|
2015-05-01 15:50:12 -07:00
|
|
|
|
if (!StringMapWrapper.contains(config, 'path')) {
|
|
|
|
|
throw new BaseException('Route config does not contain "path"');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!StringMapWrapper.contains(config, 'component') &&
|
2015-05-03 20:25:26 -07:00
|
|
|
|
!StringMapWrapper.contains(config, 'components') &&
|
|
|
|
|
!StringMapWrapper.contains(config, 'redirectTo')) {
|
|
|
|
|
throw new BaseException('Route config does not contain "component," "components," or "redirectTo"');
|
2015-05-01 15:50:12 -07:00
|
|
|
|
}
|
|
|
|
|
|
2015-05-14 15:24:35 +02:00
|
|
|
|
var recognizer:RouteRecognizer = MapWrapper.get(this._rules, parentComponent);
|
|
|
|
|
|
|
|
|
|
if (isBlank(recognizer)) {
|
2015-04-17 09:59:56 -07:00
|
|
|
|
recognizer = new RouteRecognizer();
|
|
|
|
|
MapWrapper.set(this._rules, parentComponent, recognizer);
|
|
|
|
|
}
|
|
|
|
|
|
2015-04-29 15:47:12 -07:00
|
|
|
|
config = normalizeConfig(config);
|
2015-04-17 09:59:56 -07:00
|
|
|
|
|
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-15 02:05:57 -07:00
|
|
|
|
var components = config['components'];
|
|
|
|
|
StringMapWrapper.forEach(components, (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-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)) {
|
|
|
|
|
for (var i=0; i<annotations.length; i++) {
|
|
|
|
|
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-14 15:24:35 +02:00
|
|
|
|
recognize(url:string, parentComponent): Instruction {
|
2015-04-17 09:59:56 -07:00
|
|
|
|
var componentRecognizer = MapWrapper.get(this._rules, parentComponent);
|
|
|
|
|
if (isBlank(componentRecognizer)) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
// Matches some beginning part of the given URL
|
|
|
|
|
var possibleMatches = componentRecognizer.recognize(url);
|
|
|
|
|
|
|
|
|
|
// A list of instructions that captures all of the given URL
|
|
|
|
|
var fullSolutions = ListWrapper.create();
|
2015-04-17 09:59:56 -07:00
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
for (var i = 0; i < possibleMatches.length; i++) {
|
|
|
|
|
var candidate : RouteMatch = possibleMatches[i];
|
|
|
|
|
|
|
|
|
|
// if the candidate captures all of the URL, add it to our list of solutions
|
|
|
|
|
if (candidate.unmatchedUrl.length == 0) {
|
|
|
|
|
ListWrapper.push(fullSolutions, routeMatchToInstruction(candidate, parentComponent));
|
2015-05-12 14:53:13 -07:00
|
|
|
|
} else {
|
2015-05-15 02:05:57 -07:00
|
|
|
|
|
|
|
|
|
// otherwise, recursively match the remaining part of the URL against the component's children
|
2015-05-12 14:53:13 -07:00
|
|
|
|
var children = StringMapWrapper.create(),
|
2015-05-15 02:05:57 -07:00
|
|
|
|
allChildrenMatch = true,
|
|
|
|
|
components = StringMapWrapper.get(candidate.handler, 'components');
|
2015-05-12 14:53:13 -07:00
|
|
|
|
|
2015-05-14 15:38:16 +02:00
|
|
|
|
var componentNames = StringMapWrapper.keys(components);
|
2015-05-15 02:05:57 -07:00
|
|
|
|
for (var nameIndex = 0; nameIndex < componentNames.length; nameIndex++) {
|
|
|
|
|
var name = componentNames[nameIndex];
|
|
|
|
|
var component = StringMapWrapper.get(components, name);
|
2015-05-14 15:38:16 +02:00
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
var childInstruction = this.recognize(candidate.unmatchedUrl, component);
|
2015-05-12 14:53:13 -07:00
|
|
|
|
if (isPresent(childInstruction)) {
|
2015-05-15 02:05:57 -07:00
|
|
|
|
childInstruction.params = candidate.params;
|
2015-05-12 14:53:13 -07:00
|
|
|
|
children[name] = childInstruction;
|
|
|
|
|
} else {
|
2015-05-15 02:05:57 -07:00
|
|
|
|
allChildrenMatch = false;
|
2015-05-14 15:38:16 +02:00
|
|
|
|
break;
|
2015-05-12 14:53:13 -07:00
|
|
|
|
}
|
2015-05-14 15:38:16 +02:00
|
|
|
|
}
|
2015-04-17 09:59:56 -07:00
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
if (allChildrenMatch) {
|
2015-05-12 14:53:13 -07:00
|
|
|
|
ListWrapper.push(fullSolutions, new Instruction({
|
|
|
|
|
component: parentComponent,
|
|
|
|
|
children: children,
|
2015-05-15 02:05:57 -07:00
|
|
|
|
matchedUrl: candidate.matchedUrl,
|
|
|
|
|
parentSpecificity: candidate.specificity
|
2015-05-12 14:53:13 -07:00
|
|
|
|
}));
|
2015-04-17 09:59:56 -07:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-12 14:53:13 -07:00
|
|
|
|
if (fullSolutions.length > 0) {
|
2015-05-15 02:05:57 -07:00
|
|
|
|
var mostSpecificSolution = fullSolutions[0];
|
|
|
|
|
for (var solutionIndex = 1; solutionIndex < fullSolutions.length; solutionIndex++) {
|
|
|
|
|
var solution = fullSolutions[solutionIndex];
|
|
|
|
|
if (solution.specificity > mostSpecificSolution.specificity) {
|
|
|
|
|
mostSpecificSolution = solution;
|
2015-05-14 15:38:16 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
return mostSpecificSolution;
|
2015-05-12 14:53:13 -07:00
|
|
|
|
}
|
|
|
|
|
|
2015-04-17 09:59:56 -07:00
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
2015-05-14 15:24:35 +02:00
|
|
|
|
generate(name:string, params:StringMap<string, string>, hostComponent): string {
|
2015-04-17 09:59:56 -07:00
|
|
|
|
//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-15 02:05:57 -07:00
|
|
|
|
function routeMatchToInstruction(routeMatch:RouteMatch, parentComponent): Instruction {
|
2015-04-17 09:59:56 -07:00
|
|
|
|
var children = StringMapWrapper.create();
|
2015-05-15 02:05:57 -07:00
|
|
|
|
var components = StringMapWrapper.get(routeMatch.handler, 'components');
|
|
|
|
|
StringMapWrapper.forEach(components, (component, outletName) => {
|
2015-04-17 09:59:56 -07:00
|
|
|
|
children[outletName] = new Instruction({
|
|
|
|
|
component: component,
|
2015-05-15 02:05:57 -07:00
|
|
|
|
params: routeMatch.params,
|
|
|
|
|
parentSpecificity: 0
|
2015-04-17 09:59:56 -07:00
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
return new Instruction({
|
|
|
|
|
component: parentComponent,
|
|
|
|
|
children: children,
|
2015-05-15 02:05:57 -07:00
|
|
|
|
matchedUrl: routeMatch.matchedUrl,
|
|
|
|
|
parentSpecificity: routeMatch.specificity
|
2015-04-17 09:59:56 -07:00
|
|
|
|
});
|
|
|
|
|
}
|
2015-04-29 15:47:12 -07:00
|
|
|
|
|
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
/*
|
|
|
|
|
* Given a config object:
|
|
|
|
|
* { 'component': Foo }
|
|
|
|
|
* Returns a new config object:
|
|
|
|
|
* { components: { default: Foo } }
|
|
|
|
|
*
|
|
|
|
|
* If the config object does not contain a `component` key, the original
|
|
|
|
|
* config object is returned.
|
|
|
|
|
*/
|
|
|
|
|
function normalizeConfig(config:StringMap<string, any>): StringMap<string, any> {
|
|
|
|
|
if (!StringMapWrapper.contains(config, 'component')) {
|
|
|
|
|
return config;
|
|
|
|
|
}
|
|
|
|
|
var newConfig = {
|
|
|
|
|
'components': {
|
|
|
|
|
'default': config['component']
|
|
|
|
|
}
|
|
|
|
|
};
|
2015-04-29 15:47:12 -07:00
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
StringMapWrapper.forEach(config, (value, key) => {
|
|
|
|
|
if (key != 'component' && key != 'components') {
|
|
|
|
|
newConfig[key] = value;
|
|
|
|
|
}
|
|
|
|
|
});
|
2015-04-29 15:47:12 -07:00
|
|
|
|
|
2015-05-15 02:05:57 -07:00
|
|
|
|
return newConfig;
|
2015-04-29 15:47:12 -07:00
|
|
|
|
}
|