feat(common): add ability to track all location changes (#30055)
This feature adds an `onUrlChange` to Angular's `Location` class. This is useful to track all updates coming from anywhere in the framework. Without this method, it's difficult (or impossible) to track updates run through `location.go()` or `location.replaceState()` as the browser doesn't publish events when `history.pushState()` or `.replaceState()` are run. PR Close #30055
This commit is contained in:
parent
152d99eef0
commit
3a9cf3f2ba
|
@ -57,6 +57,7 @@ export class Location {
|
||||||
_platformStrategy: LocationStrategy;
|
_platformStrategy: LocationStrategy;
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_platformLocation: PlatformLocation;
|
_platformLocation: PlatformLocation;
|
||||||
|
private urlChangeListeners: any[] = [];
|
||||||
|
|
||||||
constructor(platformStrategy: LocationStrategy, platformLocation: PlatformLocation) {
|
constructor(platformStrategy: LocationStrategy, platformLocation: PlatformLocation) {
|
||||||
this._platformStrategy = platformStrategy;
|
this._platformStrategy = platformStrategy;
|
||||||
|
@ -146,6 +147,8 @@ export class Location {
|
||||||
*/
|
*/
|
||||||
go(path: string, query: string = '', state: any = null): void {
|
go(path: string, query: string = '', state: any = null): void {
|
||||||
this._platformStrategy.pushState(state, '', path, query);
|
this._platformStrategy.pushState(state, '', path, query);
|
||||||
|
this.notifyUrlChangeListeners(
|
||||||
|
this.prepareExternalUrl(path + Location.normalizeQueryParams(query)), state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -158,6 +161,8 @@ export class Location {
|
||||||
*/
|
*/
|
||||||
replaceState(path: string, query: string = '', state: any = null): void {
|
replaceState(path: string, query: string = '', state: any = null): void {
|
||||||
this._platformStrategy.replaceState(state, '', path, query);
|
this._platformStrategy.replaceState(state, '', path, query);
|
||||||
|
this.notifyUrlChangeListeners(
|
||||||
|
this.prepareExternalUrl(path + Location.normalizeQueryParams(query)), state);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -170,6 +175,20 @@ export class Location {
|
||||||
*/
|
*/
|
||||||
back(): void { this._platformStrategy.back(); }
|
back(): void { this._platformStrategy.back(); }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Register URL change listeners. This API can be used to catch updates performed by the Angular
|
||||||
|
* framework. These are not detectible through "popstate" or "hashchange" events.
|
||||||
|
*/
|
||||||
|
onUrlChange(fn: (url: string, state: unknown) => void) {
|
||||||
|
this.urlChangeListeners.push(fn);
|
||||||
|
this.subscribe(v => { this.notifyUrlChangeListeners(v.url, v.state); });
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
private notifyUrlChangeListeners(url: string = '', state: unknown) {
|
||||||
|
this.urlChangeListeners.forEach(fn => fn(url, state));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Subscribe to the platform's `popState` events.
|
* Subscribe to the platform's `popState` events.
|
||||||
*
|
*
|
||||||
|
|
|
@ -77,4 +77,37 @@ describe('Location Class', () => {
|
||||||
}));
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('location.onUrlChange()', () => {
|
||||||
|
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 have onUrlChange method', inject([Location], (location: Location) => {
|
||||||
|
expect(typeof location.onUrlChange).toBe('function');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should add registered functions to urlChangeListeners', inject([Location], (location: Location) => {
|
||||||
|
|
||||||
|
function changeListener(url: string, state: unknown) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
expect((location as any).urlChangeListeners.length).toBe(0);
|
||||||
|
|
||||||
|
location.onUrlChange(changeListener);
|
||||||
|
|
||||||
|
expect((location as any).urlChangeListeners.length).toBe(1);
|
||||||
|
expect((location as any).urlChangeListeners[0]).toEqual(changeListener);
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
|
@ -6,18 +6,23 @@
|
||||||
* found in the LICENSE file at https://angular.io/license
|
* found in the LICENSE file at https://angular.io/license
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Location, LocationStrategy} from '@angular/common';
|
import {Location, LocationStrategy, PlatformLocation} from '@angular/common';
|
||||||
import {EventEmitter, Injectable} from '@angular/core';
|
import {EventEmitter, Injectable} from '@angular/core';
|
||||||
import {SubscriptionLike} from 'rxjs';
|
import {SubscriptionLike} from 'rxjs';
|
||||||
|
|
||||||
|
|
||||||
|
const urlChangeListeners: ((url: string, state: unknown) => void)[] = [];
|
||||||
|
function notifyUrlChangeListeners(url: string = '', state: unknown) {
|
||||||
|
urlChangeListeners.forEach(fn => fn(url, state));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A spy for {@link Location} that allows tests to fire simulated location events.
|
* A spy for {@link Location} that allows tests to fire simulated location events.
|
||||||
*
|
*
|
||||||
* @publicApi
|
* @publicApi
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SpyLocation implements Location {
|
export class SpyLocation extends Location {
|
||||||
urlChanges: string[] = [];
|
urlChanges: string[] = [];
|
||||||
private _history: LocationState[] = [new LocationState('', '', null)];
|
private _history: LocationState[] = [new LocationState('', '', null)];
|
||||||
private _historyIndex: number = 0;
|
private _historyIndex: number = 0;
|
||||||
|
@ -27,6 +32,8 @@ export class SpyLocation implements Location {
|
||||||
_baseHref: string = '';
|
_baseHref: string = '';
|
||||||
/** @internal */
|
/** @internal */
|
||||||
_platformStrategy: LocationStrategy = null !;
|
_platformStrategy: LocationStrategy = null !;
|
||||||
|
/** @internal */
|
||||||
|
_platformLocation: PlatformLocation = null !;
|
||||||
|
|
||||||
setInitialPath(url: string) { this._history[this._historyIndex].path = url; }
|
setInitialPath(url: string) { this._history[this._historyIndex].path = url; }
|
||||||
|
|
||||||
|
@ -110,6 +117,10 @@ export class SpyLocation implements Location {
|
||||||
this._subject.emit({'url': this.path(), 'state': this.getState(), 'pop': true});
|
this._subject.emit({'url': this.path(), 'state': this.getState(), 'pop': true});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
onUrlChange(fn: (url: string, state: unknown) => void) {
|
||||||
|
urlChangeListeners.push(fn);
|
||||||
|
this.subscribe(v => { notifyUrlChangeListeners(v.url, v.state); });
|
||||||
|
}
|
||||||
|
|
||||||
subscribe(
|
subscribe(
|
||||||
onNext: (value: any) => void, onThrow?: ((error: any) => void)|null,
|
onNext: (value: any) => void, onThrow?: ((error: any) => void)|null,
|
||||||
|
|
Loading…
Reference in New Issue