fix(platform-browser): ensure that Hammer loader is called only once (#40911)

Currently, the function that is provided through `HAMMER_LOADER` is called the
same number of times as the `HammerGesturesPlugin.addEventListener` method is called
(until the Hammer is loaded).

This commit adds a class property in which the loader call is saved, thereby
preventing multiple calls to the loader function.

PR Close #25995

PR Close #40911
This commit is contained in:
arturovt 2021-02-18 20:53:57 +02:00 committed by atscott
parent aed2782c4a
commit 228b5f73b1
2 changed files with 37 additions and 13 deletions

View File

@ -162,6 +162,8 @@ export class HammerGestureConfig {
*/ */
@Injectable() @Injectable()
export class HammerGesturesPlugin extends EventManagerPlugin { export class HammerGesturesPlugin extends EventManagerPlugin {
private _loaderPromise: Promise<void>|null = null;
constructor( constructor(
@Inject(DOCUMENT) doc: any, @Inject(DOCUMENT) doc: any,
@Inject(HAMMER_GESTURE_CONFIG) private _config: HammerGestureConfig, private console: Console, @Inject(HAMMER_GESTURE_CONFIG) private _config: HammerGestureConfig, private console: Console,
@ -175,9 +177,11 @@ export class HammerGesturesPlugin extends EventManagerPlugin {
} }
if (!(window as any).Hammer && !this.loader) { if (!(window as any).Hammer && !this.loader) {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
this.console.warn( this.console.warn(
`The "${eventName}" event cannot be bound because Hammer.JS is not ` + `The "${eventName}" event cannot be bound because Hammer.JS is not ` +
`loaded and no custom loader has been specified.`); `loaded and no custom loader has been specified.`);
}
return false; return false;
} }
@ -191,6 +195,7 @@ export class HammerGesturesPlugin extends EventManagerPlugin {
// If Hammer is not present but a loader is specified, we defer adding the event listener // If Hammer is not present but a loader is specified, we defer adding the event listener
// until Hammer is loaded. // until Hammer is loaded.
if (!(window as any).Hammer && this.loader) { if (!(window as any).Hammer && this.loader) {
this._loaderPromise = this._loaderPromise || this.loader();
// This `addEventListener` method returns a function to remove the added listener. // This `addEventListener` method returns a function to remove the added listener.
// Until Hammer is loaded, the returned function needs to *cancel* the registration rather // Until Hammer is loaded, the returned function needs to *cancel* the registration rather
// than remove anything. // than remove anything.
@ -199,12 +204,14 @@ export class HammerGesturesPlugin extends EventManagerPlugin {
cancelRegistration = true; cancelRegistration = true;
}; };
this.loader() this._loaderPromise
.then(() => { .then(() => {
// If Hammer isn't actually loaded when the custom loader resolves, give up. // If Hammer isn't actually loaded when the custom loader resolves, give up.
if (!(window as any).Hammer) { if (!(window as any).Hammer) {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
this.console.warn( this.console.warn(
`The custom HAMMER_LOADER completed, but Hammer.JS is not present.`); `The custom HAMMER_LOADER completed, but Hammer.JS is not present.`);
}
deregister = () => {}; deregister = () => {};
return; return;
} }
@ -216,9 +223,11 @@ export class HammerGesturesPlugin extends EventManagerPlugin {
} }
}) })
.catch(() => { .catch(() => {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
this.console.warn( this.console.warn(
`The "${eventName}" event cannot be bound because the custom ` + `The "${eventName}" event cannot be bound because the custom ` +
`Hammer.JS loader failed.`); `Hammer.JS loader failed.`);
}
deregister = () => {}; deregister = () => {};
}); });

View File

@ -67,6 +67,8 @@ import {HammerGestureConfig, HammerGesturesPlugin,} from '@angular/platform-brow
ngZone = z; ngZone = z;
})); }));
let loaderCalled = 0;
beforeEach(() => { beforeEach(() => {
originalHammerGlobal = (window as any).Hammer; originalHammerGlobal = (window as any).Hammer;
(window as any).Hammer = undefined; (window as any).Hammer = undefined;
@ -76,10 +78,13 @@ import {HammerGestureConfig, HammerGesturesPlugin,} from '@angular/platform-brow
off: jasmine.createSpy('mc.off'), off: jasmine.createSpy('mc.off'),
}; };
loader = () => new Promise((resolve, reject) => { loader = () => {
loaderCalled++;
return new Promise((resolve, reject) => {
resolveLoader = resolve; resolveLoader = resolve;
failLoader = reject; failLoader = reject;
}); });
};
// Make the hammer config return a fake hammer instance // Make the hammer config return a fake hammer instance
const hammerConfig = new HammerGestureConfig(); const hammerConfig = new HammerGestureConfig();
@ -95,9 +100,19 @@ import {HammerGestureConfig, HammerGesturesPlugin,} from '@angular/platform-brow
}); });
afterEach(() => { afterEach(() => {
loaderCalled = 0;
(window as any).Hammer = originalHammerGlobal; (window as any).Hammer = originalHammerGlobal;
}); });
it('should call the loader provider only once', () => {
plugin.addEventListener(someElement, 'swipe', () => {});
plugin.addEventListener(someElement, 'panleft', () => {});
plugin.addEventListener(someElement, 'panright', () => {});
// Ensure that the loader is called only once, because previouly
// it was called the same number of times as `addEventListener` was called.
expect(loaderCalled).toEqual(1);
});
it('should not log a warning when HammerJS is not loaded', () => { it('should not log a warning when HammerJS is not loaded', () => {
plugin.addEventListener(someElement, 'swipe', () => {}); plugin.addEventListener(someElement, 'swipe', () => {});
expect(fakeConsole.warn).not.toHaveBeenCalled(); expect(fakeConsole.warn).not.toHaveBeenCalled();