refactor(router): improve recognition and generation pipeline

This is a big change. @matsko also deserves much of the credit for the implementation.

Previously, `ComponentInstruction`s held all the state for async components.
Now, we introduce several subclasses for `Instruction` to describe each type of navigation.

BREAKING CHANGE:

Redirects now use the Link DSL syntax. Before:

```
@RouteConfig([
  { path: '/foo', redirectTo: '/bar' },
  { path: '/bar', component: BarCmp }
])
```

After:

```
@RouteConfig([
  { path: '/foo', redirectTo: ['Bar'] },
  { path: '/bar', component: BarCmp, name: 'Bar' }
])
```

BREAKING CHANGE:

This also introduces `useAsDefault` in the RouteConfig, which makes cases like lazy-loading
and encapsulating large routes with sub-routes easier.

Previously, you could use `redirectTo` like this to expand a URL like `/tab` to `/tab/posts`:

@RouteConfig([
  { path: '/tab', redirectTo: '/tab/users' }
  { path: '/tab', component: TabsCmp, name: 'Tab' }
])
AppCmp { ... }

Now the recommended way to handle this is case is to use `useAsDefault` like so:

```
@RouteConfig([
  { path: '/tab', component: TabsCmp, name: 'Tab' }
])
AppCmp { ... }

@RouteConfig([
  { path: '/posts', component: PostsCmp, useAsDefault: true, name: 'Posts' },
  { path: '/users', component: UsersCmp, name: 'Users' }
])
TabsCmp { ... }
```

In the above example, you can write just `['/Tab']` and the route `Users` is automatically selected as a child route.

Closes #4170
Closes #4490
Closes #4694
Closes #5200

Closes #5352
This commit is contained in:
Brian Ford
2015-11-20 23:18:43 +00:00
parent 422a7b18f6
commit cf7292fcb1
41 changed files with 2968 additions and 1115 deletions
+9 -34
View File
@@ -7,12 +7,9 @@ import {
isBlank
} from 'angular2/src/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/facade/exceptions';
import {Map, MapWrapper, StringMapWrapper} from 'angular2/src/facade/collection';
import {RouteHandler} from './route_handler';
import {Url, RootUrl, serializeParams} from './url_parser';
import {ComponentInstruction, ComponentInstruction_} from './instruction';
class TouchMap {
map: {[key: string]: string} = {};
@@ -33,7 +30,7 @@ class TouchMap {
}
getUnused(): {[key: string]: any} {
var unused: {[key: string]: any} = StringMapWrapper.create();
var unused: {[key: string]: any} = {};
var keys = StringMapWrapper.keys(this.keys);
keys.forEach(key => unused[key] = StringMapWrapper.get(this.map, key));
return unused;
@@ -126,7 +123,6 @@ function parsePathString(route: string): {[key: string]: any} {
results.push(new StarSegment(match[1]));
} else if (segment == '...') {
if (i < limit) {
// TODO (matsko): setup a proper error here `
throw new BaseException(`Unexpected "..." before the end of the path for "${route}".`);
}
results.push(new ContinuationSegment());
@@ -175,23 +171,17 @@ function assertPath(path: string) {
}
}
export class PathMatch {
constructor(public instruction: ComponentInstruction, public remaining: Url,
public remainingAux: Url[]) {}
}
// represents something like '/foo/:bar'
/**
* Parses a URL string using a given matcher DSL, and generates URLs from param maps
*/
export class PathRecognizer {
private _segments: Segment[];
specificity: number;
terminal: boolean = true;
hash: string;
private _cache: Map<string, ComponentInstruction> = new Map<string, ComponentInstruction>();
// TODO: cache component instruction instances by params and by ParsedUrl instance
constructor(public path: string, public handler: RouteHandler) {
constructor(public path: string) {
assertPath(path);
var parsed = parsePathString(path);
@@ -203,8 +193,7 @@ export class PathRecognizer {
this.terminal = !(lastSegment instanceof ContinuationSegment);
}
recognize(beginningSegment: Url): PathMatch {
recognize(beginningSegment: Url): {[key: string]: any} {
var nextSegment = beginningSegment;
var currentSegment: Url;
var positionalParams = {};
@@ -247,7 +236,6 @@ export class PathRecognizer {
var urlPath = captured.join('/');
var auxiliary;
var instruction: ComponentInstruction;
var urlParams;
var allParams;
if (isPresent(currentSegment)) {
@@ -267,12 +255,11 @@ export class PathRecognizer {
auxiliary = [];
urlParams = [];
}
instruction = this._getInstruction(urlPath, urlParams, this, allParams);
return new PathMatch(instruction, nextSegment, auxiliary);
return {urlPath, urlParams, allParams, auxiliary, nextSegment};
}
generate(params: {[key: string]: any}): ComponentInstruction {
generate(params: {[key: string]: any}): {[key: string]: any} {
var paramTokens = new TouchMap(params);
var path = [];
@@ -288,18 +275,6 @@ export class PathRecognizer {
var nonPositionalParams = paramTokens.getUnused();
var urlParams = serializeParams(nonPositionalParams);
return this._getInstruction(urlPath, urlParams, this, params);
}
private _getInstruction(urlPath: string, urlParams: string[], _recognizer: PathRecognizer,
params: {[key: string]: any}): ComponentInstruction {
var hashKey = urlPath + '?' + urlParams.join('?');
if (this._cache.has(hashKey)) {
return this._cache.get(hashKey);
}
var instruction = new ComponentInstruction_(urlPath, urlParams, _recognizer, params);
this._cache.set(hashKey, instruction);
return instruction;
return {urlPath, urlParams};
}
}