feat(router): support deep-linking to siblings

Closes #2807
This commit is contained in:
Brian Ford
2015-07-06 17:41:15 -07:00
parent d828664d0c
commit 286a249a9a
7 changed files with 198 additions and 92 deletions

View File

@ -32,8 +32,6 @@ import {Injectable} from 'angular2/di';
export class RouteRegistry {
private _rules: Map<any, RouteRecognizer> = new Map();
constructor(private _rootHostComponent: any) {}
/**
* Given a component and a configuration object, add the route to this registry
*/
@ -144,41 +142,22 @@ export class RouteRegistry {
}
/**
* Given a list with component names and params like: `['./user', {id: 3 }]`
* 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`.
*/
generate(linkParams: List<any>, parentComponent): string {
let normalizedLinkParams = splitAndFlattenLinkParams(linkParams);
let url = '/';
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) {
let msg = `Link "${ListWrapper.toJSON(linkParams)}" must include a route name.`;
throw new BaseException(msg);
}
for (let i = 1; i < normalizedLinkParams.length; i += 1) {
let segment = normalizedLinkParams[i];
for (let i = 0; i < linkParams.length; i += 1) {
let segment = linkParams[i];
if (!isString(segment)) {
throw new BaseException(`Unexpected segment "${segment}" in link DSL. Expected a string.`);
} else if (segment == '' || segment == '.' || segment == '..') {
throw new BaseException(`"${segment}/" is only allowed at the beginning of a link DSL.`);
}
let params = null;
if (i + 1 < normalizedLinkParams.length) {
let nextSegment = normalizedLinkParams[i + 1];
if (i + 1 < linkParams.length) {
let nextSegment = linkParams[i + 1];
if (isStringMap(nextSegment)) {
params = nextSegment;
i += 1;
@ -274,18 +253,3 @@ 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;
}, []);
}

View File

@ -1,6 +1,14 @@
import {Promise, PromiseWrapper, EventEmitter, ObservableWrapper} from 'angular2/src/facade/async';
import {Map, MapWrapper, List, ListWrapper} from 'angular2/src/facade/collection';
import {isBlank, isPresent, Type, isArray} from 'angular2/src/facade/lang';
import {
isBlank,
isString,
StringWrapper,
isPresent,
Type,
isArray,
BaseException
} from 'angular2/src/facade/lang';
import {RouteRegistry} from './route_registry';
import {Pipeline} from './pipeline';
@ -42,7 +50,7 @@ export class Router {
// todo(jeffbcross): rename _registry to registry since it is accessed from subclasses
// todo(jeffbcross): rename _pipeline to pipeline since it is accessed from subclasses
constructor(public _registry: RouteRegistry, public _pipeline: Pipeline, public parent: Router,
constructor(public registry: RouteRegistry, public _pipeline: Pipeline, public parent: Router,
public hostComponent: any) {}
@ -88,9 +96,9 @@ export class Router {
config(config: StringMap<string, any>| List<StringMap<string, any>>): Promise<any> {
if (isArray(config)) {
(<List<any>>config)
.forEach((configObject) => { this._registry.config(this.hostComponent, configObject); });
.forEach((configObject) => { this.registry.config(this.hostComponent, configObject); });
} else {
this._registry.config(this.hostComponent, config);
this.registry.config(this.hostComponent, config);
}
return this.renavigate();
}
@ -170,7 +178,7 @@ export class Router {
* Given a URL, returns an instruction representing the component graph
*/
recognize(url: string): Promise<Instruction> {
return this._registry.recognize(url, this.hostComponent);
return this.registry.recognize(url, this.hostComponent);
}
@ -192,7 +200,48 @@ export class Router {
* app's base href.
*/
generate(linkParams: List<any>): string {
return this._registry.generate(linkParams, this.hostComponent);
let normalizedLinkParams = splitAndFlattenLinkParams(linkParams);
var first = ListWrapper.first(normalizedLinkParams);
var rest = ListWrapper.slice(normalizedLinkParams, 1);
var router = this;
// 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 == '') {
while (isPresent(router.parent)) {
router = router.parent;
}
} else if (first == '..') {
router = router.parent;
while (ListWrapper.first(rest) == '..') {
rest = ListWrapper.slice(rest, 1);
router = router.parent;
if (isBlank(router)) {
throw new BaseException(
`Link "${ListWrapper.toJSON(linkParams)}" has too many "../" segments.`);
}
}
} else if (first != '.') {
throw new BaseException(
`Link "${ListWrapper.toJSON(linkParams)}" must start with "/", "./", or "../"`);
}
if (rest[rest.length - 1] == '') {
ListWrapper.removeLast(rest);
}
if (rest.length < 1) {
let msg = `Link "${ListWrapper.toJSON(linkParams)}" must include a route name.`;
throw new BaseException(msg);
}
let url = '';
if (isPresent(router.parent) && isPresent(router.parent._currentInstruction)) {
url = router.parent._currentInstruction.capturedUrl;
}
return url + '/' + this.registry.generate(rest, router.hostComponent);
}
}
@ -204,7 +253,7 @@ export class RootRouter extends Router {
super(registry, pipeline, null, hostComponent);
this._location = location;
this._location.subscribe((change) => this.navigate(change['url']));
this._registry.configFromComponent(hostComponent);
this.registry.configFromComponent(hostComponent);
this.navigate(location.path());
}
@ -216,7 +265,7 @@ export class RootRouter extends Router {
class ChildRouter extends Router {
constructor(parent: Router, hostComponent) {
super(parent._registry, parent._pipeline, parent, hostComponent);
super(parent.registry, parent._pipeline, parent, hostComponent);
this.parent = parent;
}
@ -226,3 +275,18 @@ class ChildRouter extends Router {
return this.parent.navigate(url);
}
}
/*
* 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;
}, []);
}

View File

@ -27,10 +27,11 @@ import {Location} from './location';
* means that we want to generate a link for the `team` route with params `{teamId: 1}`,
* and with a child route `user` with params `{userId: 2}`.
*
* The first route name should be prepended with either `./` or `/`.
* The first route name should be prepended with `/`, `./`, or `../`.
* If the route begins with `/`, the router will look up the route from the root of the app.
* If the route begins with `./`, the router will instead look in the current component's
* children for the route.
* children for the route. And if the route begins with `../`, the router will look at the
* current component's parent.
*
* @exportedAs angular2/router
*/