Skip to content

Commit b44b143

Browse files
jasonadenbenlesh
authored andcommitted
feat(common): add ability to retrieve the state from Location service (#30055)
Previously there wasn't a way to retrieve `history.state` from the `Location` service. The only time the framework exposed this value was in navigation events. This meant if you weren't using the Angular router, there wasn't a way to get access to this `history.state` value other than going directly to the DOM. This PR adds an API to retrieve the value of `history.state`. This will be useful and needed to provide a backwards-compatible `Location` service that can emulate AngularJS's `$location` service since we will need to be able to read the state data in order to produce AngularJS location transition events. This feature will additionally be useful to any application that wants to access state data through Angular rather than going directly to the DOM APIs. PR Close #30055
1 parent d0672c2 commit b44b143

File tree

11 files changed

+81
-5
lines changed

11 files changed

+81
-5
lines changed

packages/common/src/location/location.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {EventEmitter, Injectable} from '@angular/core';
1010
import {SubscriptionLike} from 'rxjs';
1111

1212
import {LocationStrategy} from './location_strategy';
13+
import {PlatformLocation} from './platform_location';
1314

1415
/** @publicApi */
1516
export interface PopStateEvent {
@@ -54,10 +55,13 @@ export class Location {
5455
_baseHref: string;
5556
/** @internal */
5657
_platformStrategy: LocationStrategy;
58+
/** @internal */
59+
_platformLocation: PlatformLocation;
5760

58-
constructor(platformStrategy: LocationStrategy) {
61+
constructor(platformStrategy: LocationStrategy, platformLocation: PlatformLocation) {
5962
this._platformStrategy = platformStrategy;
6063
const browserBaseHref = this._platformStrategy.getBaseHref();
64+
this._platformLocation = platformLocation;
6165
this._baseHref = Location.stripTrailingSlash(_stripIndexHtml(browserBaseHref));
6266
this._platformStrategy.onPopState((ev) => {
6367
this._subject.emit({
@@ -82,6 +86,11 @@ export class Location {
8286
return this.normalize(this._platformStrategy.path(includeHash));
8387
}
8488

89+
/**
90+
* Returns the current value of the history.state object.
91+
*/
92+
getState(): unknown { return this._platformLocation.getState(); }
93+
8594
/**
8695
* Normalizes the given path and compares to the current normalized path.
8796
*

packages/common/src/location/platform_location.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import {InjectionToken} from '@angular/core';
3131
*/
3232
export abstract class PlatformLocation {
3333
abstract getBaseHrefFromDOM(): string;
34+
abstract getState(): unknown;
3435
abstract onPopState(fn: LocationChangeListener): void;
3536
abstract onHashChange(fn: LocationChangeListener): void;
3637

packages/common/test/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ ts_library(
1111
deps = [
1212
"//packages/common",
1313
"//packages/common/locales",
14+
"//packages/common/testing",
1415
"//packages/compiler",
1516
"//packages/core",
1617
"//packages/core/testing",

packages/common/test/location/location_spec.ts

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {Location} from '@angular/common';
9+
import {CommonModule, Location, LocationStrategy, PlatformLocation} from '@angular/common';
10+
import {PathLocationStrategy} from '@angular/common/src/common';
11+
import {MockPlatformLocation} from '@angular/common/testing';
12+
import {TestBed, inject} from '@angular/core/testing';
1013

1114
const baseUrl = '/base';
1215

@@ -37,4 +40,41 @@ describe('Location Class', () => {
3740
expect(Location.stripTrailingSlash(input)).toBe(input);
3841
});
3942
});
43+
44+
describe('location.getState()', () => {
45+
beforeEach(() => {
46+
TestBed.configureTestingModule({
47+
imports: [CommonModule],
48+
providers: [
49+
{provide: LocationStrategy, useClass: PathLocationStrategy},
50+
{provide: PlatformLocation, useFactory: () => { return new MockPlatformLocation(); }},
51+
{provide: Location, useClass: Location, deps: [LocationStrategy, PlatformLocation]},
52+
]
53+
});
54+
});
55+
56+
it('should get the state object', inject([Location], (location: Location) => {
57+
58+
expect(location.getState()).toBe(null);
59+
60+
location.go('/test', '', {foo: 'bar'});
61+
62+
expect(location.getState()).toEqual({foo: 'bar'});
63+
}));
64+
65+
it('should work after using back button', inject([Location], (location: Location) => {
66+
67+
expect(location.getState()).toBe(null);
68+
69+
location.go('/test1', '', {url: 'test1'});
70+
location.go('/test2', '', {url: 'test2'});
71+
72+
expect(location.getState()).toEqual({url: 'test2'});
73+
74+
location.back();
75+
76+
expect(location.getState()).toEqual({url: 'test1'});
77+
}));
78+
79+
});
4080
});

packages/common/testing/src/location_mock.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ export class SpyLocation implements Location {
3434

3535
path(): string { return this._history[this._historyIndex].path; }
3636

37-
private state(): string { return this._history[this._historyIndex].state; }
37+
getState(): unknown { return this._history[this._historyIndex].state; }
3838

3939
isCurrentPathEqualTo(path: string, query: string = ''): boolean {
4040
const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path;
@@ -100,14 +100,14 @@ export class SpyLocation implements Location {
100100
forward() {
101101
if (this._historyIndex < (this._history.length - 1)) {
102102
this._historyIndex++;
103-
this._subject.emit({'url': this.path(), 'state': this.state(), 'pop': true});
103+
this._subject.emit({'url': this.path(), 'state': this.getState(), 'pop': true});
104104
}
105105
}
106106

107107
back() {
108108
if (this._historyIndex > 0) {
109109
this._historyIndex--;
110-
this._subject.emit({'url': this.path(), 'state': this.state(), 'pop': true});
110+
this._subject.emit({'url': this.path(), 'state': this.getState(), 'pop': true});
111111
}
112112
}
113113

packages/common/testing/src/mock_location_strategy.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export class MockLocationStrategy extends LocationStrategy {
2525
urlChanges: string[] = [];
2626
/** @internal */
2727
_subject: EventEmitter<any> = new EventEmitter();
28+
private stateChanges: any[] = [];
2829
constructor() { super(); }
2930

3031
simulatePopState(url: string): void {
@@ -42,6 +43,9 @@ export class MockLocationStrategy extends LocationStrategy {
4243
}
4344

4445
pushState(ctx: any, title: string, path: string, query: string): void {
46+
// Add state change to changes array
47+
this.stateChanges.push(ctx);
48+
4549
this.internalTitle = title;
4650

4751
const url = path + (query.length > 0 ? ('?' + query) : '');
@@ -52,6 +56,9 @@ export class MockLocationStrategy extends LocationStrategy {
5256
}
5357

5458
replaceState(ctx: any, title: string, path: string, query: string): void {
59+
// Reset the last index of stateChanges to the ctx (state) object
60+
this.stateChanges[(this.stateChanges.length || 1) - 1] = ctx;
61+
5562
this.internalTitle = title;
5663

5764
const url = path + (query.length > 0 ? ('?' + query) : '');
@@ -68,12 +75,15 @@ export class MockLocationStrategy extends LocationStrategy {
6875
back(): void {
6976
if (this.urlChanges.length > 0) {
7077
this.urlChanges.pop();
78+
this.stateChanges.pop();
7179
const nextUrl = this.urlChanges.length > 0 ? this.urlChanges[this.urlChanges.length - 1] : '';
7280
this.simulatePopState(nextUrl);
7381
}
7482
}
7583

7684
forward(): void { throw 'not implemented'; }
85+
86+
getState(): unknown { return this.stateChanges[(this.stateChanges.length || 1) - 1]; }
7787
}
7888

7989
class _MockPopStateEvent {

packages/platform-browser/src/browser/location/browser_platform_location.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,4 +73,6 @@ export class BrowserPlatformLocation extends PlatformLocation {
7373
forward(): void { this._history.forward(); }
7474

7575
back(): void { this._history.back(); }
76+
77+
getState(): unknown { return this._history.state; }
7678
}

packages/platform-server/src/location.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,9 @@ export class ServerPlatformLocation implements PlatformLocation {
8383
forward(): void { throw new Error('Not implemented'); }
8484

8585
back(): void { throw new Error('Not implemented'); }
86+
87+
// History API isn't available on server, therefore return undefined
88+
getState(): unknown { return undefined; }
8689
}
8790

8891
export function scheduleMicroTask(fn: Function) {

packages/platform-webworker/src/web_workers/worker/platform_location.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,7 @@ export class WebWorkerPlatformLocation extends PlatformLocation {
121121
const args = new UiArguments('back');
122122
this._broker.runOnService(args, null);
123123
}
124+
125+
// History API isn't available on WebWorkers, therefore return undefined
126+
getState(): unknown { return undefined; }
124127
}

tools/public_api_guard/common/common.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ export declare class HashLocationStrategy extends LocationStrategy {
116116
back(): void;
117117
forward(): void;
118118
getBaseHref(): string;
119+
getState(): unknown;
119120
onPopState(fn: LocationChangeListener): void;
120121
path(includeHash?: boolean): string;
121122
prepareExternalUrl(internal: string): string;
@@ -169,6 +170,7 @@ export declare class Location {
169170
constructor(platformStrategy: LocationStrategy);
170171
back(): void;
171172
forward(): void;
173+
getState(): unknown;
172174
go(path: string, query?: string, state?: any): void;
173175
isCurrentPathEqualTo(path: string, query?: string): boolean;
174176
normalize(url: string): string;
@@ -196,6 +198,7 @@ export declare abstract class LocationStrategy {
196198
abstract back(): void;
197199
abstract forward(): void;
198200
abstract getBaseHref(): string;
201+
abstract getState(): unknown;
199202
abstract onPopState(fn: LocationChangeListener): void;
200203
abstract path(includeHash?: boolean): string;
201204
abstract prepareExternalUrl(internal: string): string;
@@ -359,6 +362,7 @@ export declare class PathLocationStrategy extends LocationStrategy {
359362
back(): void;
360363
forward(): void;
361364
getBaseHref(): string;
365+
getState(): unknown;
362366
onPopState(fn: LocationChangeListener): void;
363367
path(includeHash?: boolean): string;
364368
prepareExternalUrl(internal: string): string;
@@ -378,6 +382,7 @@ export declare abstract class PlatformLocation {
378382
abstract back(): void;
379383
abstract forward(): void;
380384
abstract getBaseHrefFromDOM(): string;
385+
abstract getState(): unknown;
381386
abstract onHashChange(fn: LocationChangeListener): void;
382387
abstract onPopState(fn: LocationChangeListener): void;
383388
abstract pushState(state: any, title: string, url: string): void;

tools/public_api_guard/common/testing.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ export declare class MockLocationStrategy extends LocationStrategy {
77
back(): void;
88
forward(): void;
99
getBaseHref(): string;
10+
getState(): unknown;
1011
onPopState(fn: (value: any) => void): void;
1112
path(includeHash?: boolean): string;
1213
prepareExternalUrl(internal: string): string;
@@ -19,6 +20,7 @@ export declare class SpyLocation implements Location {
1920
urlChanges: string[];
2021
back(): void;
2122
forward(): void;
23+
getState(): unknown;
2224
go(path: string, query?: string, state?: any): void;
2325
isCurrentPathEqualTo(path: string, query?: string): boolean;
2426
normalize(url: string): string;

0 commit comments

Comments
 (0)