fix(router): properly read and serialize query params

This splits out `path` and `query` into separate params for `location.go`
and related methods so that we can handle them properly in both `PathLocationStrategy`
and `HashLocationStrategy`.

This handles the problem of not reading query params to populate `Location` on the
initial page load.

Closes #3957
Closes #4225
Closes #3784
This commit is contained in:
Brian Ford 2015-09-23 00:10:26 -07:00
parent 440fd11c72
commit 8bc40d3f4d
9 changed files with 60 additions and 27 deletions

View File

@ -7,6 +7,8 @@ export class SpyLocation implements Location {
/** @internal */ /** @internal */
_path: string = ''; _path: string = '';
/** @internal */ /** @internal */
_query: string = '';
/** @internal */
_subject: EventEmitter = new EventEmitter(); _subject: EventEmitter = new EventEmitter();
/** @internal */ /** @internal */
_baseHref: string = ''; _baseHref: string = '';
@ -21,12 +23,15 @@ export class SpyLocation implements Location {
normalizeAbsolutely(url: string): string { return this._baseHref + url; } normalizeAbsolutely(url: string): string { return this._baseHref + url; }
go(url: string) { go(path: string, query: string = '') {
url = this.normalizeAbsolutely(url); path = this.normalizeAbsolutely(path);
if (this._path == url) { if (this._path == path && this._query == query) {
return; return;
} }
this._path = url; this._path = path;
this._query = query;
var url = path + (query.length > 0 ? ('?' + query) : '');
this.urlChanges.push(url); this.urlChanges.push(url);
} }

View File

@ -22,8 +22,10 @@ export class MockLocationStrategy extends LocationStrategy {
ObservableWrapper.callNext(this._subject, {'url': pathname}); ObservableWrapper.callNext(this._subject, {'url': pathname});
} }
pushState(ctx: any, title: string, url: string): void { pushState(ctx: any, title: string, path: string, query: string): void {
this.internalTitle = title; this.internalTitle = title;
var url = path + (query.length > 0 ? ('?' + query) : '');
this.internalPath = url; this.internalPath = url;
this.urlChanges.push(url); this.urlChanges.push(url);
} }

View File

@ -1,6 +1,6 @@
import {DOM} from 'angular2/src/core/dom/dom_adapter'; import {DOM} from 'angular2/src/core/dom/dom_adapter';
import {Injectable} from 'angular2/angular2'; import {Injectable} from 'angular2/angular2';
import {LocationStrategy} from './location_strategy'; import {LocationStrategy, normalizeQueryParams} from './location_strategy';
import {EventListener, History, Location} from 'angular2/src/core/facade/browser'; import {EventListener, History, Location} from 'angular2/src/core/facade/browser';
/** /**
@ -61,11 +61,18 @@ export class HashLocationStrategy extends LocationStrategy {
// Dart will complain if a call to substring is // Dart will complain if a call to substring is
// executed with a position value that extends the // executed with a position value that extends the
// length of string. // length of string.
return path.length > 0 ? path.substring(1) : path; return (path.length > 0 ? path.substring(1) : path) +
normalizeQueryParams(this._location.search);
} }
pushState(state: any, title: string, url: string) { pushState(state: any, title: string, path: string, queryParams: string) {
this._history.pushState(state, title, '#' + url); var url = path + normalizeQueryParams(queryParams);
if (url.length == 0) {
url = this._location.pathname;
} else {
url = '#' + url;
}
this._history.pushState(state, title, url);
} }
forward(): void { this._history.forward(); } forward(): void { this._history.forward(); }

View File

@ -94,12 +94,18 @@ export class PrimaryInstruction {
} }
export function stringifyInstruction(instruction: Instruction): string { export function stringifyInstruction(instruction: Instruction): string {
var params = instruction.component.urlParams.length > 0 ? return stringifyInstructionPath(instruction) + stringifyInstructionQuery(instruction);
}
export function stringifyInstructionPath(instruction: Instruction): string {
return instruction.component.urlPath + stringifyAux(instruction) +
stringifyPrimary(instruction.child);
}
export function stringifyInstructionQuery(instruction: Instruction): string {
return instruction.component.urlParams.length > 0 ?
('?' + instruction.component.urlParams.join('&')) : ('?' + instruction.component.urlParams.join('&')) :
''; '';
return instruction.component.urlPath + stringifyAux(instruction) +
stringifyPrimary(instruction.child) + params;
} }
function stringifyPrimary(instruction: Instruction): string { function stringifyPrimary(instruction: Instruction): string {

View File

@ -125,9 +125,9 @@ export class Location {
* Changes the browsers URL to the normalized version of the given URL, and pushes a * Changes the browsers URL to the normalized version of the given URL, and pushes a
* new item onto the platform's history. * new item onto the platform's history.
*/ */
go(url: string): void { go(path: string, query: string = ''): void {
var finalUrl = this.normalizeAbsolutely(url); var absolutePath = this.normalizeAbsolutely(path);
this.platformStrategy.pushState(null, '', finalUrl); this.platformStrategy.pushState(null, '', absolutePath, query);
} }
/** /**

View File

@ -16,9 +16,13 @@
*/ */
export abstract class LocationStrategy { export abstract class LocationStrategy {
abstract path(): string; abstract path(): string;
abstract pushState(ctx: any, title: string, url: string): void; abstract pushState(state: any, title: string, url: string, queryParams: string): void;
abstract forward(): void; abstract forward(): void;
abstract back(): void; abstract back(): void;
abstract onPopState(fn: (_: any) => any): void; abstract onPopState(fn: (_: any) => any): void;
abstract getBaseHref(): string; abstract getBaseHref(): string;
} }
export function normalizeQueryParams(params: string): string {
return (params.length > 0 && params.substring(0, 1) != '?') ? ('?' + params) : params;
}

View File

@ -1,7 +1,7 @@
import {DOM} from 'angular2/src/core/dom/dom_adapter'; import {DOM} from 'angular2/src/core/dom/dom_adapter';
import {Injectable} from 'angular2/angular2'; import {Injectable} from 'angular2/angular2';
import {EventListener, History, Location} from 'angular2/src/core/facade/browser'; import {EventListener, History, Location} from 'angular2/src/core/facade/browser';
import {LocationStrategy} from './location_strategy'; import {LocationStrategy, normalizeQueryParams} from './location_strategy';
/** /**
* `PathLocationStrategy` is a {@link LocationStrategy} used to configure the * `PathLocationStrategy` is a {@link LocationStrategy} used to configure the
@ -67,9 +67,11 @@ export class PathLocationStrategy extends LocationStrategy {
getBaseHref(): string { return this._baseHref; } getBaseHref(): string { return this._baseHref; }
path(): string { return this._location.pathname; } path(): string { return this._location.pathname + normalizeQueryParams(this._location.search); }
pushState(state: any, title: string, url: string) { this._history.pushState(state, title, url); } pushState(state: any, title: string, url: string, queryParams: string) {
this._history.pushState(state, title, (url + normalizeQueryParams(queryParams)));
}
forward(): void { this._history.forward(); } forward(): void { this._history.forward(); }

View File

@ -15,7 +15,13 @@ import {
} from 'angular2/src/core/facade/lang'; } from 'angular2/src/core/facade/lang';
import {BaseException, WrappedException} from 'angular2/src/core/facade/exceptions'; import {BaseException, WrappedException} from 'angular2/src/core/facade/exceptions';
import {RouteRegistry} from './route_registry'; import {RouteRegistry} from './route_registry';
import {ComponentInstruction, Instruction, stringifyInstruction} from './instruction'; import {
ComponentInstruction,
Instruction,
stringifyInstruction,
stringifyInstructionPath,
stringifyInstructionQuery
} from './instruction';
import {RouterOutlet} from './router_outlet'; import {RouterOutlet} from './router_outlet';
import {Location} from './location'; import {Location} from './location';
import {getCanActivateHook} from './route_lifecycle_reflector'; import {getCanActivateHook} from './route_lifecycle_reflector';
@ -472,13 +478,14 @@ export class RootRouter extends Router {
} }
commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> { commit(instruction: Instruction, _skipLocationChange: boolean = false): Promise<any> {
var emitUrl = stringifyInstruction(instruction); var emitPath = stringifyInstructionPath(instruction);
if (emitUrl.length > 0) { var emitQuery = stringifyInstructionQuery(instruction);
emitUrl = '/' + emitUrl; if (emitPath.length > 0) {
emitPath = '/' + emitPath;
} }
var promise = super.commit(instruction); var promise = super.commit(instruction);
if (!_skipLocationChange) { if (!_skipLocationChange) {
promise = promise.then((_) => { this._location.go(emitUrl); }); promise = promise.then((_) => { this._location.go(emitPath, emitQuery); });
} }
return promise; return promise;
} }

View File

@ -70,10 +70,10 @@ export function pathSegmentsToUrl(pathSegments: string[]): Url {
return url; return url;
} }
var SEGMENT_RE = RegExpWrapper.create('^[^\\/\\(\\)\\?;=&]+'); var SEGMENT_RE = RegExpWrapper.create('^[^\\/\\(\\)\\?;=&#]+');
function matchUrlSegment(str: string): string { function matchUrlSegment(str: string): string {
var match = RegExpWrapper.firstMatch(SEGMENT_RE, str); var match = RegExpWrapper.firstMatch(SEGMENT_RE, str);
return isPresent(match) ? match[0] : null; return isPresent(match) ? match[0] : '';
} }
export class UrlParser { export class UrlParser {