Skip to content

Commit e05a6f3

Browse files
aahmedayedatscott
authored andcommitted
feat(common): add historyGo method to Location service (angular#38890)
Add new method `historyGo`, that will let the user navigate to a specific page from session history identified by its relative position to the current page. We add some tests to `location_spec.ts` to validate the behavior of the `historyGo` and `forward` methods. Add more tests for `location_spec` to test `location.historyGo(0)`, `location.historyGo()`, `location.historyGo(100)` and `location.historyGo(-100)`. We also add new tests for `Integration` spec to validate the navigation when we using `location#historyGo`. Update the `historyGo` function docs Note that this was made an optional function in the abstract classes to avoid a breaking change. Because our location classes use `implements PlatformLocation` rather than `extends PlatformLocation`, simply adding a default implementation was not sufficient to make this a non-breaking change. While we could fix the classes internal to Angular, this would still have been a breaking change for any external developers who may have followed our implementations as an example. PR Close angular#38890
1 parent 3a823ab commit e05a6f3

File tree

11 files changed

+184
-17
lines changed

11 files changed

+184
-17
lines changed

goldens/public-api/common/common.d.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -101,6 +101,7 @@ export declare class HashLocationStrategy extends LocationStrategy implements On
101101
back(): void;
102102
forward(): void;
103103
getBaseHref(): string;
104+
historyGo(relativePosition?: number): void;
104105
ngOnDestroy(): void;
105106
onPopState(fn: LocationChangeListener): void;
106107
path(includeHash?: boolean): string;
@@ -156,6 +157,7 @@ export declare class Location {
156157
forward(): void;
157158
getState(): unknown;
158159
go(path: string, query?: string, state?: any): void;
160+
historyGo(relativePosition?: number): void;
159161
isCurrentPathEqualTo(path: string, query?: string): boolean;
160162
normalize(url: string): string;
161163
onUrlChange(fn: (url: string, state: unknown) => void): void;
@@ -183,6 +185,7 @@ export declare abstract class LocationStrategy {
183185
abstract back(): void;
184186
abstract forward(): void;
185187
abstract getBaseHref(): string;
188+
historyGo?(relativePosition: number): void;
186189
abstract onPopState(fn: LocationChangeListener): void;
187190
abstract path(includeHash?: boolean): string;
188191
abstract prepareExternalUrl(internal: string): string;
@@ -330,6 +333,7 @@ export declare class PathLocationStrategy extends LocationStrategy implements On
330333
back(): void;
331334
forward(): void;
332335
getBaseHref(): string;
336+
historyGo(relativePosition?: number): void;
333337
ngOnDestroy(): void;
334338
onPopState(fn: LocationChangeListener): void;
335339
path(includeHash?: boolean): string;
@@ -357,6 +361,7 @@ export declare abstract class PlatformLocation {
357361
abstract forward(): void;
358362
abstract getBaseHrefFromDOM(): string;
359363
abstract getState(): unknown;
364+
historyGo?(relativePosition: number): void;
360365
abstract onHashChange(fn: LocationChangeListener): VoidFunction;
361366
abstract onPopState(fn: LocationChangeListener): VoidFunction;
362367
abstract pushState(state: any, title: string, url: string): void;

goldens/public-api/common/testing/testing.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ export declare class MockPlatformLocation implements PlatformLocation {
3333
forward(): void;
3434
getBaseHrefFromDOM(): string;
3535
getState(): unknown;
36+
historyGo(relativePosition?: number): void;
3637
onHashChange(fn: LocationChangeListener): VoidFunction;
3738
onPopState(fn: LocationChangeListener): VoidFunction;
3839
pushState(state: any, title: string, newUrl: string): void;
@@ -50,6 +51,7 @@ export declare class SpyLocation implements Location {
5051
forward(): void;
5152
getState(): unknown;
5253
go(path: string, query?: string, state?: any): void;
54+
historyGo(relativePosition?: number): void;
5355
isCurrentPathEqualTo(path: string, query?: string): boolean;
5456
normalize(url: string): string;
5557
onUrlChange(fn: (url: string, state: unknown) => void): void;

goldens/size-tracking/integration-payloads.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"master": {
5050
"uncompressed": {
5151
"runtime-es2015": 2289,
52-
"main-es2015": 216267,
52+
"main-es2015": 216935,
5353
"polyfills-es2015": 36723,
5454
"5-es2015": 781
5555
}

packages/common/src/location/hash_location_strategy.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,4 +98,8 @@ export class HashLocationStrategy extends LocationStrategy implements OnDestroy
9898
back(): void {
9999
this._platformLocation.back();
100100
}
101+
102+
historyGo(relativePosition: number = 0): void {
103+
this._platformLocation.historyGo?.(relativePosition);
104+
}
101105
}

packages/common/src/location/location.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,22 @@ export class Location {
188188
this._platformStrategy.back();
189189
}
190190

191+
/**
192+
* Navigate to a specific page from session history, identified by its relative position to the
193+
* current page.
194+
*
195+
* @param relativePosition Position of the target page in the history relative to the current
196+
* page.
197+
* A negative value moves backwards, a positive value moves forwards, e.g. `location.historyGo(2)`
198+
* moves forward two pages and `location.historyGo(-2)` moves back two pages. When we try to go
199+
* beyond what's stored in the history session, we stay in the current page. Same behaviour occurs
200+
* when `relativePosition` equals 0.
201+
* @see https://developer.mozilla.org/en-US/docs/Web/API/History_API#Moving_to_a_specific_point_in_history
202+
*/
203+
historyGo(relativePosition: number = 0): void {
204+
this._platformStrategy.historyGo?.(relativePosition);
205+
}
206+
191207
/**
192208
* Registers a URL change listener. Use to catch updates performed by the Angular
193209
* framework that are not detectible through "popstate" or "hashchange" events.

packages/common/src/location/location_strategy.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ export abstract class LocationStrategy {
3636
abstract replaceState(state: any, title: string, url: string, queryParams: string): void;
3737
abstract forward(): void;
3838
abstract back(): void;
39+
historyGo?(relativePosition: number): void {
40+
throw new Error('Not implemented');
41+
}
3942
abstract onPopState(fn: LocationChangeListener): void;
4043
abstract getBaseHref(): string;
4144
}
@@ -169,4 +172,8 @@ export class PathLocationStrategy extends LocationStrategy implements OnDestroy
169172
back(): void {
170173
this._platformLocation.back();
171174
}
175+
176+
historyGo(relativePosition: number = 0): void {
177+
this._platformLocation.historyGo?.(relativePosition);
178+
}
172179
}

packages/common/src/location/platform_location.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,10 @@ export abstract class PlatformLocation {
6464
abstract forward(): void;
6565

6666
abstract back(): void;
67+
68+
historyGo?(relativePosition: number): void {
69+
throw new Error('Not implemented');
70+
}
6771
}
6872

6973
export function useBrowserPlatformLocation() {
@@ -189,6 +193,10 @@ export class BrowserPlatformLocation extends PlatformLocation {
189193
this._history.back();
190194
}
191195

196+
historyGo(relativePosition: number = 0): void {
197+
this._history.go(relativePosition);
198+
}
199+
192200
getState(): unknown {
193201
return this._history.state;
194202
}

packages/common/test/location/location_spec.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,54 @@ describe('Location Class', () => {
8181

8282
expect(location.getState()).toEqual({url: 'test1'});
8383
});
84+
85+
it('should work after using forward button', () => {
86+
expect(location.getState()).toBe(null);
87+
88+
location.go('/test1', '', {url: 'test1'});
89+
location.go('/test2', '', {url: 'test2'});
90+
expect(location.getState()).toEqual({url: 'test2'});
91+
92+
location.back();
93+
expect(location.getState()).toEqual({url: 'test1'});
94+
95+
location.forward();
96+
expect(location.getState()).toEqual({url: 'test2'});
97+
});
98+
99+
it('should work after using location.historyGo()', () => {
100+
expect(location.getState()).toBe(null);
101+
102+
location.go('/test1', '', {url: 'test1'});
103+
location.go('/test2', '', {url: 'test2'});
104+
location.go('/test3', '', {url: 'test3'});
105+
expect(location.getState()).toEqual({url: 'test3'});
106+
107+
location.historyGo(-2);
108+
expect(location.getState()).toEqual({url: 'test1'});
109+
110+
location.historyGo(2);
111+
expect(location.getState()).toEqual({url: 'test3'});
112+
113+
location.go('/test3', '', {url: 'test4'});
114+
location.historyGo(0);
115+
expect(location.getState()).toEqual({url: 'test4'});
116+
117+
location.historyGo();
118+
expect(location.getState()).toEqual({url: 'test4'});
119+
120+
// we are testing the behaviour of the `historyGo` method at the moment when the value of
121+
// the relativePosition goes out of bounds.
122+
// The result should be that the locationState does not change.
123+
location.historyGo(100);
124+
expect(location.getState()).toEqual({url: 'test4'});
125+
126+
location.historyGo(-100);
127+
expect(location.getState()).toEqual({url: 'test4'});
128+
129+
location.back();
130+
expect(location.getState()).toEqual({url: 'test3'});
131+
});
84132
});
85133

86134
describe('location.onUrlChange()', () => {

packages/common/testing/src/location_mock.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,16 @@ export class SpyLocation implements Location {
123123
this._subject.emit({'url': this.path(), 'state': this.getState(), 'pop': true});
124124
}
125125
}
126+
127+
historyGo(relativePosition: number = 0): void {
128+
const nextPageIndex = this._historyIndex + relativePosition;
129+
if (nextPageIndex >= 0 && nextPageIndex < this._history.length) {
130+
this._historyIndex = nextPageIndex;
131+
this._subject.emit(
132+
{'url': this.path(), 'state': this.getState(), 'pop': true, 'type': 'popstate'});
133+
}
134+
}
135+
126136
onUrlChange(fn: (url: string, state: unknown) => void) {
127137
this._urlChangeListeners.push(fn);
128138

packages/common/testing/src/mock_platform_location.ts

Lines changed: 42 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export const MOCK_PLATFORM_LOCATION_CONFIG =
105105
export class MockPlatformLocation implements PlatformLocation {
106106
private baseHref: string = '';
107107
private hashUpdate = new Subject<LocationChangeEvent>();
108+
private urlChangeIndex: number = 0;
108109
private urlChanges: {
109110
hostname: string,
110111
protocol: string,
@@ -127,25 +128,25 @@ export class MockPlatformLocation implements PlatformLocation {
127128
}
128129

129130
get hostname() {
130-
return this.urlChanges[0].hostname;
131+
return this.urlChanges[this.urlChangeIndex].hostname;
131132
}
132133
get protocol() {
133-
return this.urlChanges[0].protocol;
134+
return this.urlChanges[this.urlChangeIndex].protocol;
134135
}
135136
get port() {
136-
return this.urlChanges[0].port;
137+
return this.urlChanges[this.urlChangeIndex].port;
137138
}
138139
get pathname() {
139-
return this.urlChanges[0].pathname;
140+
return this.urlChanges[this.urlChangeIndex].pathname;
140141
}
141142
get search() {
142-
return this.urlChanges[0].search;
143+
return this.urlChanges[this.urlChangeIndex].search;
143144
}
144145
get hash() {
145-
return this.urlChanges[0].hash;
146+
return this.urlChanges[this.urlChangeIndex].hash;
146147
}
147148
get state() {
148-
return this.urlChanges[0].state;
149+
return this.urlChanges[this.urlChangeIndex].state;
149150
}
150151

151152

@@ -183,34 +184,59 @@ export class MockPlatformLocation implements PlatformLocation {
183184
replaceState(state: any, title: string, newUrl: string): void {
184185
const {pathname, search, state: parsedState, hash} = this.parseChanges(state, newUrl);
185186

186-
this.urlChanges[0] = {...this.urlChanges[0], pathname, search, hash, state: parsedState};
187+
this.urlChanges[this.urlChangeIndex] =
188+
{...this.urlChanges[this.urlChangeIndex], pathname, search, hash, state: parsedState};
187189
}
188190

189191
pushState(state: any, title: string, newUrl: string): void {
190192
const {pathname, search, state: parsedState, hash} = this.parseChanges(state, newUrl);
191-
this.urlChanges.unshift({...this.urlChanges[0], pathname, search, hash, state: parsedState});
193+
if (this.urlChangeIndex > 0) {
194+
this.urlChanges.splice(this.urlChangeIndex + 1);
195+
}
196+
this.urlChanges.push(
197+
{...this.urlChanges[this.urlChangeIndex], pathname, search, hash, state: parsedState});
198+
this.urlChangeIndex = this.urlChanges.length - 1;
192199
}
193200

194201
forward(): void {
195-
throw new Error('Not implemented');
202+
const oldUrl = this.url;
203+
const oldHash = this.hash;
204+
if (this.urlChangeIndex < this.urlChanges.length) {
205+
this.urlChangeIndex++;
206+
}
207+
this.scheduleHashUpdate(oldHash, oldUrl);
196208
}
197209

198210
back(): void {
199211
const oldUrl = this.url;
200212
const oldHash = this.hash;
201-
this.urlChanges.shift();
202-
const newHash = this.hash;
213+
if (this.urlChangeIndex > 0) {
214+
this.urlChangeIndex--;
215+
}
216+
this.scheduleHashUpdate(oldHash, oldUrl);
217+
}
203218

204-
if (oldHash !== newHash) {
205-
scheduleMicroTask(
206-
() => this.hashUpdate.next(
207-
{type: 'hashchange', state: null, oldUrl, newUrl: this.url} as LocationChangeEvent));
219+
historyGo(relativePosition: number = 0): void {
220+
const oldUrl = this.url;
221+
const oldHash = this.hash;
222+
const nextPageIndex = this.urlChangeIndex + relativePosition;
223+
if (nextPageIndex >= 0 && nextPageIndex < this.urlChanges.length) {
224+
this.urlChangeIndex = nextPageIndex;
208225
}
226+
this.scheduleHashUpdate(oldHash, oldUrl);
209227
}
210228

211229
getState(): unknown {
212230
return this.state;
213231
}
232+
233+
private scheduleHashUpdate(oldHash: string, oldUrl: string) {
234+
if (oldHash !== this.hash) {
235+
scheduleMicroTask(
236+
() => this.hashUpdate.next(
237+
{type: 'hashchange', state: null, oldUrl, newUrl: this.url} as LocationChangeEvent));
238+
}
239+
}
214240
}
215241

216242
export function scheduleMicroTask(cb: () => any) {

0 commit comments

Comments
 (0)