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;