Skip to content

Commit 5677bf7

Browse files
committed
feat(router): introduce matrix params
Closes angular#2774 Closes angular#2989
1 parent 97ef1c2 commit 5677bf7

File tree

4 files changed

+270
-14
lines changed

4 files changed

+270
-14
lines changed

modules/angular2/src/router/path_recognizer.ts

Lines changed: 103 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,33 @@ import {RouteHandler} from './route_handler';
2626
export class Segment {
2727
name: string;
2828
regex: string;
29+
generate(params: TouchMap): string { return ''; }
30+
}
31+
32+
class TouchMap {
33+
map: StringMap<string, string> = StringMapWrapper.create();
34+
keys: StringMap<string, boolean> = StringMapWrapper.create();
35+
36+
constructor(map: StringMap<string, any>) {
37+
if (isPresent(map)) {
38+
StringMapWrapper.forEach(map, (value, key) => {
39+
this.map[key] = isPresent(value) ? value.toString() : null;
40+
this.keys[key] = true;
41+
});
42+
}
43+
}
44+
45+
get(key: string): string {
46+
StringMapWrapper.delete(this.keys, key);
47+
return this.map[key];
48+
}
49+
50+
getUnused(): StringMap<string, any> {
51+
var unused: StringMap<string, any> = StringMapWrapper.create();
52+
var keys = StringMapWrapper.keys(this.keys);
53+
ListWrapper.forEach(keys, (key) => { unused[key] = StringMapWrapper.get(this.map, key); });
54+
return unused;
55+
}
2956
}
3057

3158
function normalizeString(obj: any): string {
@@ -36,20 +63,36 @@ function normalizeString(obj: any): string {
3663
}
3764
}
3865

39-
class ContinuationSegment extends Segment {
40-
generate(params): string { return ''; }
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+
});
4177
}
4278

79+
class ContinuationSegment extends Segment {}
80+
4381
class StaticSegment extends Segment {
4482
regex: string;
4583
name: string = '';
4684

4785
constructor(public string: string) {
4886
super();
4987
this.regex = escapeRegex(string);
88+
89+
// we add this property so that the route matcher still sees
90+
// this segment as a valid path even if do not use the matrix
91+
// parameters
92+
this.regex += '(;[^\/]+)?';
5093
}
5194

52-
generate(params): string { return this.string; }
95+
generate(params: TouchMap): string { return this.string; }
5396
}
5497

