From b44b14368fa6291b2b33c97dca8266afe8ad663c Mon Sep 17 00:00:00 2001 From: Jason Aden Date: Wed, 6 Feb 2019 14:42:57 -0800 Subject: [PATCH] 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 --- packages/common/src/location/location.ts | 11 ++++- .../common/src/location/platform_location.ts | 1 + packages/common/test/BUILD.bazel | 1 + .../common/test/location/location_spec.ts | 42 ++++++++++++++++++- packages/common/testing/src/location_mock.ts | 6 +-- .../testing/src/mock_location_strategy.ts | 10 +++++ .../location/browser_platform_location.ts | 2 + packages/platform-server/src/location.ts | 3 ++ .../web_workers/worker/platform_location.ts | 3 ++ tools/public_api_guard/common/common.d.ts | 5 +++ tools/public_api_guard/common/testing.d.ts | 2 + 11 files changed, 81 insertions(+), 5 deletions(-) diff --git a/packages/common/src/location/location.ts b/packages/common/src/location/location.ts index 734d1e7a7d..d12fb052ed 100644 --- a/packages/common/src/location/location.ts +++ b/packages/common/src/location/location.ts @@ -10,6 +10,7 @@ import {EventEmitter, Injectable} from '@angular/core'; import {SubscriptionLike} from 'rxjs'; import {LocationStrategy} from './location_strategy'; +import {PlatformLocation} from './platform_location'; /** @publicApi */ export interface PopStateEvent { @@ -54,10 +55,13 @@ export class Location { _baseHref: string; /** @internal */ _platformStrategy: LocationStrategy; + /** @internal */ + _platformLocation: PlatformLocation; - constructor(platformStrategy: LocationStrategy) { + constructor(platformStrategy: LocationStrategy, platformLocation: PlatformLocation) { this._platformStrategy = platformStrategy; const browserBaseHref = this._platformStrategy.getBaseHref(); + this._platformLocation = platformLocation; this._baseHref = Location.stripTrailingSlash(_stripIndexHtml(browserBaseHref)); this._platformStrategy.onPopState((ev) => { this._subject.emit({ @@ -82,6 +86,11 @@ export class Location { return this.normalize(this._platformStrategy.path(includeHash)); } + /** + * Returns the current value of the history.state object. + */ + getState(): unknown { return this._platformLocation.getState(); } + /** * Normalizes the given path and compares to the current normalized path. * diff --git a/packages/common/src/location/platform_location.ts b/packages/common/src/location/platform_location.ts index d21ab94ee0..01acc29951 100644 --- a/packages/common/src/location/platform_location.ts +++ b/packages/common/src/location/platform_location.ts @@ -31,6 +31,7 @@ import {InjectionToken} from '@angular/core'; */ export abstract class PlatformLocation { abstract getBaseHrefFromDOM(): string; + abstract getState(): unknown; abstract onPopState(fn: LocationChangeListener): void; abstract onHashChange(fn: LocationChangeListener): void; diff --git a/packages/common/test/BUILD.bazel b/packages/common/test/BUILD.bazel index a4c83de627..28e563592a 100644 --- a/packages/common/test/BUILD.bazel +++ b/packages/common/test/BUILD.bazel @@ -11,6 +11,7 @@ ts_library( deps = [ "//packages/common", "//packages/common/locales", + "//packages/common/testing", "//packages/compiler", "//packages/core", "//packages/core/testing", diff --git a/packages/common/test/location/location_spec.ts b/packages/common/test/location/location_spec.ts index 5f1a250a3e..c11f351cee 100644 --- a/packages/common/test/location/location_spec.ts +++ b/packages/common/test/location/location_spec.ts @@ -6,7 +6,10 @@ * found in the LICENSE file at https://angular.io/license */ -import {Location} from '@angular/common'; +import {CommonModule, Location, LocationStrategy, PlatformLocation} from '@angular/common'; +import {PathLocationStrategy} from '@angular/common/src/common'; +import {MockPlatformLocation} from '@angular/common/testing'; +import {TestBed, inject} from '@angular/core/testing'; const baseUrl = '/base'; @@ -37,4 +40,41 @@ describe('Location Class', () => { expect(Location.stripTrailingSlash(input)).toBe(input); }); }); + + describe('location.getState()', () => { + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [CommonModule], + providers: [ + {provide: LocationStrategy, useClass: PathLocationStrategy}, + {provide: PlatformLocation, useFactory: () => { return new MockPlatformLocation(); }}, + {provide: Location, useClass: Location, deps: [LocationStrategy, PlatformLocation]}, + ] + }); + }); + + it('should get the state object', inject([Location], (location: Location) => { + + expect(location.getState()).toBe(null); + + location.go('/test', '', {foo: 'bar'}); + + expect(location.getState()).toEqual({foo: 'bar'}); + })); + + it('should work after using back button', inject([Location], (location: Location) => { + + expect(location.getState()).toBe(null); + + location.go('/test1', '', {url: 'test1'}); + location.go('/test2', '', {url: 'test2'}); + + expect(location.getState()).toEqual({url: 'test2'}); + + location.back(); + + expect(location.getState()).toEqual({url: 'test1'}); + })); + + }); }); \ No newline at end of file diff --git a/packages/common/testing/src/location_mock.ts b/packages/common/testing/src/location_mock.ts index 58a510a689..e68888798b 100644 --- a/packages/common/testing/src/location_mock.ts +++ b/packages/common/testing/src/location_mock.ts @@ -34,7 +34,7 @@ export class SpyLocation implements Location { path(): string { return this._history[this._historyIndex].path; } - private state(): string { return this._history[this._historyIndex].state; } + getState(): unknown { return this._history[this._historyIndex].state; } isCurrentPathEqualTo(path: string, query: string = ''): boolean { const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path; @@ -100,14 +100,14 @@ export class SpyLocation implements Location { forward() { if (this._historyIndex < (this._history.length - 1)) { this._historyIndex++; - this._subject.emit({'url': this.path(), 'state': this.state(), 'pop': true}); + this._subject.emit({'url': this.path(), 'state': this.getState(), 'pop': true}); } } back() { if (this._historyIndex > 0) { this._historyIndex--; - this._subject.emit({'url': this.path(), 'state': this.state(), 'pop': true}); + this._subject.emit({'url': this.path(), 'state': this.getState(), 'pop': true}); } } diff --git a/packages/common/testing/src/mock_location_strategy.ts b/packages/common/testing/src/mock_location_strategy.ts index c75fb8042e..2c38f8d39f 100644 --- a/packages/common/testing/src/mock_location_strategy.ts +++ b/packages/common/testing/src/mock_location_strategy.ts @@ -25,6 +25,7 @@ export class MockLocationStrategy extends LocationStrategy { urlChanges: string[] = []; /** @internal */ _subject: EventEmitter = new EventEmitter(); + private stateChanges: any[] = []; constructor() { super(); } simulatePopState(url: string): void { @@ -42,6 +43,9 @@ export class MockLocationStrategy extends LocationStrategy { } pushState(ctx: any, title: string, path: string, query: string): void { + // Add state change to changes array + this.stateChanges.push(ctx); + this.internalTitle = title; const url = path + (query.length > 0 ? ('?' + query) : ''); @@ -52,6 +56,9 @@ export class MockLocationStrategy extends LocationStrategy { } replaceState(ctx: any, title: string, path: string, query: string): void { + // Reset the last index of stateChanges to the ctx (state) object + this.stateChanges[(this.stateChanges.length || 1) - 1] = ctx; + this.internalTitle = title; const url = path + (query.length > 0 ? ('?' + query) : ''); @@ -68,12 +75,15 @@ export class MockLocationStrategy extends LocationStrategy { back(): void { if (this.urlChanges.length > 0) { this.urlChanges.pop(); + this.stateChanges.pop(); const nextUrl = this.urlChanges.length > 0 ? this.urlChanges[this.urlChanges.length - 1] : ''; this.simulatePopState(nextUrl); } } forward(): void { throw 'not implemented'; } + + getState(): unknown { return this.stateChanges[(this.stateChanges.length || 1) - 1]; } } class _MockPopStateEvent { diff --git a/packages/platform-browser/src/browser/location/browser_platform_location.ts b/packages/platform-browser/src/browser/location/browser_platform_location.ts index 274df1aa88..55e1a72895 100644 --- a/packages/platform-browser/src/browser/location/browser_platform_location.ts +++ b/packages/platform-browser/src/browser/location/browser_platform_location.ts @@ -73,4 +73,6 @@ export class BrowserPlatformLocation extends PlatformLocation { forward(): void { this._history.forward(); } back(): void { this._history.back(); } + + getState(): unknown { return this._history.state; } } diff --git a/packages/platform-server/src/location.ts b/packages/platform-server/src/location.ts index 5f337110ee..7b39a07368 100644 --- a/packages/platform-server/src/location.ts +++ b/packages/platform-server/src/location.ts @@ -83,6 +83,9 @@ export class ServerPlatformLocation implements PlatformLocation { forward(): void { throw new Error('Not implemented'); } back(): void { throw new Error('Not implemented'); } + + // History API isn't available on server, therefore return undefined + getState(): unknown { return undefined; } } export function scheduleMicroTask(fn: Function) { diff --git a/packages/platform-webworker/src/web_workers/worker/platform_location.ts b/packages/platform-webworker/src/web_workers/worker/platform_location.ts index 63fe2c546b..5592497d6c 100644 --- a/packages/platform-webworker/src/web_workers/worker/platform_location.ts +++ b/packages/platform-webworker/src/web_workers/worker/platform_location.ts @@ -121,4 +121,7 @@ export class WebWorkerPlatformLocation extends PlatformLocation { const args = new UiArguments('back'); this._broker.runOnService(args, null); } + + // History API isn't available on WebWorkers, therefore return undefined + getState(): unknown { return undefined; } } diff --git a/tools/public_api_guard/common/common.d.ts b/tools/public_api_guard/common/common.d.ts index 6ca180685f..8a74b36296 100644 --- a/tools/public_api_guard/common/common.d.ts +++ b/tools/public_api_guard/common/common.d.ts @@ -116,6 +116,7 @@ export declare class HashLocationStrategy extends LocationStrategy { back(): void; forward(): void; getBaseHref(): string; + getState(): unknown; onPopState(fn: LocationChangeListener): void; path(includeHash?: boolean): string; prepareExternalUrl(internal: string): string; @@ -169,6 +170,7 @@ export declare class Location { constructor(platformStrategy: LocationStrategy); back(): void; forward(): void; + getState(): unknown; go(path: string, query?: string, state?: any): void; isCurrentPathEqualTo(path: string, query?: string): boolean; normalize(url: string): string; @@ -196,6 +198,7 @@ export declare abstract class LocationStrategy { abstract back(): void; abstract forward(): void; abstract getBaseHref(): string; + abstract getState(): unknown; abstract onPopState(fn: LocationChangeListener): void; abstract path(includeHash?: boolean): string; abstract prepareExternalUrl(internal: string): string; @@ -359,6 +362,7 @@ export declare class PathLocationStrategy extends LocationStrategy { back(): void; forward(): void; getBaseHref(): string; + getState(): unknown; onPopState(fn: LocationChangeListener): void; path(includeHash?: boolean): string; prepareExternalUrl(internal: string): string; @@ -378,6 +382,7 @@ export declare abstract class PlatformLocation { abstract back(): void; abstract forward(): void; abstract getBaseHrefFromDOM(): string; + abstract getState(): unknown; abstract onHashChange(fn: LocationChangeListener): void; abstract onPopState(fn: LocationChangeListener): void; abstract pushState(state: any, title: string, url: string): void; diff --git a/tools/public_api_guard/common/testing.d.ts b/tools/public_api_guard/common/testing.d.ts index 2bd1077ead..02752aa1ed 100644 --- a/tools/public_api_guard/common/testing.d.ts +++ b/tools/public_api_guard/common/testing.d.ts @@ -7,6 +7,7 @@ export declare class MockLocationStrategy extends LocationStrategy { back(): void; forward(): void; getBaseHref(): string; + getState(): unknown; onPopState(fn: (value: any) => void): void; path(includeHash?: boolean): string; prepareExternalUrl(internal: string): string; @@ -19,6 +20,7 @@ export declare class SpyLocation implements Location { urlChanges: string[]; back(): void; forward(): void; + getState(): unknown; go(path: string, query?: string, state?: any): void; isCurrentPathEqualTo(path: string, query?: string): boolean; normalize(url: string): string;