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
This commit is contained in:
parent
077809398c
commit
1aff524b63
|
@ -39,9 +39,16 @@ export class $locationShim {
|
||||||
private $$search: any = '';
|
private $$search: any = '';
|
||||||
private $$hash: string = '';
|
private $$hash: string = '';
|
||||||
private $$state: unknown;
|
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;
|
private cachedState: unknown = null;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
$injector: any, private location: Location, private platformLocation: PlatformLocation,
|
$injector: any, private location: Location, private platformLocation: PlatformLocation,
|
||||||
private urlCodec: UrlCodec, private locationStrategy: LocationStrategy) {
|
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) {
|
$$parse(url: string) {
|
||||||
let pathUrl: string|undefined;
|
let pathUrl: string|undefined;
|
||||||
if (url.startsWith('/')) {
|
if (url.startsWith('/')) {
|
||||||
|
@ -363,6 +396,7 @@ export class $locationShim {
|
||||||
// state object; this makes possible quick checking if the state changed in the digest
|
// state object; this makes possible quick checking if the state changed in the digest
|
||||||
// loop. Checking deep equality would be too expensive.
|
// loop. Checking deep equality would be too expensive.
|
||||||
this.$$state = this.browserState();
|
this.$$state = this.browserState();
|
||||||
|
this.$$notifyChangeListeners(url, state, oldUrl, oldState);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Restore old values if pushState fails
|
// Restore old values if pushState fails
|
||||||
this.url(oldUrl);
|
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) {
|
function parseLinkAndReturn(location: $locationShim, toUrl: string, relHref?: string) {
|
||||||
const resetUrl = location.$$parseLinkUrl(toUrl, relHref);
|
const resetUrl = location.$$parseLinkUrl(toUrl, relHref);
|
||||||
return resetUrl && location.absUrl() || undefined;
|
return resetUrl && location.absUrl() || undefined;
|
||||||
|
|
|
@ -6,6 +6,7 @@ export declare class $locationShim {
|
||||||
hash(hash: string | number | null): this;
|
hash(hash: string | number | null): this;
|
||||||
hash(): string;
|
hash(): string;
|
||||||
host(): string;
|
host(): string;
|
||||||
|
onChange(fn: (url: string, state: unknown, oldUrl: string, oldState: unknown) => void, err?: (e: Error) => void): void;
|
||||||
path(): string;
|
path(): string;
|
||||||
path(path: string | number | null): this;
|
path(path: string | number | null): this;
|
||||||
port(): number | null;
|
port(): number | null;
|
||||||
|
|
Loading…
Reference in New Issue