Skip to content

Commit

Permalink
feat(router): use querystring params for top-level routes
Browse files Browse the repository at this point in the history
Closes #3017
  • Loading branch information
matsko committed Jul 22, 2015
1 parent a9e7c90 commit fdffcab
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 44 deletions.
19 changes: 19 additions & 0 deletions modules/angular2/src/router/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import {isPresent} from 'angular2/src/facade/lang';

export function parseAndAssignParamString(splitToken: string, paramString: string,
keyValueMap: StringMap<string, string>): void {
var first = paramString[0];
if (first == '?' || first == ';') {
paramString = paramString.substring(1);
}

paramString.split(splitToken)
.forEach((entry) => {
var tuple = entry.split('=');
var key = tuple[0];
if (!isPresent(keyValueMap[key])) {
var value = tuple.length > 1 ? tuple[1] : true;
keyValueMap[key] = value;
}
});
}
5 changes: 2 additions & 3 deletions modules/angular2/src/router/instruction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,9 @@ export class Instruction {
reuse: boolean = false;
specificity: number;

private _params: StringMap<string, string>;

constructor(public component: any, public capturedUrl: string,
private _recognizer: PathRecognizer, public child: Instruction = null) {
private _recognizer: PathRecognizer, public child: Instruction = null,
private _params: StringMap<string, any> = null) {
this.accumulatedUrl = capturedUrl;
this.specificity = _recognizer.specificity;
if (isPresent(child)) {
Expand Down
61 changes: 33 additions & 28 deletions modules/angular2/src/router/path_recognizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
ListWrapper
} from 'angular2/src/facade/collection';
import {IMPLEMENTS} from 'angular2/src/facade/lang';

import {parseAndAssignParamString} from 'angular2/src/router/helpers';
import {escapeRegex} from './url';
import {RouteHandler} from './route_handler';

Expand Down Expand Up @@ -63,19 +63,6 @@ function normalizeString(obj: any): string {
}
}

function parseAndAssignMatrixParams(keyValueMap, matrixString) {
if (matrixString[0] == ';') {
matrixString = matrixString.substring(1);
}

matrixString.split(';').forEach((entry) => {
var tuple = entry.split('=');
var key = tuple[0];
var value = tuple.length > 1 ? tuple[1] : true;
keyValueMap[key] = value;
});
}

class ContinuationSegment extends Segment {}

class StaticSegment extends Segment {
Expand Down Expand Up @@ -198,7 +185,10 @@ export class PathRecognizer {
specificity: number;
terminal: boolean = true;

constructor(public path: string, public handler: RouteHandler) {
static matrixRegex: RegExp = RegExpWrapper.create('^(.*\/[^\/]+?)(;[^\/]+)?\/?$');
static queryRegex: RegExp = RegExpWrapper.create('^(.*\/[^\/]+?)(\\?[^\/]+)?$');

constructor(public path: string, public handler: RouteHandler, public isRoot: boolean = false) {
assertPath(path);
var parsed = parsePathString(path);
var specificity = parsed['specificity'];
Expand Down Expand Up @@ -228,16 +218,16 @@ export class PathRecognizer {
var containsStarSegment =
segmentsLimit >= 0 && this.segments[segmentsLimit] instanceof StarSegment;

var matrixString;
var paramsString, useQueryString = this.isRoot && this.terminal;
if (!containsStarSegment) {
var matches =
RegExpWrapper.firstMatch(RegExpWrapper.create('^(.*\/[^\/]+?)(;[^\/]+)?\/?$'), url);
var matches = RegExpWrapper.firstMatch(
useQueryString ? PathRecognizer.queryRegex : PathRecognizer.matrixRegex, url);
if (isPresent(matches)) {
url = matches[1];
matrixString = matches[2];
paramsString = matches[2];
}

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

var params = StringMapWrapper.create();
Expand All @@ -256,8 +246,11 @@ export class PathRecognizer {
}
}

if (isPresent(matrixString) && matrixString.length > 0 && matrixString[0] == ';') {
parseAndAssignMatrixParams(params, matrixString);
if (isPresent(paramsString) && paramsString.length > 0) {
var expectedStartingValue = useQueryString ? '?' : ';';
if (paramsString[0] == expectedStartingValue) {
parseAndAssignParamString(expectedStartingValue, paramsString, params);
}
}

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

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

var unusedParams = paramTokens.getUnused();
StringMapWrapper.forEach(unusedParams, (value, key) => {
url += ';' + key;
if (isPresent(value)) {
url += '=' + value;
}
});
if (!StringMapWrapper.isEmpty(unusedParams)) {
url += useQueryString ? '?' : ';';
var paramToken = useQueryString ? '&' : ';';
var i = 0;
StringMapWrapper.forEach(unusedParams, (value, key) => {
if (i++ > 0) {
url += paramToken;
}
url += key;
if (!isPresent(value) && useQueryString) {
value = 'true';
}
if (isPresent(value)) {
url += '=' + value;
}
});
}

if (applyLeadingSlash) {
url += '/';
Expand Down
39 changes: 35 additions & 4 deletions modules/angular2/src/router/route_recognizer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {RouteHandler} from './route_handler';
import {Route, AsyncRoute, Redirect, RouteDefinition} from './route_config_impl';
import {AsyncRouteHandler} from './async_route_handler';
import {SyncRouteHandler} from './sync_route_handler';
import {parseAndAssignParamString} from 'angular2/src/router/helpers';

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

constructor(public isRoot: boolean = false) {}

config(config: RouteDefinition): boolean {
var handler;
if (config instanceof Redirect) {
Expand All @@ -44,7 +47,7 @@ export class RouteRecognizer {
} else if (config instanceof AsyncRoute) {
handler = new AsyncRouteHandler(config.loader);
}
var recognizer = new PathRecognizer(config.path, handler);
var recognizer = new PathRecognizer(config.path, handler, this.isRoot);
MapWrapper.forEach(this.matchers, (matcher, _) => {
if (recognizer.regex.toString() == matcher.regex.toString()) {
throw new BaseException(
Expand Down Expand Up @@ -80,6 +83,17 @@ export class RouteRecognizer {
}
});

var queryParams = StringMapWrapper.create();
var queryString = '';
var queryIndex = url.indexOf('?');
if (queryIndex >= 0) {
queryString = url.substring(queryIndex + 1);
url = url.substring(0, queryIndex);
}
if (this.isRoot && queryString.length > 0) {
parseAndAssignParamString('&', queryString, queryParams);
}

MapWrapper.forEach(this.matchers, (pathRecognizer, regex) => {
var match;
if (isPresent(match = RegExpWrapper.firstMatch(regex, url))) {
Expand All @@ -89,7 +103,12 @@ export class RouteRecognizer {
matchedUrl = match[0];
unmatchedUrl = url.substring(match[0].length);
}
solutions.push(new RouteMatch(pathRecognizer, matchedUrl, unmatchedUrl));
var params = null;
if (pathRecognizer.terminal && !StringMapWrapper.isEmpty(queryParams)) {
params = queryParams;
matchedUrl += '?' + queryString;
}
solutions.push(new RouteMatch(pathRecognizer, matchedUrl, unmatchedUrl, params));
}
});

Expand All @@ -109,10 +128,22 @@ export class RouteRecognizer {
}

export class RouteMatch {
private _params: StringMap<string, any>;
private _paramsParsed: boolean = false;

constructor(public recognizer: PathRecognizer, public matchedUrl: string,
public unmatchedUrl: string) {}
public unmatchedUrl: string, p: StringMap<string, any> = null) {
this._params = isPresent(p) ? p : StringMapWrapper.create();
}

params(): StringMap<string, string> { return this.recognizer.parseParams(this.matchedUrl); }
params(): StringMap<string, any> {
if (!this._paramsParsed) {
this._paramsParsed = true;
StringMapWrapper.forEach(this.recognizer.parseParams(this.matchedUrl),
(value, key) => { StringMapWrapper.set(this._params, key, value); });
}
return this._params;
}
}

function configObjToHandler(config: any): RouteHandler {
Expand Down
12 changes: 7 additions & 5 deletions modules/angular2/src/router/route_registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,13 @@ export class RouteRegistry {
/**
* Given a component and a configuration object, add the route to this registry
*/
config(parentComponent: any, config: RouteDefinition): void {
config(parentComponent: any, config: RouteDefinition, isRootLevelRoute: boolean = false): void {
config = normalizeRouteConfig(config);

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

if (isBlank(recognizer)) {
recognizer = new RouteRecognizer();
recognizer = new RouteRecognizer(isRootLevelRoute);
this._rules.set(parentComponent, recognizer);
}

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

if (annotation instanceof RouteConfig) {
ListWrapper.forEach(annotation.configs, (config) => this.config(component, config));
ListWrapper.forEach(annotation.configs,
(config) => this.config(component, config, isRootComponent));
}
}
}
Expand Down Expand Up @@ -120,7 +121,8 @@ export class RouteRegistry {

if (partialMatch.unmatchedUrl.length == 0) {
if (recognizer.terminal) {
return new Instruction(componentType, partialMatch.matchedUrl, recognizer);
return new Instruction(componentType, partialMatch.matchedUrl, recognizer, null,
partialMatch.params());
} else {
return null;
}
Expand Down
7 changes: 4 additions & 3 deletions modules/angular2/src/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ export class Router {
* ```
*/
config(definitions: List<RouteDefinition>): Promise<any> {
definitions.forEach(
(routeDefinition) => { this.registry.config(this.hostComponent, routeDefinition); });
definitions.forEach((routeDefinition) => {
this.registry.config(this.hostComponent, routeDefinition, this instanceof RootRouter);
});
return this.renavigate();
}

Expand Down Expand Up @@ -290,7 +291,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, true);
this.navigate(location.path());
}

Expand Down
15 changes: 15 additions & 0 deletions modules/angular2/test/router/path_recognizer_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ export function main() {
.toThrowError(`Path "hi//there" contains "//" which is not allowed in a route config.`);
});

describe('querystring params', () => {
it('should parse querystring params so long as the recognizer is a root', () => {
var rec = new PathRecognizer('/hello/there', mockRouteHandler, true);
var params = rec.parseParams('/hello/there?name=igor');
expect(params).toEqual({'name': 'igor'});
});

it('should return a combined map of parameters with the param expected in the URL path',
() => {
var rec = new PathRecognizer('/hello/:name', mockRouteHandler, true);
var params = rec.parseParams('/hello/paul?topic=success');
expect(params).toEqual({'name': 'paul', 'topic': 'success'});
});
});

describe('matrix params', () => {
it('should recognize a trailing matrix value on a path value and assign it to the params return value',
() => {
Expand Down
Loading

0 comments on commit fdffcab

Please sign in to comment.