5598
@IMPLEMENTS(Segment)
@@ -58,23 +101,22 @@ class DynamicSegment {
58101

59102
constructor(public name: string) {}
60103

61-
generate(params: StringMap<string, string>): string {
62-
if (!StringMapWrapper.contains(params, this.name)) {
104+
generate(params: TouchMap): string {
105+
if (!StringMapWrapper.contains(params.map, this.name)) {
63106
throw new BaseException(
64107
`Route generator for '${this.name}' was not included in parameters passed.`);
65108
}
66-
return normalizeString(StringMapWrapper.get(params, this.name));
109+
return normalizeString(params.get(this.name));
67110
}
68111
}
69112

70113

71114
class StarSegment {
72115
regex: string = "(.+)";
116+
73117
constructor(public name: string) {}
74118

75-
generate(params: StringMap<string, string>): string {
76-
return normalizeString(StringMapWrapper.get(params, this.name));
77-
}
119+
generate(params: TouchMap): string { return normalizeString(params.get(this.name)); }
78120
}
79121

80122

@@ -168,9 +210,27 @@ export class PathRecognizer {
168210
}
169211

170212
parseParams(url: string): StringMap<string, string> {
213+
// the last segment is always the star one since it's terminal
214+
var segmentsLimit = this.segments.length - 1;
215+
var containsStarSegment =
216+
segmentsLimit >= 0 && this.segments[segmentsLimit] instanceof StarSegment;
217+
218+
var matrixString;
219+
if (!containsStarSegment) {
220+
var matches =
221+
RegExpWrapper.firstMatch(RegExpWrapper.create('^(.*\/[^\/]+?)(;[^\/]+)?\/?$'), url);
222+
if (isPresent(matches)) {
223+
url = matches[1];
224+
matrixString = matches[2];
225+
}
226+
227+
url = StringWrapper.replaceAll(url, /(;[^\/]+)(?=(\/|\Z))/g, '');
228+
}
229+
171230
var params = StringMapWrapper.create();
172231
var urlPart = url;
173-
for (var i = 0; i < this.segments.length; i++) {
232+
233+
for (var i = 0; i <= segmentsLimit; i++) {
174234
var segment = this.segments[i];
175235
if (segment instanceof ContinuationSegment) {
176236
continue;
@@ -179,16 +239,45 @@ export class PathRecognizer {
179239
var match = RegExpWrapper.firstMatch(RegExpWrapper.create('/' + segment.regex), urlPart);
180240
urlPart = StringWrapper.substring(urlPart, match[0].length);
181241
if (segment.name.length > 0) {
182-
StringMapWrapper.set(params, segment.name, match[1]);
242+
params[segment.name] = match[1];
183243
}
184244
}
185245

246+
if (isPresent(matrixString) && matrixString.length > 0 && matrixString[0] == ';') {
247+
parseAndAssignMatrixParams(params, matrixString);
248+
}
249+
186250
return params;
187251
}
188252

189-
generate(params: StringMap<string, string>): string {
190-
return ListWrapper.join(ListWrapper.map(this.segments, (segment) => segment.generate(params)),
191-
'/');
253+
generate(params: StringMap<string, any>): string {
254+
var paramTokens = new TouchMap(params);
255+
var applyLeadingSlash = false;
256+
257+
var url = '';
258+
for (var i = 0; i < this.segments.length; i++) {
259+
let segment = this.segments[i];
260+
let s = segment.generate(paramTokens);
261+
applyLeadingSlash = applyLeadingSlash || (segment instanceof ContinuationSegment);
262+
263+
if (s.length > 0) {
264+
url += (i > 0 ? '/' : '') + s;
265+
}
266+
}
267+
268+
var unusedParams = paramTokens.getUnused();
269+
StringMapWrapper.forEach(unusedParams, (value, key) => {
270+
url += ';' + key;
271+
if (isPresent(value)) {
272+
url += '=' + value;
273+
}
274+
});
275+
276+
if (applyLeadingSlash) {
277+
url += '/';
278+
}
279+
280+
return url;
192281
}
193282

194283
resolveComponentType(): Promise<any> { return this.handler.resolveComponentType(); }
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
import {
2+
AsyncTestCompleter,
3+
describe,
4+
it,
5+
iit,
6+
ddescribe,
7+
expect,
8+
inject,
9+
beforeEach,
10+
SpyObject
11+
} from 'angular2/test_lib';
12+
13+
import {PathRecognizer} from 'angular2/src/router/path_recognizer';
14+
import {SyncRouteHandler} from 'angular2/src/router/sync_route_handler';
15+
16+
class DummyClass {
17+
constructor() {}
18+
}
19+
20+
var mockRouteHandler = new SyncRouteHandler(DummyClass);
21+
22+
export function main() {
23+
describe('PathRecognizer', () => {
24+
describe('matrix params', () => {
25+
it('should recognize a trailing matrix value on a path value and assign it to the params return value',
26+
() => {
27+
var rec = new PathRecognizer('/hello/:id', mockRouteHandler);
28+
var params = rec.parseParams('/hello/matias;key=value');
29+
30+
expect(params['id']).toEqual('matias');
31+
expect(params['key']).toEqual('value');
32+
});
33+
34+
it('should recognize and parse multiple matrix params separated by a colon value', () => {
35+
var rec = new PathRecognizer('/jello/:sid', mockRouteHandler);
36+
var params = rec.parseParams('/jello/man;color=red;height=20');
37+
38+
expect(params['sid']).toEqual('man');
39+
expect(params['color']).toEqual('red');
40+
expect(params['height']).toEqual('20');
41+
});
42+
43+
it('should recognize a matrix param value on a static path value', () => {
44+
var rec = new PathRecognizer('/static/man', mockRouteHandler);
45+
var params = rec.parseParams('/static/man;name=dave');
46+
expect(params['name']).toEqual('dave');
47+
});
48+
49+
it('should not parse matrix params when a wildcard segment is used', () => {
50+
var rec = new PathRecognizer('/wild/*everything', mockRouteHandler);
51+
var params = rec.parseParams('/wild/super;variable=value');
52+
expect(params['everything']).toEqual('super;variable=value');
53+
});
54+
55+
it('should set matrix param values to true when no value is present within the path string',
56+
() => {
57+
var rec = new PathRecognizer('/path', mockRouteHandler);
58+
var params = rec.parseParams('/path;one;two;three=3');
59+
expect(params['one']).toEqual(true);
60+
expect(params['two']).toEqual(true);
61+
expect(params['three']).toEqual('3');
62+
});
63+
64+
it('should ignore earlier instances of matrix params and only consider the ones at the end of the path',
65+
() => {
66+
var rec = new PathRecognizer('/one/two/three', mockRouteHandler);
67+
var params = rec.parseParams('/one;a=1/two;b=2/three;c=3');
68+
expect(params).toEqual({'c': '3'});
69+
});
70+
});
71+
});
72+
}

modules/angular2/test/router/route_recognizer_spec.ts

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
SpyObject
1111
} from 'angular2/test_lib';
1212

13+
import {Map, StringMap, StringMapWrapper} from 'angular2/src/facade/collection';
14+
1315
import {RouteRecognizer, RouteMatch} from 'angular2/src/router/route_recognizer';
1416

1517
export function main() {
@@ -122,6 +124,79 @@ export function main() {
122124
expect(() => recognizer.generate('user', {})['url'])
123125
.toThrowError('Route generator for \'name\' was not included in parameters passed.');
124126
});
127+
128+
describe('matrix params', () => {
129+
it('should recognize matrix parameters within the URL path', () => {
130+
var recognizer = new RouteRecognizer();
131+
recognizer.addConfig('profile/:name', handler, 'user');
132+
133+
var solution = recognizer.recognize('/profile/matsko;comments=all')[0];
134+
var params = solution.params();
135+
expect(params['name']).toEqual('matsko');
136+
expect(params['comments']).toEqual('all');
137+
});
138+
139+
it('should recognize multiple matrix params and set parameters that contain no value to true',
140+
() => {
141+
var recognizer = new RouteRecognizer();
142+
recognizer.addConfig('/profile/hello', handler, 'user');
143+
144+
var solution =
145+
recognizer.recognize('/profile/hello;modal;showAll=true;hideAll=false')[0];
146+
var params = solution.params();
147+
148+
expect(params['modal']).toEqual(true);
149+
expect(params['showAll']).toEqual('true');
150+
expect(params['hideAll']).toEqual('false');
151+
});
152+
153+
it('should only consider the matrix parameters at the end of the path handler', () => {
154+
var recognizer = new RouteRecognizer();
155+
recognizer.addConfig('/profile/hi/:name', handler, 'user');
156+
157+
var solution = recognizer.recognize('/profile;a=1/hi;b=2;c=3/william;d=4')[0];
158+
var params = solution.params();
159+
160+
expect(params).toEqual({'name': 'william', 'd': '4'});
161+
});
162+
163+
it('should generate and populate the given static-based route with matrix params', () => {
164+
var recognizer = new RouteRecognizer();
165+
recognizer.addConfig('forum/featured', handler, 'forum-page');
166+
167+
var params = StringMapWrapper.create();
168+
params['start'] = 10;
169+
params['end'] = 100;
170+
171+
var result = recognizer.generate('forum-page', params);
172+
expect(result['url']).toEqual('forum/featured;start=10;end=100');
173+
});
174+
175+
it('should generate and populate the given dynamic-based route with matrix params', () => {
176+
var recognizer = new RouteRecognizer();
177+
recognizer.addConfig('forum/:topic', handler, 'forum-page');
178+
179+
var params = StringMapWrapper.create();
180+
params['topic'] = 'crazy';
181+
params['total-posts'] = 100;
182+
params['moreDetail'] = null;
183+
184+
var result = recognizer.generate('forum-page', params);
185+
expect(result['url']).toEqual('forum/crazy;total-posts=100;moreDetail');
186+
});
187+
188+
it('should not apply any matrix params if a dynamic route segment takes up the slot when a path is generated',
189+
() => {
190+
var recognizer = new RouteRecognizer();
191+
recognizer.addConfig('hello/:name', handler, 'profile-page');
192+
193+
var params = StringMapWrapper.create();
194+
params['name'] = 'matsko';
195+
196+
var result = recognizer.generate('profile-page', params);
197+
expect(result['url']).toEqual('hello/matsko');
198+
});
199+
});
125200
});
126201
}
127202

modules/angular2/test/router/router_spec.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,26 @@ export function main() {
115115
expect(router.generate(['/firstCmp', 'secondCmp'])).toEqual('/first/second');
116116
expect(router.generate(['/firstCmp/secondCmp'])).toEqual('/first/second');
117117
});
118+
119+
describe('matrix params', () => {
120+
it('should apply inline matrix params for each router path within the generated URL', () => {
121+
router.config({'path': '/first/...', 'component': DummyParentComp, 'as': 'firstCmp'});
122+
123+
var path =
124+
router.generate(['/firstCmp', {'key': 'value'}, 'secondCmp', {'project': 'angular'}]);
125+
expect(path).toEqual('/first;key=value/second;project=angular');
126+
});
127+
128+
it('should apply inline matrix params for each router path within the generated URL and also include named params',
129+
() => {
130+
router.config(
131+
{'path': '/first/:token/...', 'component': DummyParentComp, 'as': 'firstCmp'});
132+
133+
var path =
134+
router.generate(['/firstCmp', {'token': 'min'}, 'secondCmp', {'author': 'max'}]);
135+
expect(path).toEqual('/first/min/second;author=max');
136+
});
137+
});
118138
});
119139
}
120140

0 commit comments

Comments
 (0)