Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(common): add historyGo method to Location service #38890

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions goldens/public-api/common/common.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down
2 changes: 2 additions & 0 deletions goldens/public-api/common/testing/testing.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down
2 changes: 1 addition & 1 deletion goldens/size-tracking/integration-payloads.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
"master": {
"uncompressed": {
"runtime-es2015": 2289,
"main-es2015": 216267,
"main-es2015": 216935,
"polyfills-es2015": 36723,
"5-es2015": 781
}
Expand Down
4 changes: 4 additions & 0 deletions packages/common/src/location/hash_location_strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
16 changes: 16 additions & 0 deletions packages/common/src/location/location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions packages/common/src/location/location_strategy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down Expand Up @@ -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);
}
}
8 changes: 8 additions & 0 deletions packages/common/src/location/platform_location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down Expand Up @@ -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;
}
Expand Down
48 changes: 48 additions & 0 deletions packages/common/test/location/location_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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'});
aahmedayed marked this conversation as resolved.
Show resolved Hide resolved

location.historyGo(-100);
expect(location.getState()).toEqual({url: 'test4'});

location.back();
expect(location.getState()).toEqual({url: 'test3'});
});
});

describe('location.onUrlChange()', () => {
Expand Down
10 changes: 10 additions & 0 deletions packages/common/testing/src/location_mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
IgorMinar marked this conversation as resolved.
Show resolved Hide resolved
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);

Expand Down
58 changes: 42 additions & 16 deletions packages/common/testing/src/mock_platform_location.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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;
}


Expand Down Expand Up @@ -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 (this.urlChangeIndex > 0) {
this.urlChangeIndex--;
}
this.scheduleHashUpdate(oldHash, oldUrl);
}

if (oldHash !== newHash) {
scheduleMicroTask(
() => this.hashUpdate.next(
{type: 'hashchange', state: null, oldUrl, newUrl: this.url} as LocationChangeEvent));
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) {
Expand Down
41 changes: 41 additions & 0 deletions packages/router/test/integration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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([
Expand Down