Skip to content

Commit

Permalink
feat(common): add ability to track all location changes (#30055)
Browse files Browse the repository at this point in the history
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
  • Loading branch information
jasonaden authored and benlesh committed Apr 24, 2019
1 parent 152d99e commit 3a9cf3f
Show file tree
Hide file tree
Showing 3 changed files with 65 additions and 2 deletions.
19 changes: 19 additions & 0 deletions packages/common/src/location/location.ts
Expand Up @@ -57,6 +57,7 @@ export class Location {
_platformStrategy: LocationStrategy;
/** @internal */
_platformLocation: PlatformLocation;
private urlChangeListeners: any[] = [];

constructor(platformStrategy: LocationStrategy, platformLocation: PlatformLocation) {
this._platformStrategy = platformStrategy;
Expand Down Expand Up @@ -146,6 +147,8 @@ export class Location {
*/
go(path: string, query: string = '', state: any = null): void {
this._platformStrategy.pushState(state, '', path, query);
this.notifyUrlChangeListeners(
this.prepareExternalUrl(path + Location.normalizeQueryParams(query)), state);
}

/**
Expand All @@ -158,6 +161,8 @@ export class Location {
*/
replaceState(path: string, query: string = '', state: any = null): void {
this._platformStrategy.replaceState(state, '', path, query);
this.notifyUrlChangeListeners(
this.prepareExternalUrl(path + Location.normalizeQueryParams(query)), state);
}

/**
Expand All @@ -170,6 +175,20 @@ export class Location {
*/
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.
*
Expand Down
33 changes: 33 additions & 0 deletions packages/common/test/location/location_spec.ts
Expand Up @@ -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);

}));

});
});
15 changes: 13 additions & 2 deletions packages/common/testing/src/location_mock.ts
Expand Up @@ -6,18 +6,23 @@
* 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 {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.
*
* @publicApi
*/
@Injectable()
export class SpyLocation implements Location {
export class SpyLocation extends Location {
urlChanges: string[] = [];
private _history: LocationState[] = [new LocationState('', '', null)];
private _historyIndex: number = 0;
Expand All @@ -27,6 +32,8 @@ export class SpyLocation implements Location {
_baseHref: string = '';
/** @internal */
_platformStrategy: LocationStrategy = null !;
/** @internal */
_platformLocation: PlatformLocation = null !;

setInitialPath(url: string) { this._history[this._historyIndex].path = url; }

Expand Down Expand Up @@ -110,6 +117,10 @@ export class SpyLocation implements Location {
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(
onNext: (value: any) => void, onThrow?: ((error: any) => void)|null,
Expand Down

0 comments on commit 3a9cf3f

Please sign in to comment.