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
This commit is contained in:
Jason Aden 2019-02-06 14:42:57 -08:00 committed by Ben Lesh
parent d0672c252e
commit b44b14368f
11 changed files with 81 additions and 5 deletions

View File

@ -10,6 +10,7 @@ import {EventEmitter, Injectable} from '@angular/core';
import {SubscriptionLike} from 'rxjs'; import {SubscriptionLike} from 'rxjs';
import {LocationStrategy} from './location_strategy'; import {LocationStrategy} from './location_strategy';
import {PlatformLocation} from './platform_location';
/** @publicApi */ /** @publicApi */
export interface PopStateEvent { export interface PopStateEvent {
@ -54,10 +55,13 @@ export class Location {
_baseHref: string; _baseHref: string;
/** @internal */ /** @internal */
_platformStrategy: LocationStrategy; _platformStrategy: LocationStrategy;
/** @internal */
_platformLocation: PlatformLocation;
constructor(platformStrategy: LocationStrategy) { constructor(platformStrategy: LocationStrategy, platformLocation: PlatformLocation) {
this._platformStrategy = platformStrategy; this._platformStrategy = platformStrategy;
const browserBaseHref = this._platformStrategy.getBaseHref(); const browserBaseHref = this._platformStrategy.getBaseHref();
this._platformLocation = platformLocation;
this._baseHref = Location.stripTrailingSlash(_stripIndexHtml(browserBaseHref)); this._baseHref = Location.stripTrailingSlash(_stripIndexHtml(browserBaseHref));
this._platformStrategy.onPopState((ev) => { this._platformStrategy.onPopState((ev) => {
this._subject.emit({ this._subject.emit({
@ -82,6 +86,11 @@ export class Location {
return this.normalize(this._platformStrategy.path(includeHash)); 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. * Normalizes the given path and compares to the current normalized path.
* *

View File

@ -31,6 +31,7 @@ import {InjectionToken} from '@angular/core';
*/ */
export abstract class PlatformLocation { export abstract class PlatformLocation {
abstract getBaseHrefFromDOM(): string; abstract getBaseHrefFromDOM(): string;
abstract getState(): unknown;
abstract onPopState(fn: LocationChangeListener): void; abstract onPopState(fn: LocationChangeListener): void;
abstract onHashChange(fn: LocationChangeListener): void; abstract onHashChange(fn: LocationChangeListener): void;

View File

@ -11,6 +11,7 @@ ts_library(
deps = [ deps = [
"//packages/common", "//packages/common",
"//packages/common/locales", "//packages/common/locales",
"//packages/common/testing",
"//packages/compiler", "//packages/compiler",
"//packages/core", "//packages/core",
"//packages/core/testing", "//packages/core/testing",

View File

@ -6,7 +6,10 @@
* found in the LICENSE file at https://angular.io/license * 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'; const baseUrl = '/base';
@ -37,4 +40,41 @@ describe('Location Class', () => {
expect(Location.stripTrailingSlash(input)).toBe(input); 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'});
}));
});
}); });

View File

@ -34,7 +34,7 @@ export class SpyLocation implements Location {
path(): string { return this._history[this._historyIndex].path; } 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 { isCurrentPathEqualTo(path: string, query: string = ''): boolean {
const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path; const givenPath = path.endsWith('/') ? path.substring(0, path.length - 1) : path;
@ -100,14 +100,14 @@ export class SpyLocation implements Location {
forward() { forward() {
if (this._historyIndex < (this._history.length - 1)) { if (this._historyIndex < (this._history.length - 1)) {
this._historyIndex++; 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() { back() {
if (this._historyIndex > 0) { if (this._historyIndex > 0) {
this._historyIndex--; this._historyIndex--;
this._subject.emit({'url': this.path(), 'state': this.state(), 'pop': true}); this._subject.emit({'url': this.path(), 'state': this.getState(), 'pop': true});
} }
} }

View File

@ -25,6 +25,7 @@ export class MockLocationStrategy extends LocationStrategy {
urlChanges: string[] = []; urlChanges: string[] = [];
/** @internal */ /** @internal */
_subject: EventEmitter<any> = new EventEmitter(); _subject: EventEmitter<any> = new EventEmitter();
private stateChanges: any[] = [];
constructor() { super(); } constructor() { super(); }
simulatePopState(url: string): void { simulatePopState(url: string): void {
@ -42,6 +43,9 @@ export class MockLocationStrategy extends LocationStrategy {
} }
pushState(ctx: any, title: string, path: string, query: string): void { pushState(ctx: any, title: string, path: string, query: string): void {
// Add state change to changes array
this.stateChanges.push(ctx);
this.internalTitle = title; this.internalTitle = title;
const url = path + (query.length > 0 ? ('?' + query) : ''); 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 { 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; this.internalTitle = title;
const url = path + (query.length > 0 ? ('?' + query) : ''); const url = path + (query.length > 0 ? ('?' + query) : '');
@ -68,12 +75,15 @@ export class MockLocationStrategy extends LocationStrategy {
back(): void { back(): void {
if (this.urlChanges.length > 0) { if (this.urlChanges.length > 0) {
this.urlChanges.pop(); this.urlChanges.pop();
this.stateChanges.pop();
const nextUrl = this.urlChanges.length > 0 ? this.urlChanges[this.urlChanges.length - 1] : ''; const nextUrl = this.urlChanges.length > 0 ? this.urlChanges[this.urlChanges.length - 1] : '';
this.simulatePopState(nextUrl); this.simulatePopState(nextUrl);
} }
} }
forward(): void { throw 'not implemented'; } forward(): void { throw 'not implemented'; }
getState(): unknown { return this.stateChanges[(this.stateChanges.length || 1) - 1]; }
} }
class _MockPopStateEvent { class _MockPopStateEvent {

View File

@ -73,4 +73,6 @@ export class BrowserPlatformLocation extends PlatformLocation {
forward(): void { this._history.forward(); } forward(): void { this._history.forward(); }
back(): void { this._history.back(); } back(): void { this._history.back(); }
getState(): unknown { return this._history.state; }
} }

View File

@ -83,6 +83,9 @@ export class ServerPlatformLocation implements PlatformLocation {
forward(): void { throw new Error('Not implemented'); } forward(): void { throw new Error('Not implemented'); }
back(): 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) { export function scheduleMicroTask(fn: Function) {

View File

@ -121,4 +121,7 @@ export class WebWorkerPlatformLocation extends PlatformLocation {
const args = new UiArguments('back'); const args = new UiArguments('back');
this._broker.runOnService(args, null); this._broker.runOnService(args, null);
} }
// History API isn't available on WebWorkers, therefore return undefined
getState(): unknown { return undefined; }
} }

View File

@ -116,6 +116,7 @@ export declare class HashLocationStrategy extends LocationStrategy {
back(): void; back(): void;
forward(): void; forward(): void;
getBaseHref(): string; getBaseHref(): string;
getState(): unknown;
onPopState(fn: LocationChangeListener): void; onPopState(fn: LocationChangeListener): void;
path(includeHash?: boolean): string; path(includeHash?: boolean): string;
prepareExternalUrl(internal: string): string; prepareExternalUrl(internal: string): string;
@ -169,6 +170,7 @@ export declare class Location {
constructor(platformStrategy: LocationStrategy); constructor(platformStrategy: LocationStrategy);
back(): void; back(): void;
forward(): void; forward(): void;
getState(): unknown;
go(path: string, query?: string, state?: any): void; go(path: string, query?: string, state?: any): void;
isCurrentPathEqualTo(path: string, query?: string): boolean; isCurrentPathEqualTo(path: string, query?: string): boolean;
normalize(url: string): string; normalize(url: string): string;
@ -196,6 +198,7 @@ export declare abstract class LocationStrategy {
abstract back(): void; abstract back(): void;
abstract forward(): void; abstract forward(): void;
abstract getBaseHref(): string; abstract getBaseHref(): string;
abstract getState(): unknown;
abstract onPopState(fn: LocationChangeListener): void; abstract onPopState(fn: LocationChangeListener): void;
abstract path(includeHash?: boolean): string; abstract path(includeHash?: boolean): string;
abstract prepareExternalUrl(internal: string): string; abstract prepareExternalUrl(internal: string): string;
@ -359,6 +362,7 @@ export declare class PathLocationStrategy extends LocationStrategy {
back(): void; back(): void;
forward(): void; forward(): void;
getBaseHref(): string; getBaseHref(): string;
getState(): unknown;
onPopState(fn: LocationChangeListener): void; onPopState(fn: LocationChangeListener): void;
path(includeHash?: boolean): string; path(includeHash?: boolean): string;
prepareExternalUrl(internal: string): string; prepareExternalUrl(internal: string): string;
@ -378,6 +382,7 @@ export declare abstract class PlatformLocation {
abstract back(): void; abstract back(): void;
abstract forward(): void; abstract forward(): void;
abstract getBaseHrefFromDOM(): string; abstract getBaseHrefFromDOM(): string;
abstract getState(): unknown;
abstract onHashChange(fn: LocationChangeListener): void; abstract onHashChange(fn: LocationChangeListener): void;
abstract onPopState(fn: LocationChangeListener): void; abstract onPopState(fn: LocationChangeListener): void;
abstract pushState(state: any, title: string, url: string): void; abstract pushState(state: any, title: string, url: string): void;

View File

@ -7,6 +7,7 @@ export declare class MockLocationStrategy extends LocationStrategy {
back(): void; back(): void;
forward(): void; forward(): void;
getBaseHref(): string; getBaseHref(): string;
getState(): unknown;
onPopState(fn: (value: any) => void): void; onPopState(fn: (value: any) => void): void;
path(includeHash?: boolean): string; path(includeHash?: boolean): string;
prepareExternalUrl(internal: string): string; prepareExternalUrl(internal: string): string;
@ -19,6 +20,7 @@ export declare class SpyLocation implements Location {
urlChanges: string[]; urlChanges: string[];
back(): void; back(): void;
forward(): void; forward(): void;
getState(): unknown;
go(path: string, query?: string, state?: any): void; go(path: string, query?: string, state?: any): void;
isCurrentPathEqualTo(path: string, query?: string): boolean; isCurrentPathEqualTo(path: string, query?: string): boolean;
normalize(url: string): string; normalize(url: string): string;