Skip to content

Commit fdffcab

Browse files
committed
feat(router): use querystring params for top-level routes
Closes angular#3017
1 parent a9e7c90 commit fdffcab

File tree

10 files changed

+277
-44
lines changed

10 files changed

+277
-44
lines changed
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import {isPresent} from 'angular2/src/facade/lang';
2+
3+
export function parseAndAssignParamString(splitToken: string, paramString: string,
4+
keyValueMap: StringMap<string, string>): void {
5+
var first = paramString[0];
6+
if (first == '?' || first == ';') {
7+
paramString = paramString.substring(1);
8+
}
9+
10+
paramString.split(splitToken)
11+
.forEach((entry) => {
12+
var tuple = entry.split('=');
13+
var key = tuple[0];
14+
if (!isPresent(keyValueMap[key])) {
15+
var value = tuple.length > 1 ? tuple[1] : true;
16+
keyValueMap[key] = value;
17+
}
18+
});
19+
}

modules/angular2/src/router/instruction.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,9 @@ export class Instruction {
2727
reuse: boolean = false;
2828
specificity: number;
2929

30-
private _params: StringMap<string, string>;
31-
3230
constructor(public component: any, public capturedUrl: string,
33-
private _recognizer: PathRecognizer, public child: Instruction = null) {
31+
private _recognizer: PathRecognizer, public child: Instruction = null,
32+
private _params: StringMap<string, any> = null) {
3433
this.accumulatedUrl = capturedUrl;
3534
this.specificity = _recognizer.specificity;
3635
if (isPresent(child)) {

modules/angular2/src/router/path_recognizer.ts

Lines changed: 33 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import {
1717
ListWrapper
1818
} from 'angular2/src/facade/collection';
1919
import {IMPLEMENTS} from 'angular2/src/facade/lang';
20-
20+
import {parseAndAssignParamString} from 'angular2/src/router/helpers';
2121
import {escapeRegex} from './url';
2222
import {RouteHandler} from './route_handler';
2323

@@ -63,19 +63,6 @@ function normalizeString(obj: any): string {
6363
}
6464
}
6565

66-
function parseAndAssignMatrixParams(keyValueMap, matrixString) {
67-
if (matrixString[0] == ';') {
68-
matrixString = matrixString.substring(1);
69-
}
70-
71-
matrixString.split(';').forEach((entry) => {
72-
var tuple = entry.split('=');
73-
var key = tuple[0];
74-
var value = tuple.length > 1 ? tuple[1] : true;
75-
keyValueMap[key] = value;
76-
});
77-
}
78-
7966
class ContinuationSegment extends Segment {}
8067

8168
class StaticSegment extends Segment {
@@ -198,7 +185,10 @@ export class PathRecognizer {
198185
specificity: number;
199186
terminal: boolean = true;
200187

201-
constructor(public path: string, public handler: RouteHandler) {
188+
static matrixRegex: RegExp = RegExpWrapper.create('^(.*\/[^\/]+?)(;[^\/]+)?\/?$');
189+
static queryRegex: RegExp = RegExpWrapper.create('^(.*\/[^\/]+?)(\\?[^\/]+)?$');
190+
191+
constructor(public path: string, public handler: RouteHandler, public isRoot: boolean = false) {
202192
assertPath(path);
203193
var parsed = parsePathString(path);
204194
var specificity = parsed['specificity'];
@@ -228,16 +218,16 @@ export class PathRecognizer {
228218
var containsStarSegment =
229219
segmentsLimit >= 0 && this.segments[segmentsLimit] instanceof StarSegment;
230220

231-
var matrixString;
221+
var paramsString, useQueryString = this.isRoot && this.terminal;
232222
if (!containsStarSegment) {
233-
var matches =
234-
RegExpWrapper.firstMatch(RegExpWrapper.create('^(.*\/[^\/]+?)(;[^\/]+)?\/?$'), url);
223+
var matches = RegExpWrapper.firstMatch(
224+
useQueryString ? PathRecognizer.queryRegex : PathRecognizer.matrixRegex, url);
235225
if (isPresent(matches)) {
236226
url = matches[1];
237-
matrixString = matches[2];
227+
paramsString = matches[2];
238228
}
239229

240-
url = StringWrapper.replaceAll(url, /(;[^\/]+)(?=(\/|\Z))/g, '');
230+
url = StringWrapper.replaceAll(url, /(;[^\/]+)(?=(\/|$))/g, '');
241231
}
242232

243233
var params = StringMapWrapper.create();
@@ -256,8 +246,11 @@ export class PathRecognizer {
256246
}
257247
}
258248

259-
if (isPresent(matrixString) && matrixString.length > 0 && matrixString[0] == ';') {
260-
parseAndAssignMatrixParams(params, matrixString);
249+
if (isPresent(paramsString) && paramsString.length > 0) {
250+
var expectedStartingValue = useQueryString ? '?' : ';';
251+
if (paramsString[0] == expectedStartingValue) {
252+
parseAndAssignParamString(expectedStartingValue, paramsString, params);
253+
}
261254
}
262255

263256
return params;
@@ -266,6 +259,7 @@ export class PathRecognizer {
266259
generate(params: StringMap<string, any>): string {
267260
var paramTokens = new TouchMap(params);
268261
var applyLeadingSlash = false;
262+
var useQueryString = this.isRoot && this.terminal;
269263

270264
var url = '';
271265
for (var i = 0; i < this.segments.length; i++) {
@@ -279,12 +273,23 @@ export class PathRecognizer {
279273
}
280274

281275
var unusedParams = paramTokens.getUnused();
282-
StringMapWrapper.forEach(unusedParams, (value, key) => {
283-
url += ';' + key;
284-
if (isPresent(value)) {
285-
url += '=' + value;
286-
}
287-
});
276+
if (!StringMapWrapper.isEmpty(unusedParams)) {
277+
url += useQueryString ? '?' : ';';
278+
var paramToken = useQueryString ? '&' : ';';
279+
var i = 0;
280+
StringMapWrapper.forEach(unusedParams, (value, key) => {
281+
if (i++ > 0) {
282+
url += paramToken;
283+
}
284+
url += key;
285+
if (!isPresent(value) && useQueryString) {
286+
value = 'true';
287+
}
288+
if (isPresent(value)) {
289+
url += '=' + value;
290+
}
291+
});
292+
}
288293

289294
if (applyLeadingSlash) {
290295
url += '/';

modules/angular2/src/router/route_recognizer.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import {RouteHandler} from './route_handler';
2222
import {Route, AsyncRoute, Redirect, RouteDefinition} from './route_config_impl';
2323
import {AsyncRouteHandler} from './async_route_handler';
2424
import {SyncRouteHandler} from './sync_route_handler';
25+
import {parseAndAssignParamString} from 'angular2/src/router/helpers';
2526

2627
/**
2728
* `RouteRecognizer` is responsible for recognizing routes for a single component.
@@ -33,6 +34,8 @@ export class RouteRecognizer {
3334
redirects: Map<string, string> = new Map();
3435
matchers: Map<RegExp, PathRecognizer> = new Map();
3536

37+
constructor(public isRoot: boolean = false) {}
38+
3639
config(config: RouteDefinition): boolean {
3740
var handler;
3841
if (config instanceof Redirect) {
@@ -44,7 +47,7 @@ export class RouteRecognizer {
4447
} else if (config instanceof AsyncRoute) {
4548
handler = new AsyncRouteHandler(config.loader);
4649
}
47-
var recognizer = new PathRecognizer(config.path, handler);
50+
var recognizer = new PathRecognizer(config.path, handler, this.isRoot);
4851
MapWrapper.forEach(this.matchers, (matcher, _) => {
4952
if (recognizer.regex.toString() == matcher.regex.toString()) {
5053
throw new BaseException(
@@ -80,6 +83,17 @@ export class RouteRecognizer {
8083
}
8184
});
8285

86+
var queryParams = StringMapWrapper.create();
87+
var queryString = '';
88+
var queryIndex = url.indexOf('?');
89+
if (queryIndex >= 0) {
90+
queryString = url.substring(queryIndex + 1);
91+
url = url.substring(0, queryIndex);
92+
}
93+
if (this.isRoot && queryString.length > 0) {
94+
parseAndAssignParamString('&', queryString, queryParams);
95+
}
96+
8397
MapWrapper.forEach(this.matchers, (pathRecognizer, regex) => {
8498
var match;
8599
if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) {
@@ -89,7 +103,12 @@ export class RouteRecognizer {
89103
matchedUrl = match[0];
90104
unmatchedUrl = url.substring(match[0].length);
91105
}
92-
solutions.push(new RouteMatch(pathRecognizer, matchedUrl, unmatchedUrl));
106+
var params = null;
107+
if (pathRecognizer.terminal && !StringMapWrapper.isEmpty(queryParams)) {
108+
params = queryParams;
109+
matchedUrl += '?' + queryString;
110+
}
111+
solutions.push(new RouteMatch(pathRecognizer, matchedUrl, unmatchedUrl, params));
93112
}
94113
});
95114

@@ -109,10 +128,22 @@ export class RouteRecognizer {
109128
}
110129

111130
export class RouteMatch {
131+
private _params: StringMap<string, any>;
132+
private _paramsParsed: boolean = false;
133+
112134
constructor(public recognizer: PathRecognizer, public matchedUrl: string,
113-
public unmatchedUrl: string) {}
135+
public unmatchedUrl: string, p: StringMap<string, any> = null) {
136+
this._params = isPresent(p) ? p : StringMapWrapper.create();
137+
}
114138

115-
params(): StringMap<string, string> { return this.recognizer.parseParams(this.matchedUrl); }
139+
params(): StringMap<string, any> {
140+
if (!this._paramsParsed) {
141+
this._paramsParsed = true;
142+
StringMapWrapper.forEach(this.recognizer.parseParams(this.matchedUrl),
143+
(value, key) => { StringMapWrapper.set(this._params, key, value); });
144+
}
145+
return this._params;
146+
}
116147
}
117148

118149
function configObjToHandler(config: any): RouteHandler {

modules/angular2/src/router/route_registry.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,13 @@ export class RouteRegistry {
3737
/**
3838
* Given a component and a configuration object, add the route to this registry
3939
*/
40-
config(parentComponent: any, config: RouteDefinition): void {
40+
config(parentComponent: any, config: RouteDefinition, isRootLevelRoute: boolean = false): void {
4141
config = normalizeRouteConfig(config);
4242

4343
var recognizer: RouteRecognizer = this._rules.get(parentComponent);
4444

4545
if (isBlank(recognizer)) {
46-
recognizer = new RouteRecognizer();
46+
recognizer = new RouteRecognizer(isRootLevelRoute);
4747
this._rules.set(parentComponent, recognizer);
4848
}
4949

@@ -61,7 +61,7 @@ export class RouteRegistry {
6161
/**
6262
* Reads the annotations of a component and configures the registry based on them
6363
*/
64-
configFromComponent(component: any): void {
64+
configFromComponent(component: any, isRootComponent: boolean = false): void {
6565
if (!isType(component)) {
6666
return;
6767
}
@@ -77,7 +77,8 @@ export class RouteRegistry {
7777
var annotation = annotations[i];
7878

7979
if (annotation instanceof RouteConfig) {
80-
ListWrapper.forEach(annotation.configs, (config) => this.config(component, config));
80+
ListWrapper.forEach(annotation.configs,
81+
(config) => this.config(component, config, isRootComponent));
8182
}
8283
}
8384
}
@@ -120,7 +121,8 @@ export class RouteRegistry {
120121

121122
if (partialMatch.unmatchedUrl.length == 0) {
122123
if (recognizer.terminal) {
123-
return new Instruction(componentType, partialMatch.matchedUrl, recognizer);
124+
return new Instruction(componentType, partialMatch.matchedUrl, recognizer, null,
125+
partialMatch.params());
124126
} else {
125127
return null;
126128
}

modules/angular2/src/router/router.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,9 @@ export class Router {
8787
* ```
8888
*/
8989
config(definitions: List<RouteDefinition>): Promise<any> {
90-
definitions.forEach(
91-
(routeDefinition) => { this.registry.config(this.hostComponent, routeDefinition); });
90+
definitions.forEach((routeDefinition) => {
91+
this.registry.config(this.hostComponent, routeDefinition, this instanceof RootRouter);
92+
});
9293
return this.renavigate();
9394
}
9495

@@ -290,7 +291,7 @@ export class RootRouter extends Router {
290291
super(registry, pipeline, null, hostComponent);
291292
this._location = location;
292293
this._location.subscribe((change) => this.navigate(change['url']));
293-
this.registry.configFromComponent(hostComponent);
294+
this.registry.configFromComponent(hostComponent, true);
294295
this.navigate(location.path());
295296
}
296297

modules/angular2/test/router/path_recognizer_spec.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,21 @@ export function main() {
3939
.toThrowError(`Path "hi//there" contains "//" which is not allowed in a route config.`);
4040
});
4141

42+
describe('querystring params', () => {
43+
it('should parse querystring params so long as the recognizer is a root', () => {
44+
var rec = new PathRecognizer('/hello/there', mockRouteHandler, true);
45+
var params = rec.parseParams('/hello/there?name=igor');
46+
expect(params).toEqual({'name': 'igor'});
47+
});
48+
49+
it('should return a combined map of parameters with the param expected in the URL path',
50+
() => {
51+
var rec = new PathRecognizer('/hello/:name', mockRouteHandler, true);
52+
var params = rec.parseParams('/hello/paul?topic=success');
53+
expect(params).toEqual({'name': 'paul', 'topic': 'success'});
54+
});
55+
});
56+
4257
describe('matrix params', () => {
4358
it('should recognize a trailing matrix value on a path value and assign it to the params return value',
4459
() => {

0 commit comments

Comments
 (0)