Skip to content
Permalink
Browse files

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
  • Loading branch information...
jasonaden committed May 14, 2019
1 parent 0778093 commit 1aff524b63fd9108ae2ca47fe1d7dc61155fb7e1
@@ -39,9 +39,16 @@ export class $locationShim {
private $$search: any = '';
private $$hash: string = '';
private $$state: unknown;
private $$changeListeners: [
((url: string, state: unknown, oldUrl: string, oldState: unknown, err?: (e: Error) => void) =>
void),
(e: Error) => void
][] = [];

private cachedState: unknown = null;



constructor(
$injector: any, private location: Location, private platformLocation: PlatformLocation,
private urlCodec: UrlCodec, private locationStrategy: LocationStrategy) {
@@ -313,6 +320,32 @@ export class $locationShim {
}
}

/**
* Register URL change listeners. This API can be used to catch updates performed by the
* AngularJS framework. These changes are a subset of the `$locationChangeStart/Success` events
* as those events fire when AngularJS updates it's internally referenced version of the browser
* URL. It's possible for `$locationChange` events to happen, but for the browser URL
* (window.location) to remain unchanged. This `onChange` callback will fire only when AngularJS
* actually updates the browser URL (window.location).
*/
onChange(
fn: (url: string, state: unknown, oldUrl: string, oldState: unknown) => void,
err: (e: Error) => void = (e: Error) => {}) {
this.$$changeListeners.push([fn, err]);
}

/** @internal */
$$notifyChangeListeners(
url: string = '', state: unknown, oldUrl: string = '', oldState: unknown) {
this.$$changeListeners.forEach(([fn, err]) => {
try {
fn(url, state, oldUrl, oldState);
} catch (e) {
err(e);
}
});
}

$$parse(url: string) {
let pathUrl: string|undefined;
if (url.startsWith('/')) {
@@ -363,6 +396,7 @@ export class $locationShim {
// state object; this makes possible quick checking if the state changed in the digest
// loop. Checking deep equality would be too expensive.
this.$$state = this.browserState();
this.$$notifyChangeListeners(url, state, oldUrl, oldState);
} catch (e) {
// Restore old values if pushState fails
this.url(oldUrl);
@@ -624,6 +624,87 @@ describe('New URL Parsing', () => {
});
});

describe('$location.onChange()', () => {

let $location: $locationShim;
let upgradeModule: UpgradeModule;

beforeEach(() => {
TestBed.configureTestingModule({
imports: [
CommonModule,
LocationUpgradeTestModule.config({useHash: false, startUrl: 'http://host.com/'}),
],
providers: [UpgradeModule],
});

upgradeModule = TestBed.get(UpgradeModule);
upgradeModule.$injector = {get: injectorFactory()};
});

beforeEach(inject([$locationShim], (loc: $locationShim) => { $location = loc; }));

it('should have onChange method', () => { expect(typeof $location.onChange).toBe('function'); });

it('should add registered functions to changeListeners', () => {

function changeListener(url: string, state: unknown) { return undefined; }
function errorHandler(e: Error) {}

expect(($location as any).$$changeListeners.length).toBe(0);

$location.onChange(changeListener, errorHandler);

expect(($location as any).$$changeListeners.length).toBe(1);
expect(($location as any).$$changeListeners[0][0]).toEqual(changeListener);
expect(($location as any).$$changeListeners[0][1]).toEqual(errorHandler);
});

it('should call changeListeners when URL is updated', () => {

const onChangeVals =
{url: 'url', state: 'state' as unknown, oldUrl: 'oldUrl', oldState: 'oldState' as unknown};

function changeListener(url: string, state: unknown, oldUrl: string, oldState: unknown) {
onChangeVals.url = url;
onChangeVals.state = state;
onChangeVals.oldUrl = oldUrl;
onChangeVals.oldState = oldState;
}

$location.onChange(changeListener);

// Mock out setting browserUrl
($location as any).browserUrl = (url: string, replace: boolean, state: unknown) => {};

const newState = {foo: 'bar'};
($location as any).setBrowserUrlWithFallback('/newUrl', false, newState);
expect(onChangeVals.url).toBe('/newUrl');
expect(onChangeVals.state).toBe(newState);
expect(onChangeVals.oldUrl).toBe('/');
expect(onChangeVals.oldState).toBe(null);
});

it('should call forward errors to error handler', () => {

let error !: Error;

function changeListener(url: string, state: unknown, oldUrl: string, oldState: unknown) {
throw new Error('Handle error');
}
function errorHandler(e: Error) { error = e; }

$location.onChange(changeListener, errorHandler);

// Mock out setting browserUrl
($location as any).browserUrl = (url: string, replace: boolean, state: unknown) => {};

($location as any).setBrowserUrlWithFallback('/newUrl');
expect(error.message).toBe('Handle error');
});

});

function parseLinkAndReturn(location: $locationShim, toUrl: string, relHref?: string) {
const resetUrl = location.$$parseLinkUrl(toUrl, relHref);
return resetUrl && location.absUrl() || undefined;
@@ -6,6 +6,7 @@ export declare class $locationShim {
hash(hash: string | number | null): this;
hash(): string;
host(): string;
onChange(fn: (url: string, state: unknown, oldUrl: string, oldState: unknown) => void, err?: (e: Error) => void): void;
path(): string;
path(path: string | number | null): this;
port(): number | null;

0 comments on commit 1aff524

Please sign in to comment.
You can’t perform that action at this time.