Skip to content

Commit 4c39dcb

Browse files
BREAKING CHANGE: Replace LocationServices.setUrl with LocationServices.url
This makes `url()` a getter/setter. It also adds the optional `state` parameter to pass through to the browser history when using pushstate. End users should not notice this change, but plugin authors may.
1 parent a7d5fcb commit 4c39dcb

12 files changed

+118
-111
lines changed

src/common/coreservices.ts

+19-3
Original file line numberDiff line numberDiff line change
@@ -49,19 +49,35 @@ export interface CoreServices {
4949

5050
export interface LocationServices extends Disposable {
5151
/**
52-
* Changes the url
52+
* Gets the current url string
53+
*
54+
*
55+
* #### Example:
56+
* ```js
57+
* locationServices.url(); // "/some/path?query=value#anchor"
58+
* ```
59+
*
60+
* @returns the current value of the url, as a string.
61+
*/
62+
url(): string;
63+
64+
/**
65+
* Updates the url, or gets the current url
5366
*
5467
* Updates the url, changing it to the value in `newurl`
5568
*
5669
* #### Example:
5770
* ```js
58-
* locationServices.setUrl("/some/path?query=value#anchor", true);
71+
* locationServices.url("/some/path?query=value#anchor", true);
5972
* ```
6073
*
6174
* @param newurl The new value for the URL
6275
* @param replace When true, replaces the current history entry (instead of appending it) with this new url
76+
* @param state The history's state object, i.e., pushState (if the LocationServices implementation supports it)
77+
* @return the url (after potentially being processed)
6378
*/
64-
setUrl(newurl: string, replace?: boolean): void;
79+
url(newurl: string, replace?: boolean, state?: any): string;
80+
6581
/**
6682
* Gets the path portion of the current url
6783
*

src/url/urlRouter.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ export class UrlRouter implements Disposable {
8181
if (!match) return false;
8282

8383
let result = rule.handler(match, path, search, hash);
84-
if (isString(result)) $url.setUrl(result, true);
84+
if (isString(result)) $url.url(result, true);
8585
return true;
8686
}
8787

@@ -118,7 +118,7 @@ export class UrlRouter implements Disposable {
118118
}
119119
if ($url.path() === this.location) return;
120120

121-
$url.setUrl(this.location, true);
121+
$url.url(this.location, true);
122122
}
123123

124124
/**
@@ -133,7 +133,7 @@ export class UrlRouter implements Disposable {
133133
*/
134134
push(urlMatcher: UrlMatcher, params?: RawParams, options?: { replace?: (string|boolean) }) {
135135
let replace = options && !!options.replace;
136-
this._router.urlService.setUrl(urlMatcher.format(params || {}), replace);
136+
this._router.urlService.url(urlMatcher.format(params || {}), replace);
137137
}
138138

139139
/**

src/url/urlService.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ const makeStub = (keys: string[]): any =>
1010
keys.reduce((acc, key) => (acc[key] = notImplemented(key), acc), { dispose: noop });
1111

1212
/** @hidden */
13-
const locationServicesFns = ["setUrl", "path", "search", "hash", "onChange"];
13+
const locationServicesFns = ["url", "path", "search", "hash", "onChange"];
1414
/** @hidden */
1515
const locationConfigFns = ["port", "protocol", "host", "baseHref", "html5Mode", "hashPrefix"];
1616

@@ -31,7 +31,10 @@ export class UrlService implements LocationServices {
3131
static locationConfigStub: LocationConfig = makeStub(locationConfigFns);
3232

3333
/** @inheritdoc */
34-
setUrl(newurl: string, replace?: boolean): void { return };
34+
url(): string;
35+
/** @inheritdoc */
36+
url(newurl: string, replace?: boolean, state?): void;
37+
url(newurl?, replace?, state?): any { return };
3538
/** @inheritdoc */
3639
path(): string { return };
3740
/** @inheritdoc */

src/vanilla/hashLocation.ts

+7-16
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/ /** */
55
import { isDefined } from "../common/index";
66
import { LocationServices } from "../common/coreservices";
7-
import { splitHash, splitQuery, trimHashVal, getParams, locationPluginFactory } from "./utils";
7+
import { splitHash, splitQuery, trimHashVal, getParams, locationPluginFactory, buildUrl } from "./utils";
88
import { UIRouter } from "../router";
99
import { LocationPlugin } from "./interface";
1010
import { pushTo, deregAll } from "../common/common";
@@ -15,30 +15,21 @@ import { BrowserLocationConfig } from "./browserLocationConfig";
1515
export class HashLocationService implements LocationServices, Disposable {
1616
private _listeners: Function[] = [];
1717

18-
hash() {
19-
return splitHash(trimHashVal(location.hash))[1];
20-
}
21-
22-
path() {
23-
return splitHash(splitQuery(trimHashVal(location.hash))[0])[0];
24-
}
18+
hash = () => splitHash(trimHashVal(location.hash))[1];
19+
path = () => splitHash(splitQuery(trimHashVal(location.hash))[0])[0];
20+
search = () => getParams(splitQuery(splitHash(trimHashVal(location.hash))[0])[1]);
2521

26-
search() {
27-
return getParams(splitQuery(splitHash(trimHashVal(location.hash))[0])[1]);
28-
}
29-
30-
setUrl(url: string, replace: boolean = true) {
22+
url(url?: string, replace: boolean = true): string {
3123
if (isDefined(url)) location.hash = url;
24+
return buildUrl(this);
3225
};
3326

3427
onChange(cb: EventListener) {
3528
window.addEventListener('hashchange', cb, false);
3629
return pushTo(this._listeners, () => window.removeEventListener('hashchange', cb));
3730
}
3831

39-
dispose() {
40-
deregAll(this._listeners);
41-
}
32+
dispose = () => deregAll(this._listeners);
4233
}
4334

4435
/** A `UIRouterPlugin` uses the browser hash to get/set the current location */

src/vanilla/memoryLocation.ts

+9-28
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/ /** */
55
import { isDefined } from "../common/index";
66
import { LocationConfig, LocationServices } from "../common/coreservices";
7-
import { splitQuery, getParams, splitHash, locationPluginFactory } from "./utils";
7+
import { splitQuery, getParams, splitHash, locationPluginFactory, buildUrl } from "./utils";
88
import { removeFrom, unnestR, deregAll, noop } from "../common/common";
99
import { UIRouter } from "../router";
1010
import { LocationPlugin } from "./interface";
@@ -44,51 +44,32 @@ export class MemoryLocationService implements LocationServices, Disposable {
4444
this._listeners.forEach(cb => cb(evt));
4545
}
4646

47-
url() {
48-
let s = this._url.search;
49-
let hash = this._url.hash;
50-
let query = Object.keys(s).map(key => (isArray(s[key]) ? s[key] : [s[key]]) .map(val => key + "=" + val))
51-
.reduce(unnestR, [])
52-
.join("&");
47+
hash = () => this._url.hash;
48+
path = () => this._url.path;
49+
search = () => this._url.search;
5350

54-
return this._url.path +
55-
(query ? "?" + query : "") +
56-
(hash ? "#" + hash : "");
57-
}
58-
59-
hash() {
60-
return this._url.hash;
61-
}
62-
63-
path() {
64-
return this._url.path;
65-
}
66-
67-
search() {
68-
return this._url.search;
69-
}
70-
71-
setUrl(url: string, replace: boolean = false) {
51+
url(url?: string, replace: boolean = false, state?): string {
7252
if (isDefined(url)) {
7353
let path = splitHash(splitQuery(url)[0])[0];
7454
let hash = splitHash(url)[1];
7555
let search = getParams(splitQuery(splitHash(url)[0])[1]);
7656

7757
let oldval = this.url();
7858
this._url = { path, search, hash };
59+
7960
let newval = this.url();
8061
this._urlChanged(newval, oldval);
8162
}
63+
64+
return buildUrl(this);
8265
}
8366

8467
onChange(cb: EventListener) {
8568
this._listeners.push(cb);
8669
return () => removeFrom(this._listeners, cb);
8770
}
8871

89-
dispose() {
90-
deregAll(this._listeners);
91-
}
72+
dispose = () => deregAll(this._listeners);
9273
}
9374

9475
/** A `UIRouterPlugin` that gets/sets the current location from an in-memory object */

src/vanilla/pushStateLocation.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/ /** */
55
import { isDefined } from "../common/index";
66
import { LocationServices, LocationConfig } from "../common/coreservices";
7-
import { splitQuery, trimHashVal, getParams, locationPluginFactory } from "./utils";
7+
import { splitQuery, trimHashVal, getParams, locationPluginFactory, buildUrl } from "./utils";
88
import { LocationPlugin } from "./interface";
99
import { UIRouter } from "../router";
1010
import { pushTo, deregAll } from "../common/common";
@@ -44,12 +44,14 @@ export class PushStateLocationService implements LocationServices, Disposable {
4444
return getParams(splitQuery(this._location.search)[1]);
4545
}
4646

47-
setUrl(url: string, replace: boolean = false) {
47+
url(url?: string, replace: boolean = false, state?: any): any {
4848
if (isDefined(url)) {
4949
let fullUrl = this._config.baseHref() + url;
50-
if (replace) this._history.replaceState(null, null, fullUrl);
51-
else this._history.pushState(null, null, fullUrl);
50+
if (replace) this._history.replaceState(state, null, fullUrl);
51+
else this._history.pushState(state, null, fullUrl);
5252
}
53+
54+
return buildUrl(this);
5355
}
5456

5557
onChange(cb: EventListener) {

src/vanilla/utils.ts

+17-3
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@
33
* @module vanilla
44
*/ /** */
55
import {isArray} from "../common/index";
6-
import { LocationServices, LocationConfig, services } from "../common/coreservices";
6+
import { LocationServices, LocationConfig } from "../common/coreservices";
77
import { UIRouter } from "../router";
8-
import { identity } from "../common/common";
8+
import { identity, unnestR } from "../common/common";
99

1010
const beforeAfterSubstr = (char: string) => (str: string): string[] => {
1111
if (!str) return ["", ""];
@@ -31,7 +31,21 @@ export const keyValsToObjectR = (accum, [key, val]) => {
3131
};
3232

3333
export const getParams = (queryString: string): any =>
34-
queryString.split("&").filter(identity).map(splitEqual).reduce(keyValsToObjectR, {});
34+
queryString.split("&").filter(identity).map(splitEqual).reduce(keyValsToObjectR, {});
35+
36+
export const buildUrl = (loc: LocationServices) => {
37+
let path = loc.path();
38+
let searchObject = loc.search();
39+
let hash = loc.hash();
40+
41+
let search = Object.keys(searchObject).map(key => {
42+
let param = searchObject[key];
43+
let vals = isArray(param) ? param : [param];
44+
return vals.map(val => key + "=" + val);
45+
}).reduce(unnestR, []).join("&");
46+
47+
return path + (search ? "?" + search : "") + (hash ? "#" + hash : "");
48+
};
3549

3650
export function locationPluginFactory(
3751
name: string,

test/lazyLoadSpec.ts

+3-3
Original file line numberDiff line numberDiff line change
@@ -287,7 +287,7 @@ describe('future state', function () {
287287
});
288288

289289
it('triggered by a URL sync should re-parse the URL to activate the lazy loaded state', (done) => {
290-
router.urlService.setUrl('/a/def');
290+
router.urlService.url('/a/def');
291291
$urlRouter.sync();
292292
$transitions.onSuccess({}, () => {
293293
expect($state.current.name).toBe('A');
@@ -333,7 +333,7 @@ describe('future state', function () {
333333
});
334334

335335
it('should re-parse the URL to activate the final state', (done) => {
336-
router.urlService.setUrl('/a/def/b');
336+
router.urlService.url('/a/def/b');
337337
$urlRouter.sync();
338338
$transitions.onSuccess({}, () => {
339339
expect($state.current.name).toBe('A.B');
@@ -451,7 +451,7 @@ describe('future state', function () {
451451
});
452452

453453
it('should load and activate a nested future state by url sync', (done) => {
454-
router.urlService.setUrl('/a/aid/b/bid');
454+
router.urlService.url('/a/aid/b/bid');
455455
$urlRouter.sync();
456456
$transitions.onSuccess({}, (trans) => {
457457
expect($state.current.name).toBe('A.B');

test/stateRegistrySpec.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ describe("StateRegistry", () => {
6464
registry.register(state);
6565

6666
spyOn($state, "transitionTo");
67-
router.urlService.setUrl("/foo");
67+
router.urlService.url("/foo");
6868
router.urlRouter.sync();
6969
expect($state.transitionTo['calls'].mostRecent().args[0]).toBe(state.$$state());
7070

test/stateServiceSpec.ts

+11-11
Original file line numberDiff line numberDiff line change
@@ -115,12 +115,12 @@ describe('stateService', function () {
115115
});
116116

117117
it("should not update the URL in response to synchronizing URL", ((done) => {
118-
$loc.setUrl('/a/b/c');
119-
var setUrl = spyOn($loc, 'setUrl').and.callThrough();
118+
$loc.url('/a/b/c');
119+
var url = spyOn($loc, 'url').and.callThrough();
120120

121121
wait().then(() => {
122122
expect($state.current.name).toBe('C');
123-
let pushedUrls = setUrl.calls.all().map(x => x.args[0]).filter(x => x !== undefined);
123+
let pushedUrls = url.calls.all().map(x => x.args[0]).filter(x => x !== undefined);
124124
expect(pushedUrls).toEqual([]);
125125
expect($loc.path()).toBe('/a/b/c');
126126
done();
@@ -130,12 +130,12 @@ describe('stateService', function () {
130130
it("should update the URL in response to synchronizing URL then redirecting", ((done) => {
131131
$transitions.onStart({ to: 'C' }, () => $state.target('D'));
132132

133-
$loc.setUrl('/a/b/c');
134-
var setUrl = spyOn($loc, 'setUrl').and.callThrough();
133+
$loc.url('/a/b/c');
134+
var url = spyOn($loc, 'url').and.callThrough();
135135

136136
wait().then(() => {
137137
expect($state.current.name).toBe('D');
138-
let pushedUrls = setUrl.calls.all().map(x => x.args[0]).filter(x => x !== undefined);
138+
let pushedUrls = url.calls.all().map(x => x.args[0]).filter(x => x !== undefined);
139139
expect(pushedUrls).toEqual(['/a/b/c/d']);
140140
expect($loc.path()).toBe('/a/b/c/d');
141141
done();
@@ -176,7 +176,7 @@ describe('stateService', function () {
176176
var dynamicstate, childWithParam, childNoParam;
177177

178178
beforeEach(async function (done) {
179-
$loc.setUrl("asdfasfdasf");
179+
$loc.url("asdfasfdasf");
180180
dynlog = paramsChangedLog = "";
181181
dynamicstate = {
182182
name: 'dyn',
@@ -368,7 +368,7 @@ describe('stateService', function () {
368368
done();
369369
});
370370

371-
$loc.setUrl('/dynstate/p1/pd1?search=s1&searchDyn=sd2');
371+
$loc.url('/dynstate/p1/pd1?search=s1&searchDyn=sd2');
372372
});
373373

374374
it('exits and enters a state when any non-dynamic params change (triggered via url)', (done) => {
@@ -377,7 +377,7 @@ describe('stateService', function () {
377377
done();
378378
});
379379

380-
$loc.setUrl('/dynstate/p1/pd1?search=s2&searchDyn=sd2');
380+
$loc.url('/dynstate/p1/pd1?search=s2&searchDyn=sd2');
381381
});
382382

383383
it('does not exit nor enter a state when only dynamic params change (triggered via $state transition)', async (done) => {
@@ -410,7 +410,7 @@ describe('stateService', function () {
410410
}
411411
});
412412

413-
$loc.setUrl('/dynstate/p1/pd1?search=s1&searchDyn=sd2'); // {search: 's1', searchDyn: 'sd2'});
413+
$loc.url('/dynstate/p1/pd1?search=s1&searchDyn=sd2'); // {search: 's1', searchDyn: 'sd2'});
414414
});
415415

416416
it('updates $stateParams and $location.search when only dynamic params change (triggered via $state transition)', async (done) => {
@@ -443,7 +443,7 @@ describe('stateService', function () {
443443
});
444444

445445
it('doesn\'t re-enter state (triggered by url change)', function (done) {
446-
$loc.setUrl($loc.path() + "?term=hello");
446+
$loc.url($loc.path() + "?term=hello");
447447
awaitTransition(router).then(() => {
448448
expect($loc.search()).toEqual({term: 'hello'});
449449
expect(entered).toBeFalsy();

0 commit comments

Comments
 (0)