Skip to content

Commit f66ce09

Browse files
committed
feat(router): support deep-linking to anywhere in the app
Closes #2642
1 parent 2335075 commit f66ce09

16 files changed

+330
-169
lines changed

modules/angular2/router.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,8 @@ import {List} from './src/facade/collection';
3434
export const routerDirectives: List<any> = CONST_EXPR([RouterOutlet, RouterLink]);
3535

3636
export var routerInjectables: List<any> = [
37-
RouteRegistry,
37+
bind(RouteRegistry)
38+
.toFactory((appRoot) => new RouteRegistry(appRoot), [appComponentTypeToken]),
3839
Pipeline,
3940
bind(LocationStrategy).toClass(HTML5LocationStrategy),
4041
Location,

modules/angular2/src/facade/collection.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,6 @@ export class ListWrapper {
239239
}
240240
static toString<T>(l: List<T>): string { return l.toString(); }
241241
static toJSON<T>(l: List<T>): string { return JSON.stringify(l); }
242-
243242
}
244243

245244
export function isListLikeIterable(obj): boolean {
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import {RouteHandler} from './route_handler';
2+
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
3+
import {isPresent, Type} from 'angular2/src/facade/lang';
4+
5+
export class AsyncRouteHandler implements RouteHandler {
6+
_resolvedComponent: Promise<any> = null;
7+
componentType: Type;
8+
9+
constructor(private _loader: Function) {}
10+
11+
resolveComponentType(): Promise<any> {
12+
if (isPresent(this._resolvedComponent)) {
13+
return this._resolvedComponent;
14+
}
15+
16+
return this._resolvedComponent = this._loader().then((componentType) => {
17+
this.componentType = componentType;
18+
return componentType;
19+
});
20+
}
21+
}

modules/angular2/src/router/instruction.ts

Lines changed: 21 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -6,54 +6,49 @@ import {
66
List,
77
ListWrapper
88
} from 'angular2/src/facade/collection';
9-
import {isPresent, normalizeBlank} from 'angular2/src/facade/lang';
9+
import {isPresent, isBlank, normalizeBlank} from 'angular2/src/facade/lang';
10+
11+
import {PathRecognizer} from './path_recognizer';
1012

1113
export class RouteParams {
1214
constructor(public params: StringMap<string, string>) {}
1315

1416
get(param: string): string { return normalizeBlank(StringMapWrapper.get(this.params, param)); }
1517
}
1618

19+
1720
/**
1821
* An `Instruction` represents the component hierarchy of the application based on a given route
1922
*/
2023
export class Instruction {
21-
component: any;
22-
child: Instruction;
23-
24-
// the part of the URL captured by this instruction
25-
capturedUrl: string;
26-
27-
// the part of the URL captured by this instruction and all children
24+
// "capturedUrl" is the part of the URL captured by this instruction
25+
// "accumulatedUrl" is the part of the URL captured by this instruction and all children
2826
accumulatedUrl: string;
2927

30-
params: StringMap<string, string>;
31-
reuse: boolean;
28+
reuse: boolean = false;
3229
specificity: number;
3330

34-
constructor({params, component, child, matchedUrl, parentSpecificity}: {
35-
params?: StringMap<string, any>,
36-
component?: any,
37-
child?: Instruction,
38-
matchedUrl?: string,
39-
parentSpecificity?: number
40-
} = {}) {
41-
this.reuse = false;
42-
this.capturedUrl = matchedUrl;
43-
this.accumulatedUrl = matchedUrl;
44-
this.specificity = parentSpecificity;
31+
private _params: StringMap<string, string>;
32+
33+
constructor(public component: any, public capturedUrl: string,
34+
private _recognizer: PathRecognizer, public child: Instruction = null) {
35+
this.accumulatedUrl = capturedUrl;
36+
this.specificity = _recognizer.specificity;
4537
if (isPresent(child)) {
4638
this.child = child;
4739
this.specificity += child.specificity;
4840
var childUrl = child.accumulatedUrl;
4941
if (isPresent(childUrl)) {
5042
this.accumulatedUrl += childUrl;
5143
}
52-
} else {
53-
this.child = null;
5444
}
55-
this.component = component;
56-
this.params = params;
45+
}
46+
47+
params(): StringMap<string, string> {
48+
if (isBlank(this._params)) {
49+
this._params = this._recognizer.parseParams(this.capturedUrl);
50+
}
51+
return this._params;
5752
}
5853

5954
hasChild(): boolean { return isPresent(this.child); }
@@ -73,5 +68,5 @@ export class Instruction {
7368

7469
function shouldReuseComponent(instr1: Instruction, instr2: Instruction): boolean {
7570
return instr1.component == instr2.component &&
76-
StringMapWrapper.equals(instr1.params, instr2.params);
71+
StringMapWrapper.equals(instr1.params(), instr2.params());
7772
}

modules/angular2/src/router/path_recognizer.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
BaseException,
99
normalizeBlank
1010
} from 'angular2/src/facade/lang';
11+
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
1112
import {
1213
Map,
1314
MapWrapper,
@@ -19,6 +20,7 @@ import {
1920
import {IMPLEMENTS} from 'angular2/src/facade/lang';
2021

2122
import {escapeRegex} from './url';
23+
import {RouteHandler} from './route_handler';
2224

2325
// TODO(jeffbcross): implement as interface when ts2dart adds support:
2426
// https://github.com/angular/ts2dart/issues/173
@@ -27,7 +29,7 @@ export class Segment {
2729
regex: string;
2830
}
2931

30-
export class ContinuationSegment extends Segment {
32+
class ContinuationSegment extends Segment {
3133
generate(params): string { return ''; }
3234
}
3335

@@ -52,7 +54,7 @@ class DynamicSegment {
5254
generate(params: StringMap<string, string>): string {
5355
if (!StringMapWrapper.contains(params, this.name)) {
5456
throw new BaseException(
55-
`Route generator for '${this.name}' was not included in parameters passed.`)
57+
`Route generator for '${this.name}' was not included in parameters passed.`);
5658
}
5759
return normalizeBlank(StringMapWrapper.get(params, this.name));
5860
}
@@ -135,7 +137,7 @@ export class PathRecognizer {
135137
specificity: number;
136138
terminal: boolean = true;
137139

138-
constructor(public path: string, public handler: any) {
140+
constructor(public path: string, public handler: RouteHandler) {
139141
var parsed = parsePathString(path);
140142
var specificity = parsed['specificity'];
141143
var segments = parsed['segments'];
@@ -178,7 +180,9 @@ export class PathRecognizer {
178180
}
179181

180182
generate(params: StringMap<string, string>): string {
181-
return ListWrapper.join(
182-
ListWrapper.map(this.segments, (segment) => '/' + segment.generate(params)), '');
183+
return ListWrapper.join(ListWrapper.map(this.segments, (segment) => segment.generate(params)),
184+
'/');
183185
}
186+
187+
resolveComponentType(): Promise<any> { return this.handler.resolveComponentType(); }
184188
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import {Promise, PromiseWrapper} from 'angular2/src/facade/async';
2+
import {Type} from 'angular2/src/facade/lang';
3+
4+
export interface RouteHandler {
5+
componentType: Type;
6+
resolveComponentType(): Promise<any>;
7+
}

modules/angular2/src/router/route_recognizer.ts

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ import {
22
RegExp,
33
RegExpWrapper,
44
StringWrapper,
5+
isBlank,
56
isPresent,
7+
isType,
8+
isStringMap,
69
BaseException
710
} from 'angular2/src/facade/lang';
811
import {
@@ -14,7 +17,10 @@ import {
1417
StringMapWrapper
1518
} from 'angular2/src/facade/collection';
1619

17-
import {PathRecognizer, ContinuationSegment} from './path_recognizer';
20+
import {PathRecognizer} from './path_recognizer';
21+
import {RouteHandler} from './route_handler';
22+
import {AsyncRouteHandler} from './async_route_handler';
23+
import {SyncRouteHandler} from './sync_route_handler';
1824

1925
/**
2026
* `RouteRecognizer` is responsible for recognizing routes for a single component.
@@ -33,7 +39,8 @@ export class RouteRecognizer {
3339
this.redirects.set(path, target);
3440
}
3541

36-
addConfig(path: string, handler: any, alias: string = null): boolean {
42+
addConfig(path: string, handlerObj: any, alias: string = null): boolean {
43+
var handler = configObjToHandler(handlerObj['component']);
3744
var recognizer = new PathRecognizer(path, handler);
3845
MapWrapper.forEach(this.matchers, (matcher, _) => {
3946
if (recognizer.regex.toString() == matcher.regex.toString()) {
@@ -65,28 +72,21 @@ export class RouteRecognizer {
6572
if (path == url) {
6673
url = target;
6774
}
68-
} else if (StringWrapper.startsWith(url, path)) {
69-
url = target + StringWrapper.substring(url, path.length);
75+
} else if (url.startsWith(path)) {
76+
url = target + url.substring(path.length);
7077
}
7178
});
7279

7380
MapWrapper.forEach(this.matchers, (pathRecognizer, regex) => {
7481
var match;
7582
if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) {
76-
// TODO(btford): determine a good generic way to deal with terminal matches
7783
var matchedUrl = '/';
7884
var unmatchedUrl = '';
7985
if (url != '/') {
8086
matchedUrl = match[0];
81-
unmatchedUrl = StringWrapper.substring(url, match[0].length);
87+
unmatchedUrl = url.substring(match[0].length);
8288
}
83-
solutions.push(new RouteMatch({
84-
specificity: pathRecognizer.specificity,
85-
handler: pathRecognizer.handler,
86-
params: pathRecognizer.parseParams(url),
87-
matchedUrl: matchedUrl,
88-
unmatchedUrl: unmatchedUrl
89-
}));
89+
solutions.push(new RouteMatch(pathRecognizer, matchedUrl, unmatchedUrl));
9090
}
9191
});
9292

@@ -95,30 +95,39 @@ export class RouteRecognizer {
9595

9696
hasRoute(name: string): boolean { return this.names.has(name); }
9797

98-
generate(name: string, params: any): string {
99-
var pathRecognizer = this.names.get(name);
100-
return isPresent(pathRecognizer) ? pathRecognizer.generate(params) : null;
98+
generate(name: string, params: any): StringMap<string, any> {
99+
var pathRecognizer: PathRecognizer = this.names.get(name);
100+
if (isBlank(pathRecognizer)) {
101+
return null;
102+
}
103+
var url = pathRecognizer.generate(params);
104+
return {url, 'nextComponent': pathRecognizer.handler.componentType};
101105
}
102106
}
103107

104108
export class RouteMatch {
105-
specificity: number;
106-
handler: StringMap<string, any>;
107-
params: StringMap<string, string>;
108-
matchedUrl: string;
109-
unmatchedUrl: string;
109+
constructor(public recognizer: PathRecognizer, public matchedUrl: string,
110+
public unmatchedUrl: string) {}
111+
112+
params(): StringMap<string, string> { return this.recognizer.parseParams(this.matchedUrl); }
113+
}
110114

111-
constructor({specificity, handler, params, matchedUrl, unmatchedUrl}: {
112-
specificity?: number,
113-
handler?: StringMap<string, any>,
114-
params?: StringMap<string, string>,
115-
matchedUrl?: string,
116-
unmatchedUrl?: string
117-
} = {}) {
118-
this.specificity = specificity;
119-
this.handler = handler;
120-
this.params = params;
121-
this.matchedUrl = matchedUrl;
122-
this.unmatchedUrl = unmatchedUrl;
115+
function configObjToHandler(config: any): RouteHandler {
116+
if (isType(config)) {
117+
return new SyncRouteHandler(config);
118+
} else if (isStringMap(config)) {
119+
if (isBlank(config['type'])) {
120+
throw new BaseException(
121+
`Component declaration when provided as a map should include a 'type' property`);
122+
}
123+
var componentType = config['type'];
124+
if (componentType == 'constructor') {
125+
return new SyncRouteHandler(config['constructor']);
126+
} else if (componentType == 'loader') {
127+
return new AsyncRouteHandler(config['loader']);
128+
} else {
129+
throw new BaseException(`oops`);
130+
}
123131
}
132+
throw new BaseException(`Unexpected component "${config}".`);
124133
}

0 commit comments

Comments
 (0)