From 3a9cf3f2babd4939f34883bf8704bb76c759ea36 Mon Sep 17 00:00:00 2001 From: Jason Aden Date: Mon, 22 Apr 2019 15:48:27 -0700 Subject: [PATCH] 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 --- packages/common/src/location/location.ts | 19 +++++++++++ .../common/test/location/location_spec.ts | 33 +++++++++++++++++++ packages/common/testing/src/location_mock.ts | 15 +++++++-- 3 files changed, 65 insertions(+), 2 deletions(-) diff --git a/packages/common/src/location/location.ts b/packages/common/src/location/location.ts index d12fb052ed..0a7683c511 100644 --- a/packages/common/src/location/location.ts +++ b/packages/common/src/location/location.ts @@ -57,6 +57,7 @@ export class Location { _platformStrategy: LocationStrategy; /** @internal */ _platformLocation: PlatformLocation; + private urlChangeListeners: any[] = []; constructor(platformStrategy: LocationStrategy, platformLocation: PlatformLocation) { this._platformStrategy = platformStrategy; @@ -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); } /** @@ -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); } /** @@ -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. * diff --git a/packages/common/test/location/location_spec.ts b/packages/common/test/location/location_spec.ts index c11f351cee..97709e4b7c 100644 --- a/packages/common/test/location/location_spec.ts +++ b/packages/common/test/location/location_spec.ts @@ -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); + + })); + + }); }); \ No newline at end of file diff --git a/packages/common/testing/src/location_mock.ts b/packages/common/testing/src/location_mock.ts index e68888798b..d72d38ea31 100644 --- a/packages/common/testing/src/location_mock.ts +++ b/packages/common/testing/src/location_mock.ts @@ -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; @@ -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; } @@ -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,