From 79266aae69d57d2fe5f9ba112293b9016d637366 Mon Sep 17 00:00:00 2001 From: Nathan Walker Date: Sun, 25 Oct 2015 17:41:21 -0700 Subject: [PATCH] feat(Location): add search() method which returns an Object closes https://github.com/angular/angular/issues/4824 --- modules/angular2/src/core/facade/decode.dart | 11 +++++ modules/angular2/src/core/facade/decode.ts | 10 +++++ modules/angular2/src/mock/location_mock.ts | 2 + .../src/mock/mock_location_strategy.ts | 7 ++- .../src/router/hash_location_strategy.ts | 2 + modules/angular2/src/router/location.ts | 43 +++++++++++++++++++ .../angular2/src/router/location_strategy.ts | 1 + .../src/router/path_location_strategy.ts | 2 + modules/angular2/test/router/location_spec.ts | 19 ++++++++ 9 files changed, 96 insertions(+), 1 deletion(-) create mode 100644 modules/angular2/src/core/facade/decode.dart create mode 100644 modules/angular2/src/core/facade/decode.ts diff --git a/modules/angular2/src/core/facade/decode.dart b/modules/angular2/src/core/facade/decode.dart new file mode 100644 index 000000000000..c7af9cdf8c4c --- /dev/null +++ b/modules/angular2/src/core/facade/decode.dart @@ -0,0 +1,11 @@ +library angular2.src.core.facade.decode; + +import 'dart:core' show String, Uri; + +String tryDecodeURIComponent(String encodedComponent) { + try { + return Uri.decodeComponent(encodedComponent); + } catch(exception, stackTrace) { + // Ignore any invalid uri component + } +} \ No newline at end of file diff --git a/modules/angular2/src/core/facade/decode.ts b/modules/angular2/src/core/facade/decode.ts new file mode 100644 index 000000000000..6af0149f7154 --- /dev/null +++ b/modules/angular2/src/core/facade/decode.ts @@ -0,0 +1,10 @@ +/** + * Tries to decode the URI component without throwing an exception. + */ +export function tryDecodeURIComponent(value: string): string { + try { + return decodeURIComponent(value); + } catch (e) { + // Ignore any invalid uri component + } +} \ No newline at end of file diff --git a/modules/angular2/src/mock/location_mock.ts b/modules/angular2/src/mock/location_mock.ts index 0c70727e46f5..dd525bfc9fe0 100644 --- a/modules/angular2/src/mock/location_mock.ts +++ b/modules/angular2/src/mock/location_mock.ts @@ -24,6 +24,8 @@ export class SpyLocation implements Location { path(): string { return this._path; } + search(): any { return this._query; } + simulateUrlPop(pathname: string) { ObservableWrapper.callEmit(this._subject, {'url': pathname, 'pop': true}); } diff --git a/modules/angular2/src/mock/mock_location_strategy.ts b/modules/angular2/src/mock/mock_location_strategy.ts index de7dcb746d2d..9fe7f4ae3925 100644 --- a/modules/angular2/src/mock/mock_location_strategy.ts +++ b/modules/angular2/src/mock/mock_location_strategy.ts @@ -11,6 +11,7 @@ import {LocationStrategy} from 'angular2/src/router/location_strategy'; export class MockLocationStrategy extends LocationStrategy { internalBaseHref: string = '/'; internalPath: string = '/'; + internalSearch: string = ''; internalTitle: string = ''; urlChanges: string[] = []; /** @internal */ @@ -24,6 +25,8 @@ export class MockLocationStrategy extends LocationStrategy { path(): string { return this.internalPath; } + search(): any { return this.internalSearch; } + prepareExternalUrl(internal: string): string { if (internal.startsWith('/') && this.internalBaseHref.endsWith('/')) { return this.internalBaseHref + internal.substring(1); @@ -34,8 +37,10 @@ export class MockLocationStrategy extends LocationStrategy { pushState(ctx: any, title: string, path: string, query: string): void { this.internalTitle = title; - var url = path + (query.length > 0 ? ('?' + query) : ''); + query = query.length > 0 ? ('?' + query) : ''; + var url = path + query; this.internalPath = url; + this.internalSearch = query; var externalUrl = this.prepareExternalUrl(url); this.urlChanges.push(externalUrl); diff --git a/modules/angular2/src/router/hash_location_strategy.ts b/modules/angular2/src/router/hash_location_strategy.ts index 68d01c4142bb..87be8d97a809 100644 --- a/modules/angular2/src/router/hash_location_strategy.ts +++ b/modules/angular2/src/router/hash_location_strategy.ts @@ -77,6 +77,8 @@ export class HashLocationStrategy extends LocationStrategy { normalizeQueryParams(this._platformLocation.search); } + search(): string { return this._platformLocation.search; } + prepareExternalUrl(internal: string): string { var url = joinWithSlash(this._baseHref, internal); return url.length > 0 ? ('#' + url) : url; diff --git a/modules/angular2/src/router/location.ts b/modules/angular2/src/router/location.ts index 8684a34681d1..61220a5c5d67 100644 --- a/modules/angular2/src/router/location.ts +++ b/modules/angular2/src/router/location.ts @@ -1,6 +1,8 @@ import {LocationStrategy} from './location_strategy'; import {EventEmitter, ObservableWrapper} from 'angular2/src/facade/async'; import {Injectable, Inject} from 'angular2/core'; +import {StringWrapper, RegExpWrapper, isPresent, isArray} from 'angular2/src/facade/lang'; +import {tryDecodeURIComponent} from 'angular2/src/core/facade/decode'; /** * `Location` is a service that applications can use to interact with a browser's URL. @@ -62,6 +64,19 @@ export class Location { */ path(): string { return this.normalize(this.platformStrategy.path()); } + /** + * Returns query params as an object. + */ + search(): any { + // the search value is always prefixed with a `?` + let search = this.platformStrategy.search(); + + // Dart will complain if a call to substring is + // executed with a position value that extends the + // length of string. + return (search.length > 0 ? parseKeyValue(search.substring(1)) : {}); + } + /** * Given a string representing a URL, returns the normalized URL path without leading or * trailing slashes @@ -140,3 +155,31 @@ function stripTrailingSlash(url: string): string { } return url; } + +/** + * Parses an escaped url query string into key-value pairs. + */ +function parseKeyValue(keyValue: string = ""): any { + let obj: any = {}; + let pairs: Array = keyValue.split('&'); + for (let i = 0; i < pairs.length; i++) { + let key: string; + let val: string; + let kv: string = pairs[i]; + let parts: Array; + if (isPresent(kv)) { + key = kv = StringWrapper.replaceAll(kv, /\+/g, '%20'); + parts = kv.split('='); + if (parts.length === 2) { + key = parts[0]; + val = parts[1]; + } + key = tryDecodeURIComponent(key); + if (isPresent(key)) { + val = isPresent(val) ? tryDecodeURIComponent(val) : ''; + obj[key] = val; + } + } + } + return obj; +} diff --git a/modules/angular2/src/router/location_strategy.ts b/modules/angular2/src/router/location_strategy.ts index 186dfa19bb20..e919c2ce430f 100644 --- a/modules/angular2/src/router/location_strategy.ts +++ b/modules/angular2/src/router/location_strategy.ts @@ -20,6 +20,7 @@ import {OpaqueToken} from 'angular2/core'; export abstract class LocationStrategy { abstract path(): string; abstract prepareExternalUrl(internal: string): string; + abstract search(): any; abstract pushState(state: any, title: string, url: string, queryParams: string): void; abstract replaceState(state: any, title: string, url: string, queryParams: string): void; abstract forward(): void; diff --git a/modules/angular2/src/router/path_location_strategy.ts b/modules/angular2/src/router/path_location_strategy.ts index 7dc68b3820f2..3784681b93ab 100644 --- a/modules/angular2/src/router/path_location_strategy.ts +++ b/modules/angular2/src/router/path_location_strategy.ts @@ -88,6 +88,8 @@ export class PathLocationStrategy extends LocationStrategy { return this._platformLocation.pathname + normalizeQueryParams(this._platformLocation.search); } + search(): string { return this._platformLocation.search; } + pushState(state: any, title: string, url: string, queryParams: string) { var externalUrl = this.prepareExternalUrl(url + normalizeQueryParams(queryParams)); this._platformLocation.pushState(state, title, externalUrl); diff --git a/modules/angular2/test/router/location_spec.ts b/modules/angular2/test/router/location_spec.ts index 5bd8e6ba1bc9..a97bacb7f5f8 100644 --- a/modules/angular2/test/router/location_spec.ts +++ b/modules/angular2/test/router/location_spec.ts @@ -84,5 +84,24 @@ export function main() { location.go('/home', "key=value"); expect(location.path()).toEqual("/home?key=value"); }); + + it('should provide query params as an Object', () => { + var locationStrategy = new MockLocationStrategy(); + var location = new Location(locationStrategy); + + location.go('/home', "key=value"); + expect(location.search()['key']).toEqual("value"); + + // should parse various formats cleanly + location.go('/home', "opinion=angular+is+awesome!"); + expect(location.search()['opinion']).toEqual("angular is awesome!"); + + location.go( + '/home', + "opinion=Jeff+Cross is good as a worldwide news+reporter&random=other:formatting"); + expect(location.search()['opinion']) + .toEqual("Jeff Cross is good as a worldwide news reporter"); + expect(location.search()['random']).toEqual("other:formatting"); + }); }); }