feat(router): support deep-linking to anywhere in the app

Closes #2642
This commit is contained in:
Brian Ford
2015-06-30 13:18:51 -07:00
parent 2335075506
commit f66ce096d8
16 changed files with 331 additions and 170 deletions

View File

@ -13,6 +13,7 @@ import {
isPresent,
isBlank,
isType,
isString,
isStringMap,
isFunction,
StringWrapper,
@ -29,7 +30,9 @@ import {Injectable} from 'angular2/di';
*/
@Injectable()
export class RouteRegistry {
_rules: Map<any, RouteRecognizer> = new Map();
private _rules: Map<any, RouteRecognizer> = new Map();
constructor(private _rootHostComponent: any) {}
/**
* Given a component and a configuration object, add the route to this registry
@ -118,40 +121,80 @@ export class RouteRegistry {
}
_completeRouteMatch(candidate: RouteMatch): Promise<Instruction> {
return componentHandlerToComponentType(candidate.handler)
.then((componentType) => {
this.configFromComponent(componentType);
_completeRouteMatch(partialMatch: RouteMatch): Promise<Instruction> {
var recognizer = partialMatch.recognizer;
var handler = recognizer.handler;
return handler.resolveComponentType().then((componentType) => {
this.configFromComponent(componentType);
if (candidate.unmatchedUrl.length == 0) {
return new Instruction({
component: componentType,
params: candidate.params,
matchedUrl: candidate.matchedUrl,
parentSpecificity: candidate.specificity
});
}
if (partialMatch.unmatchedUrl.length == 0) {
return new Instruction(componentType, partialMatch.matchedUrl, recognizer);
}
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
});
});
});
return this.recognize(partialMatch.unmatchedUrl, componentType)
.then(childInstruction => {
if (isBlank(childInstruction)) {
return null;
} else {
return new Instruction(componentType, partialMatch.matchedUrl, recognizer,
childInstruction);
}
});
});
}
generate(name: string, params: StringMap<string, string>, hostComponent): string {
// TODO: implement for hierarchical routes
var componentRecognizer = this._rules.get(hostComponent);
return isPresent(componentRecognizer) ? componentRecognizer.generate(name, params) : null;
/**
* Given a list with component names and params like: `['./user', {id: 3 }]`
* generates a url with a leading slash relative to the provided `parentComponent`.
*/
generate(linkParams: List<any>, parentComponent): string {
let normalizedLinkParams = splitAndFlattenLinkParams(linkParams);
let url = '/';
let componentCursor = parentComponent;
// 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 (normalizedLinkParams[0] == '') {
componentCursor = this._rootHostComponent;
} else if (normalizedLinkParams[0] != '.') {
throw new BaseException(
`Link "${ListWrapper.toJSON(linkParams)}" must start with "/" or "./"`);
}
if (normalizedLinkParams[normalizedLinkParams.length - 1] == '') {
ListWrapper.removeLast(normalizedLinkParams);
}
if (normalizedLinkParams.length < 2) {
throw new BaseException(
`Link "${ListWrapper.toJSON(linkParams)}" must include a route name.`);
}
for (let i = 1; i < normalizedLinkParams.length; i += 1) {
let segment = normalizedLinkParams[i];
if (!isString(segment)) {
throw new BaseException(`Unexpected segment "${segment}" in link DSL. Expected a string.`);
}
let params = null;
if (i + 1 < normalizedLinkParams.length) {
let nextSegment = normalizedLinkParams[i + 1];
if (isStringMap(nextSegment)) {
params = nextSegment;
i += 1;
}
}
var componentRecognizer = this._rules.get(componentCursor);
if (isBlank(componentRecognizer)) {
throw new BaseException(`Could not find route config for "${segment}".`);
}
var response = componentRecognizer.generate(segment, params);
url += response['url'];
componentCursor = response['nextComponent'];
}
return url;
}
}
@ -200,19 +243,6 @@ function normalizeComponentDeclaration(config: any): StringMap<string, any> {
}
}
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`);
}
}
/*
* Given a list of instructions, returns the most specific instruction
*/
@ -244,3 +274,18 @@ function assertTerminalComponent(component, path) {
}
}
}
/*
* Given: ['/a/b', {c: 2}]
* Returns: ['', 'a', 'b', {c: 2}]
*/
var SLASH = new RegExp('/');
function splitAndFlattenLinkParams(linkParams: List<any>): List<any> {
return ListWrapper.reduce(linkParams, (accumulation, item) => {
if (isString(item)) {
return ListWrapper.concat(accumulation, StringWrapper.split(item, SLASH));
}
accumulation.push(item);
return accumulation;
}, []);
}