feat(common): add `historyGo` method to `Location` service (#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 #38890
This commit is contained in:
parent
3a823abcc5
commit
e05a6f3bb3
|
@ -101,6 +101,7 @@ export declare class HashLocationStrategy extends LocationStrategy implements On
|
|||
back(): void;
|
||||
forward(): void;
|
||||
getBaseHref(): string;
|
||||
historyGo(relativePosition?: number): void;
|
||||
ngOnDestroy(): void;
|
||||
onPopState(fn: LocationChangeListener): void;
|
||||
path(includeHash?: boolean): string;
|
||||
|
@ -156,6 +157,7 @@ export declare class Location {
|
|||
forward(): void;
|
||||
getState(): unknown;
|
||||
go(path: string, query?: string, state?: any): void;
|
||||
historyGo(relativePosition?: number): void;
|
||||
isCurrentPathEqualTo(path: string, query?: string): boolean;
|
||||
normalize(url: string): string;
|
||||
onUrlChange(fn: (url: string, state: unknown) => void): void;
|
||||
|
@ -183,6 +185,7 @@ export declare abstract class LocationStrategy {
|
|||
abstract back(): void;
|
||||
abstract forward(): void;
|
||||
abstract getBaseHref(): string;
|
||||
historyGo?(relativePosition: number): void;
|
||||
abstract onPopState(fn: LocationChangeListener): void;
|
||||
abstract path(includeHash?: boolean): string;
|
||||
abstract prepareExternalUrl(internal: string): string;
|
||||
|
@ -330,6 +333,7 @@ export declare class PathLocationStrategy extends LocationStrategy implements On
|
|||
back(): void;
|
||||
forward(): void;
|
||||
getBaseHref(): string;
|
||||
historyGo(relativePosition?: number): void;
|
||||
ngOnDestroy(): void;
|
||||
onPopState(fn: LocationChangeListener): void;
|
||||
path(includeHash?: boolean): string;
|
||||
|
@ -357,6 +361,7 @@ export declare abstract class PlatformLocation {
|
|||
abstract forward(): void;
|
||||
abstract getBaseHrefFromDOM(): string;
|
||||
abstract getState(): unknown;
|
||||
historyGo?(relativePosition: number): void;
|
||||
abstract onHashChange(fn: LocationChangeListener): VoidFunction;
|
||||
abstract onPopState(fn: LocationChangeListener): VoidFunction;
|
||||
abstract pushState(state: any, title: string, url: string): void;
|
||||
|
|
|
@ -33,6 +33,7 @@ export declare class MockPlatformLocation implements PlatformLocation {
|
|||
forward(): void;
|
||||
getBaseHrefFromDOM(): string;
|
||||
getState(): unknown;
|
||||
historyGo(relativePosition?: number): void;
|
||||
onHashChange(fn: LocationChangeListener): VoidFunction;
|
||||
onPopState(fn: LocationChangeListener): VoidFunction;
|
||||
pushState(state: any, title: string, newUrl: string): void;
|
||||
|
@ -50,6 +51,7 @@ export declare class SpyLocation implements Location {
|
|||
forward(): void;
|
||||
getState(): unknown;
|
||||
go(path: string, query?: string, state?: any): void;
|
||||
historyGo(relativePosition?: number): void;
|
||||
isCurrentPathEqualTo(path: string, query?: string): boolean;
|
||||
normalize(url: string): string;
|
||||
onUrlChange(fn: (url: string, state: unknown) => void): void;
|
||||
|
|
|
@ -49,7 +49,7 @@
|
|||
"master": {
|
||||
"uncompressed": {
|
||||
"runtime-es2015": 2289,
|
||||
"main-es2015": 216267,
|
||||
"main-es2015": 216935,
|
||||
"polyfills-es2015": 36723,
|
||||
"5-es2015": 781
|
||||
}
|
||||
|
|
|
@ -98,4 +98,8 @@ export class HashLocationStrategy extends LocationStrategy implements OnDestroy
|
|||
back(): void {
|
||||
this._platformLocation.back();
|
||||
}
|
||||
|
||||
historyGo(relativePosition: number = 0): void {
|
||||
this._platformLocation.historyGo?.(relativePosition);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -188,6 +188,22 @@ export class Location {
|
|||
this._platformStrategy.back();
|
||||
}
|
||||
|
||||
/**
|
||||
* Navigate to a specific page from session history, identified by its relative position to the
|
||||
* current page.
|
||||
*
|
||||
* @param relativePosition Position of the target page in the history relative to the current
|
||||
* page.
|
||||
* A negative value moves backwards, a positive value moves forwards, e.g. `location.historyGo(2)`
|
||||
* moves forward two pages and `location.historyGo(-2)` moves back two pages. When we try to go
|
||||
* beyond what's stored in the history session, we stay in the current page. Same behaviour occurs
|
||||
* when `relativePosition` equals 0.
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/API/History_API#Moving_to_a_specific_point_in_history
|
||||
*/
|
||||
historyGo(relativePosition: number = 0): void {
|
||||
this._platformStrategy.historyGo?.(relativePosition);
|
||||
}
|
||||
|
||||
/**
|
||||
* Registers a URL change listener. Use to catch updates performed by the Angular
|
||||
* framework that are not detectible through "popstate" or "hashchange" events.
|
||||
|
|
|
@ -36,6 +36,9 @@ export abstract class LocationStrategy {
|
|||
abstract replaceState(state: any, title: string, url: string, queryParams: string): void;
|
||||
abstract forward(): void;
|
||||
abstract back(): void;
|
||||
historyGo?(relativePosition: number): void {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
abstract onPopState(fn: LocationChangeListener): void;
|
||||
abstract getBaseHref(): string;
|
||||
}
|
||||
|
@ -169,4 +172,8 @@ export class PathLocationStrategy extends LocationStrategy implements OnDestroy
|
|||
back(): void {
|
||||
this._platformLocation.back();
|
||||
}
|
||||
|
||||
historyGo(relativePosition: number = 0): void {
|
||||
this._platformLocation.historyGo?.(relativePosition);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -64,6 +64,10 @@ export abstract class PlatformLocation {
|
|||
abstract forward(): void;
|
||||
|
||||
abstract back(): void;
|
||||
|
||||
historyGo?(relativePosition: number): void {
|
||||
throw new Error('Not implemented');
|
||||
}
|
||||
}
|
||||
|
||||
export function useBrowserPlatformLocation() {
|
||||
|
@ -189,6 +193,10 @@ export class BrowserPlatformLocation extends PlatformLocation {
|
|||
this._history.back();
|
||||
}
|
||||
|
||||
historyGo(relativePosition: number = 0): void {
|
||||
this._history.go(relativePosition);
|
||||
}
|
||||
|
||||
getState(): unknown {
|
||||
return this._history.state;
|
||||
}
|
||||
|
|
|
@ -81,6 +81,54 @@ describe('Location Class', () => {
|
|||
|
||||
expect(location.getState()).toEqual({url: 'test1'});
|
||||
});
|
||||
|
||||
it('should work after using forward button', () => {
|
||||
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'});
|
||||
|
||||
location.forward();
|
||||
expect(location.getState()).toEqual({url: 'test2'});
|
||||
});
|
||||
|
||||
it('should work after using location.historyGo()', () => {
|
||||
expect(location.getState()).toBe(null);
|
||||
|
||||
location.go('/test1', '', {url: 'test1'});
|
||||
location.go('/test2', '', {url: 'test2'});
|
||||
location.go('/test3', '', {url: 'test3'});
|
||||
expect(location.getState()).toEqual({url: 'test3'});
|
||||
|
||||
location.historyGo(-2);
|
||||
expect(location.getState()).toEqual({url: 'test1'});
|
||||
|
||||
location.historyGo(2);
|
||||
expect(location.getState()).toEqual({url: 'test3'});
|
||||
|
||||
location.go('/test3', '', {url: 'test4'});
|
||||
location.historyGo(0);
|
||||
expect(location.getState()).toEqual({url: 'test4'});
|
||||
|
||||
location.historyGo();
|
||||
expect(location.getState()).toEqual({url: 'test4'});
|
||||
|
||||
// we are testing the behaviour of the `historyGo` method at the moment when the value of
|
||||
// the relativePosition goes out of bounds.
|
||||
// The result should be that the locationState does not change.
|
||||
location.historyGo(100);
|
||||
expect(location.getState()).toEqual({url: 'test4'});
|
||||
|
||||
location.historyGo(-100);
|
||||
expect(location.getState()).toEqual({url: 'test4'});
|
||||
|
||||
location.back();
|
||||
expect(location.getState()).toEqual({url: 'test3'});
|
||||
});
|
||||
});
|
||||
|
||||
describe('location.onUrlChange()', () => {
|
||||
|
|
|
@ -123,6 +123,16 @@ export class SpyLocation implements Location {
|
|||
this._subject.emit({'url': this.path(), 'state': this.getState(), 'pop': true});
|
||||
}
|
||||
}
|
||||
|
||||
historyGo(relativePosition: number = 0): void {
|
||||
const nextPageIndex = this._historyIndex + relativePosition;
|
||||
if (nextPageIndex >= 0 && nextPageIndex < this._history.length) {
|
||||
this._historyIndex = nextPageIndex;
|
||||
this._subject.emit(
|
||||
{'url': this.path(), 'state': this.getState(), 'pop': true, 'type': 'popstate'});
|
||||
}
|
||||
}
|
||||
|
||||
onUrlChange(fn: (url: string, state: unknown) => void) {
|
||||
this._urlChangeListeners.push(fn);
|
||||
|
||||
|
|
|
@ -105,6 +105,7 @@ export const MOCK_PLATFORM_LOCATION_CONFIG =
|
|||
export class MockPlatformLocation implements PlatformLocation {
|
||||
private baseHref: string = '';
|
||||
private hashUpdate = new Subject<LocationChangeEvent>();
|
||||
private urlChangeIndex: number = 0;
|
||||
private urlChanges: {
|
||||
hostname: string,
|
||||
protocol: string,
|
||||
|
@ -127,25 +128,25 @@ export class MockPlatformLocation implements PlatformLocation {
|
|||
}
|
||||
|
||||
get hostname() {
|
||||
return this.urlChanges[0].hostname;
|
||||
return this.urlChanges[this.urlChangeIndex].hostname;
|
||||
}
|
||||
get protocol() {
|
||||
return this.urlChanges[0].protocol;
|
||||
return this.urlChanges[this.urlChangeIndex].protocol;
|
||||
}
|
||||
get port() {
|
||||
return this.urlChanges[0].port;
|
||||
return this.urlChanges[this.urlChangeIndex].port;
|
||||
}
|
||||
get pathname() {
|
||||
return this.urlChanges[0].pathname;
|
||||
return this.urlChanges[this.urlChangeIndex].pathname;
|
||||
}
|
||||
get search() {
|
||||
return this.urlChanges[0].search;
|
||||
return this.urlChanges[this.urlChangeIndex].search;
|
||||
}
|
||||
get hash() {
|
||||
return this.urlChanges[0].hash;
|
||||
return this.urlChanges[this.urlChangeIndex].hash;
|
||||
}
|
||||
get state() {
|
||||
return this.urlChanges[0].state;
|
||||
return this.urlChanges[this.urlChangeIndex].state;
|
||||
}
|
||||
|
||||
|
||||
|
@ -183,34 +184,59 @@ export class MockPlatformLocation implements PlatformLocation {
|
|||
replaceState(state: any, title: string, newUrl: string): void {
|
||||
const {pathname, search, state: parsedState, hash} = this.parseChanges(state, newUrl);
|
||||
|
||||
this.urlChanges[0] = {...this.urlChanges[0], pathname, search, hash, state: parsedState};
|
||||
this.urlChanges[this.urlChangeIndex] =
|
||||
{...this.urlChanges[this.urlChangeIndex], pathname, search, hash, state: parsedState};
|
||||
}
|
||||
|
||||
pushState(state: any, title: string, newUrl: string): void {
|
||||
const {pathname, search, state: parsedState, hash} = this.parseChanges(state, newUrl);
|
||||
this.urlChanges.unshift({...this.urlChanges[0], pathname, search, hash, state: parsedState});
|
||||
if (this.urlChangeIndex > 0) {
|
||||
this.urlChanges.splice(this.urlChangeIndex + 1);
|
||||
}
|
||||
this.urlChanges.push(
|
||||
{...this.urlChanges[this.urlChangeIndex], pathname, search, hash, state: parsedState});
|
||||
this.urlChangeIndex = this.urlChanges.length - 1;
|
||||
}
|
||||
|
||||
forward(): void {
|
||||
throw new Error('Not implemented');
|
||||
const oldUrl = this.url;
|
||||
const oldHash = this.hash;
|
||||
if (this.urlChangeIndex < this.urlChanges.length) {
|
||||
this.urlChangeIndex++;
|
||||
}
|
||||
this.scheduleHashUpdate(oldHash, oldUrl);
|
||||
}
|
||||
|
||||
back(): void {
|
||||
const oldUrl = this.url;
|
||||
const oldHash = this.hash;
|
||||
this.urlChanges.shift();
|
||||
const newHash = this.hash;
|
||||
|
||||
if (oldHash !== newHash) {
|
||||
scheduleMicroTask(
|
||||
() => this.hashUpdate.next(
|
||||
{type: 'hashchange', state: null, oldUrl, newUrl: this.url} as LocationChangeEvent));
|
||||
if (this.urlChangeIndex > 0) {
|
||||
this.urlChangeIndex--;
|
||||
}
|
||||
this.scheduleHashUpdate(oldHash, oldUrl);
|
||||
}
|
||||
|
||||
historyGo(relativePosition: number = 0): void {
|
||||
const oldUrl = this.url;
|
||||
const oldHash = this.hash;
|
||||
const nextPageIndex = this.urlChangeIndex + relativePosition;
|
||||
if (nextPageIndex >= 0 && nextPageIndex < this.urlChanges.length) {
|
||||
this.urlChangeIndex = nextPageIndex;
|
||||
}
|
||||
this.scheduleHashUpdate(oldHash, oldUrl);
|
||||
}
|
||||
|
||||
getState(): unknown {
|
||||
return this.state;
|
||||
}
|
||||
|
||||
private scheduleHashUpdate(oldHash: string, oldUrl: string) {
|
||||
if (oldHash !== this.hash) {
|
||||
scheduleMicroTask(
|
||||
() => this.hashUpdate.next(
|
||||
{type: 'hashchange', state: null, oldUrl, newUrl: this.url} as LocationChangeEvent));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function scheduleMicroTask(cb: () => any) {
|
||||
|
|
|
@ -187,6 +187,47 @@ describe('Integration', () => {
|
|||
expect(navigation.extras.state).toEqual(state);
|
||||
})));
|
||||
|
||||
it('should navigate correctly when using `Location#historyGo',
|
||||
fakeAsync(inject([Router, Location], (router: Router, location: SpyLocation) => {
|
||||
router.resetConfig([
|
||||
{path: 'first', component: SimpleCmp},
|
||||
{path: 'second', component: SimpleCmp},
|
||||
|
||||
]);
|
||||
|
||||
createRoot(router, RootCmp);
|
||||
|
||||
router.navigateByUrl('/first');
|
||||
tick();
|
||||
router.navigateByUrl('/second');
|
||||
tick();
|
||||
expect(router.url).toEqual('/second');
|
||||
|
||||
location.historyGo(-1);
|
||||
tick();
|
||||
expect(router.url).toEqual('/first');
|
||||
|
||||
location.historyGo(1);
|
||||
tick();
|
||||
expect(router.url).toEqual('/second');
|
||||
|
||||
location.historyGo(-100);
|
||||
tick();
|
||||
expect(router.url).toEqual('/second');
|
||||
|
||||
location.historyGo(100);
|
||||
tick();
|
||||
expect(router.url).toEqual('/second');
|
||||
|
||||
location.historyGo(0);
|
||||
tick();
|
||||
expect(router.url).toEqual('/second');
|
||||
|
||||
location.historyGo();
|
||||
tick();
|
||||
expect(router.url).toEqual('/second');
|
||||
})));
|
||||
|
||||
it('should not error if state is not {[key: string]: any}',
|
||||
fakeAsync(inject([Router, Location], (router: Router, location: SpyLocation) => {
|
||||
router.resetConfig([
|
||||
|
|
Loading…
Reference in New Issue