Skip to content

Commit 8022d36

Browse files
committedMay 16, 2019
feat(common): add ability to watch for AngularJS URL updates through onUrlChange hook (#30466)
The LocationShim (replacement for `$location`) was added to centralize dealing with the browser URL. Additionally, an `onUrlChange` method was added to Angular's Location service. This PR adds a corresponding method to the LocationShim so updates from AngularJS can be tracked in Angular. PR Close #30466
1 parent 581336a commit 8022d36

File tree

3 files changed

+116
-0
lines changed

3 files changed

+116
-0
lines changed
 

‎packages/common/upgrade/src/location_shim.ts

+34
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,16 @@ export class $locationShim {
3939
private $$search: any = '';
4040
private $$hash: string = '';
4141
private $$state: unknown;
42+
private $$changeListeners: [
43+
((url: string, state: unknown, oldUrl: string, oldState: unknown, err?: (e: Error) => void) =>
44+
void),
45+
(e: Error) => void
46+
][] = [];
4247

4348
private cachedState: unknown = null;
4449

50+
51+
4552
constructor(
4653
$injector: any, private location: Location, private platformLocation: PlatformLocation,
4754
private urlCodec: UrlCodec, private locationStrategy: LocationStrategy) {
@@ -313,6 +320,32 @@ export class $locationShim {
313320
}
314321
}
315322

323+
/**
324+
* Register URL change listeners. This API can be used to catch updates performed by the
325+
* AngularJS framework. These changes are a subset of the `$locationChangeStart/Success` events
326+
* as those events fire when AngularJS updates it's internally referenced version of the browser
327+
* URL. It's possible for `$locationChange` events to happen, but for the browser URL
328+
* (window.location) to remain unchanged. This `onChange` callback will fire only when AngularJS
329+
* actually updates the browser URL (window.location).
330+
*/
331+
onChange(
332+
fn: (url: string, state: unknown, oldUrl: string, oldState: unknown) => void,
333+
err: (e: Error) => void = (e: Error) => {}) {
334+
this.$$changeListeners.push([fn, err]);
335+
}
336+
337+
/** @internal */
338+
$$notifyChangeListeners(
339+
url: string = '', state: unknown, oldUrl: string = '', oldState: unknown) {
340+
this.$$changeListeners.forEach(([fn, err]) => {
341+
try {
342+
fn(url, state, oldUrl, oldState);
343+
} catch (e) {
344+
err(e);
345+
}
346+
});
347+
}
348+
316349
$$parse(url: string) {
317350
let pathUrl: string|undefined;
318351
if (url.startsWith('/')) {
@@ -363,6 +396,7 @@ export class $locationShim {
363396
// state object; this makes possible quick checking if the state changed in the digest
364397
// loop. Checking deep equality would be too expensive.
365398
this.$$state = this.browserState();
399+
this.$$notifyChangeListeners(url, state, oldUrl, oldState);
366400
} catch (e) {
367401
// Restore old values if pushState fails
368402
this.url(oldUrl);

‎packages/common/upgrade/test/upgrade.spec.ts

+81
Original file line numberDiff line numberDiff line change
@@ -624,6 +624,87 @@ describe('New URL Parsing', () => {
624624
});
625625
});
626626

627+
describe('$location.onChange()', () => {
628+
629+
let $location: $locationShim;
630+
let upgradeModule: UpgradeModule;
631+
632+
beforeEach(() => {
633+
TestBed.configureTestingModule({
634+
imports: [
635+
CommonModule,
636+
LocationUpgradeTestModule.config({useHash: false, startUrl: 'http://host.com/'}),
637+
],
638+
providers: [UpgradeModule],
639+
});
640+
641+
upgradeModule = TestBed.get(UpgradeModule);
642+
upgradeModule.$injector = {get: injectorFactory()};
643+
});
644+
645+
beforeEach(inject([$locationShim], (loc: $locationShim) => { $location = loc; }));
646+
647+
it('should have onChange method', () => { expect(typeof $location.onChange).toBe('function'); });
648+
649+
it('should add registered functions to changeListeners', () => {
650+
651+
function changeListener(url: string, state: unknown) { return undefined; }
652+
function errorHandler(e: Error) {}
653+
654+
expect(($location as any).$$changeListeners.length).toBe(0);
655+
656+
$location.onChange(changeListener, errorHandler);
657+
658+
expect(($location as any).$$changeListeners.length).toBe(1);
659+
expect(($location as any).$$changeListeners[0][0]).toEqual(changeListener);
660+
expect(($location as any).$$changeListeners[0][1]).toEqual(errorHandler);
661+
});
662+
663+
it('should call changeListeners when URL is updated', () => {
664+
665+
const onChangeVals =
666+
{url: 'url', state: 'state' as unknown, oldUrl: 'oldUrl', oldState: 'oldState' as unknown};
667+
668+
function changeListener(url: string, state: unknown, oldUrl: string, oldState: unknown) {
669+
onChangeVals.url = url;
670+
onChangeVals.state = state;
671+
onChangeVals.oldUrl = oldUrl;
672+
onChangeVals.oldState = oldState;
673+
}
674+
675+
$location.onChange(changeListener);
676+
677+
// Mock out setting browserUrl
678+
($location as any).browserUrl = (url: string, replace: boolean, state: unknown) => {};
679+
680+
const newState = {foo: 'bar'};
681+
($location as any).setBrowserUrlWithFallback('/newUrl', false, newState);
682+
expect(onChangeVals.url).toBe('/newUrl');
683+
expect(onChangeVals.state).toBe(newState);
684+
expect(onChangeVals.oldUrl).toBe('/');
685+
expect(onChangeVals.oldState).toBe(null);
686+
});
687+
688+
it('should call forward errors to error handler', () => {
689+
690+
let error !: Error;
691+
692+
function changeListener(url: string, state: unknown, oldUrl: string, oldState: unknown) {
693+
throw new Error('Handle error');
694+
}
695+
function errorHandler(e: Error) { error = e; }
696+
697+
$location.onChange(changeListener, errorHandler);
698+
699+
// Mock out setting browserUrl
700+
($location as any).browserUrl = (url: string, replace: boolean, state: unknown) => {};
701+
702+
($location as any).setBrowserUrlWithFallback('/newUrl');
703+
expect(error.message).toBe('Handle error');
704+
});
705+
706+
});
707+
627708
function parseLinkAndReturn(location: $locationShim, toUrl: string, relHref?: string) {
628709
const resetUrl = location.$$parseLinkUrl(toUrl, relHref);
629710
return resetUrl && location.absUrl() || undefined;

‎tools/public_api_guard/common/upgrade.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export declare class $locationShim {
66
hash(hash: string | number | null): this;
77
hash(): string;
88
host(): string;
9+
onChange(fn: (url: string, state: unknown, oldUrl: string, oldState: unknown) => void, err?: (e: Error) => void): void;
910
path(): string;
1011
path(path: string | number | null): this;
1112
port(): number | null;

0 commit comments

Comments
 (0)
Please sign in to comment.