Skip to content

Commit

Permalink
feat(common): add ability to retrieve the state from Location service (
Browse files Browse the repository at this point in the history
…#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
  • Loading branch information
jasonaden authored and benlesh committed Apr 24, 2019
1 parent d0672c2 commit b44b143
Show file tree
Hide file tree
Showing 11 changed files with 81 additions and 5 deletions.
11 changes: 10 additions & 1 deletion packages/common/src/location/location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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({
Expand All @@ -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.
*
Expand Down
1 change: 1 addition & 0 deletions packages/common/src/location/platform_location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down
1 change: 1 addition & 0 deletions packages/common/test/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ ts_library(
deps = [
"//packages/common",
"//packages/common/locales",
"//packages/common/testing",
"//packages/compiler",
"//packages/core",
"//packages/core/testing",
Expand Down
42 changes: 41 additions & 1 deletion packages/common/test/location/location_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand Down Expand Up @@ -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'});
}));

});
});
6 changes: 3 additions & 3 deletions packages/common/testing/src/location_mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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});
}
}

Expand Down
10 changes: 10 additions & 0 deletions packages/common/testing/src/mock_location_strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export class MockLocationStrategy extends LocationStrategy {
urlChanges: string[] = [];
/** @internal */
_subject: EventEmitter<any> = new EventEmitter();
private stateChanges: any[] = [];
constructor() { super(); }

simulatePopState(url: string): void {
Expand All @@ -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) : '');
Expand All @@ -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) : '');
Expand All @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
3 changes: 3 additions & 0 deletions packages/platform-server/src/location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
5 changes: 5 additions & 0 deletions tools/public_api_guard/common/common.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions tools/public_api_guard/common/testing.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down

0 comments on commit b44b143

Please sign in to comment.