refactor(platform-browser): remove Hammer integration

The integration was deprecated in v20 and will now be removed.

BREAKING CHANGE: Hammer.js integration has been removed. Use your own implementation.
This commit is contained in:
Matthieu Riegler 2026-03-06 18:40:03 +01:00 committed by Leon Senft
parent dacd357016
commit f99e7ed20f
11 changed files with 13 additions and 596 deletions

View file

@ -97,48 +97,6 @@ export abstract class EventManagerPlugin {
abstract supports(eventName: string): boolean;
}
// @public @deprecated
export const HAMMER_GESTURE_CONFIG: InjectionToken<HammerGestureConfig>;
// @public @deprecated
export const HAMMER_LOADER: InjectionToken<HammerLoader>;
// @public @deprecated
export class HammerGestureConfig {
buildHammer(element: HTMLElement): HammerInstance;
events: string[];
options?: {
cssProps?: any;
domEvents?: boolean;
enable?: boolean | ((manager: any) => boolean);
preset?: any[];
touchAction?: string;
recognizers?: any[];
inputClass?: any;
inputTarget?: EventTarget;
};
overrides: {
[key: string]: Object;
};
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<HammerGestureConfig, never>;
// (undocumented)
static ɵprov: i0.ɵɵInjectableDeclaration<HammerGestureConfig>;
}
// @public @deprecated
export type HammerLoader = () => Promise<void>;
// @public @deprecated
export class HammerModule {
// (undocumented)
static ɵfac: i0.ɵɵFactoryDeclaration<HammerModule, never>;
// (undocumented)
static ɵinj: i0.ɵɵInjectorDeclaration<HammerModule>;
// (undocumented)
static ɵmod: i0.ɵɵNgModuleDeclaration<HammerModule, never, never, never>;
}
// @public
export interface HydrationFeature<FeatureKind extends HydrationFeatureKind> {
// (undocumented)

1
modules/types.d.ts vendored
View file

@ -8,7 +8,6 @@
// This file contains all ambient imports needed to compile the modules/ source code
/// <reference types="hammerjs" />
/// <reference types="jasmine" />
/// <reference types="jasminewd2" />
/// <reference types="node" />

View file

@ -97,7 +97,6 @@
"@types/convert-source-map": "^2.0.0",
"@types/dom-navigation": "^1.0.5",
"@types/firefox-webext-browser": "^143.0.0",
"@types/hammerjs": "2.0.46",
"@types/jasmine": "^6.0.0",
"@types/jasminewd2": "^2.0.8",
"@types/node": "^20.14.8",

View file

@ -31,7 +31,6 @@ ts_project(
),
deps = [
":goog_types",
"//:node_modules/@types/hammerjs",
"//:node_modules/tslib",
"//:node_modules/zone.js",
],

View file

@ -11,7 +11,6 @@ ng_project(
],
),
deps = [
"//:node_modules/@types/hammerjs",
"//packages:goog_types",
"//packages:types",
"//packages/common",

View file

@ -1,321 +0,0 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
/// <reference types="hammerjs" />
import {DOCUMENT} from '@angular/common';
import {
Inject,
Injectable,
InjectionToken,
Injector,
NgModule,
Optional,
ɵConsole as Console,
} from '@angular/core';
import {EVENT_MANAGER_PLUGINS} from './event_manager';
import {EventManagerPlugin} from './event_manager_plugin';
/**
* Supported HammerJS recognizer event names.
*/
const EVENT_NAMES = {
// pan
'pan': true,
'panstart': true,
'panmove': true,
'panend': true,
'pancancel': true,
'panleft': true,
'panright': true,
'panup': true,
'pandown': true,
// pinch
'pinch': true,
'pinchstart': true,
'pinchmove': true,
'pinchend': true,
'pinchcancel': true,
'pinchin': true,
'pinchout': true,
// press
'press': true,
'pressup': true,
// rotate
'rotate': true,
'rotatestart': true,
'rotatemove': true,
'rotateend': true,
'rotatecancel': true,
// swipe
'swipe': true,
'swipeleft': true,
'swiperight': true,
'swipeup': true,
'swipedown': true,
// tap
'tap': true,
'doubletap': true,
};
/**
* DI token for providing [HammerJS](https://hammerjs.github.io/) support to Angular.
* @see {@link HammerGestureConfig}
*
* @ngModule HammerModule
* @publicApi
*
* @deprecated The HammerJS integration is deprecated. Replace it by your own implementation.
*/
export const HAMMER_GESTURE_CONFIG = new InjectionToken<HammerGestureConfig>(
typeof ngDevMode === 'undefined' || ngDevMode ? 'HammerGestureConfig' : '',
);
/**
* Function that loads HammerJS, returning a promise that is resolved once HammerJs is loaded.
*
* @publicApi
*
* @deprecated The hammerjs integration is deprecated. Replace it by your own implementation.
*/
export type HammerLoader = () => Promise<void>;
/**
* Injection token used to provide a HammerLoader to Angular.
*
* @see {@link HammerLoader}
*
* @publicApi
*
* @deprecated The HammerJS integration is deprecated. Replace it by your own implementation.
*/
export const HAMMER_LOADER = new InjectionToken<HammerLoader>(
typeof ngDevMode === 'undefined' || ngDevMode ? 'HammerLoader' : '',
);
export interface HammerInstance {
on(eventName: string, callback?: Function): void;
off(eventName: string, callback?: Function): void;
destroy?(): void;
}
/**
* An injectable [HammerJS Manager](https://hammerjs.github.io/api/#hammermanager)
* for gesture recognition. Configures specific event recognition.
* @publicApi
*
* @deprecated The HammerJS integration is deprecated. Replace it by your own implementation.
*/
@Injectable()
export class HammerGestureConfig {
/**
* A set of supported event names for gestures to be used in Angular.
* Angular supports all built-in recognizers, as listed in
* [HammerJS documentation](https://hammerjs.github.io/).
*/
events: string[] = [];
/**
* Maps gesture event names to a set of configuration options
* that specify overrides to the default values for specific properties.
*
* The key is a supported event name to be configured,
* and the options object contains a set of properties, with override values
* to be applied to the named recognizer event.
* For example, to disable recognition of the rotate event, specify
* `{"rotate": {"enable": false}}`.
*
* Properties that are not present take the HammerJS default values.
* For information about which properties are supported for which events,
* and their allowed and default values, see
* [HammerJS documentation](https://hammerjs.github.io/).
*
*/
overrides: {[key: string]: Object} = {};
/**
* Properties whose default values can be overridden for a given event.
* Different sets of properties apply to different events.
* For information about which properties are supported for which events,
* and their allowed and default values, see
* [HammerJS documentation](https://hammerjs.github.io/).
*/
options?: {
cssProps?: any;
domEvents?: boolean;
enable?: boolean | ((manager: any) => boolean);
preset?: any[];
touchAction?: string;
recognizers?: any[];
inputClass?: any;
inputTarget?: EventTarget;
};
/**
* Creates a [HammerJS Manager](https://hammerjs.github.io/api/#hammermanager)
* and attaches it to a given HTML element.
* @param element The element that will recognize gestures.
* @returns A HammerJS event-manager object.
*/
buildHammer(element: HTMLElement): HammerInstance {
const mc = new Hammer!(element, this.options);
mc.get('pinch').set({enable: true});
mc.get('rotate').set({enable: true});
for (const eventName in this.overrides) {
mc.get(eventName).set(this.overrides[eventName]);
}
return mc;
}
}
/**
* Event plugin that adds Hammer support to an application.
*
* @ngModule HammerModule
*/
@Injectable()
export class HammerGesturesPlugin extends EventManagerPlugin {
private _loaderPromise: Promise<void> | null = null;
constructor(
@Inject(DOCUMENT) doc: any,
@Inject(HAMMER_GESTURE_CONFIG) private _config: HammerGestureConfig,
private _injector: Injector,
@Optional() @Inject(HAMMER_LOADER) private loader?: HammerLoader | null,
) {
super(doc);
}
override supports(eventName: string): boolean {
if (!EVENT_NAMES.hasOwnProperty(eventName.toLowerCase()) && !this.isCustomEvent(eventName)) {
return false;
}
if (!(window as any).Hammer && !this.loader) {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
// Get a `Console` through an injector to tree-shake the
// class when it is unused in production.
const _console = this._injector.get(Console);
_console.warn(
`The "${eventName}" event cannot be bound because Hammer.JS is not ` +
`loaded and no custom loader has been specified.`,
);
}
return false;
}
return true;
}
override addEventListener(element: HTMLElement, eventName: string, handler: Function): Function {
const zone = this.manager.getZone();
eventName = eventName.toLowerCase();
// If Hammer is not present but a loader is specified, we defer adding the event listener
// until Hammer is loaded.
if (!(window as any).Hammer && this.loader) {
this._loaderPromise = this._loaderPromise || zone.runOutsideAngular(() => this.loader!());
// This `addEventListener` method returns a function to remove the added listener.
// Until Hammer is loaded, the returned function needs to *cancel* the registration rather
// than remove anything.
let cancelRegistration = false;
let deregister: Function = () => {
cancelRegistration = true;
};
zone.runOutsideAngular(() =>
this._loaderPromise!.then(() => {
// If Hammer isn't actually loaded when the custom loader resolves, give up.
if (!(window as any).Hammer) {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
const _console = this._injector.get(Console);
_console.warn(`The custom HAMMER_LOADER completed, but Hammer.JS is not present.`);
}
deregister = () => {};
return;
}
if (!cancelRegistration) {
// Now that Hammer is loaded and the listener is being loaded for real,
// the deregistration function changes from canceling registration to
// removal.
deregister = this.addEventListener(element, eventName, handler);
}
}).catch(() => {
if (typeof ngDevMode === 'undefined' || ngDevMode) {
const _console = this._injector.get(Console);
_console.warn(
`The "${eventName}" event cannot be bound because the custom ` +
`Hammer.JS loader failed.`,
);
}
deregister = () => {};
}),
);
// Return a function that *executes* `deregister` (and not `deregister` itself) so that we
// can change the behavior of `deregister` once the listener is added. Using a closure in
// this way allows us to avoid any additional data structures to track listener removal.
return () => {
deregister();
};
}
return zone.runOutsideAngular(() => {
// Creating the manager bind events, must be done outside of angular
const mc = this._config.buildHammer(element);
const callback = function (eventObj: HammerInput) {
zone.runGuarded(function () {
handler(eventObj);
});
};
mc.on(eventName, callback);
return () => {
mc.off(eventName, callback);
// destroy mc to prevent memory leak
if (typeof mc.destroy === 'function') {
mc.destroy();
}
};
});
}
isCustomEvent(eventName: string): boolean {
return this._config.events.indexOf(eventName) > -1;
}
}
/**
* Adds support for HammerJS.
*
* Import this module at the root of your application so that Angular can work with
* HammerJS to detect gesture events.
*
* Note that applications still need to include the HammerJS script itself. This module
* simply sets up the coordination layer between HammerJS and Angular's `EventManager`.
*
* @publicApi
*
* @deprecated The hammer integration is deprecated. Replace it by your own implementation.
*/
@NgModule({
providers: [
{
provide: EVENT_MANAGER_PLUGINS,
useClass: HammerGesturesPlugin,
multi: true,
deps: [DOCUMENT, HAMMER_GESTURE_CONFIG, Injector, [new Optional(), HAMMER_LOADER]],
},
{provide: HAMMER_GESTURE_CONFIG, useClass: HammerGestureConfig},
],
})
export class HammerModule {}

View file

@ -22,12 +22,15 @@ export {REMOVE_STYLES_ON_COMPONENT_DESTROY} from './dom/dom_renderer';
export {EVENT_MANAGER_PLUGINS, EventManager} from './dom/events/event_manager';
export {EventManagerPlugin} from './dom/events/event_manager_plugin';
export {
HAMMER_GESTURE_CONFIG,
HAMMER_LOADER,
HammerGestureConfig,
HammerLoader,
HammerModule,
} from './dom/events/hammer_gestures';
HydrationFeature,
HydrationFeatureKind,
provideClientHydration,
withEventReplay,
withHttpTransferCacheOptions,
withI18nSupport,
withIncrementalHydration,
withNoHttpTransferCache,
} from './hydration';
export {
DomSanitizer,
SafeHtml,
@ -37,16 +40,6 @@ export {
SafeUrl,
SafeValue,
} from './security/dom_sanitization_service';
export {
HydrationFeature,
HydrationFeatureKind,
provideClientHydration,
withEventReplay,
withHttpTransferCacheOptions,
withI18nSupport,
withNoHttpTransferCache,
withIncrementalHydration,
} from './hydration';
export * from './private_export';
export {VERSION} from './version';

View file

@ -11,7 +11,6 @@ export {BrowserDomAdapter as ɵBrowserDomAdapter} from './browser/browser_adapte
export {BrowserGetTestability as ɵBrowserGetTestability} from './browser/testability';
export {DomRendererFactory2 as ɵDomRendererFactory2} from './dom/dom_renderer';
export {DomEventsPlugin as ɵDomEventsPlugin} from './dom/events/dom_events';
export {HammerGesturesPlugin as ɵHammerGesturesPlugin} from './dom/events/hammer_gestures';
export {KeyEventsPlugin as ɵKeyEventsPlugin} from './dom/events/key_events';
export {SharedStylesHost as ɵSharedStylesHost} from './dom/shared_styles_host';
export {RuntimeErrorCode as ɵRuntimeErrorCode} from './errors';

View file

@ -1,194 +0,0 @@
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
import {ApplicationRef, Injector, NgZone} from '@angular/core';
import {fakeAsync, inject, TestBed, tick} from '@angular/core/testing';
import {EventManager} from '../../../index';
import {HammerGestureConfig, HammerGesturesPlugin} from '../../../src/dom/events/hammer_gestures';
import {isNode} from '@angular/private/testing';
describe('HammerGesturesPlugin', () => {
let plugin: HammerGesturesPlugin;
if (isNode) {
// Jasmine will throw if there are no tests.
it('should pass', () => {});
return;
}
describe('with no custom loader', () => {
beforeEach(() => {
plugin = new HammerGesturesPlugin(
document,
new HammerGestureConfig(),
TestBed.inject(Injector),
);
});
it('should warn user and do nothing when Hammer.js not loaded', () => {
const warnSpy = spyOn(console, 'warn');
expect(plugin.supports('swipe')).toBe(false);
expect(warnSpy).toHaveBeenCalledWith(
`The "swipe" event cannot be bound because Hammer.JS is not ` +
`loaded and no custom loader has been specified.`,
);
});
});
describe('with a custom loader', () => {
// Use a fake custom loader for tests, with helper functions to resolve or reject.
let loader: () => Promise<void>;
let resolveLoader: () => void;
let failLoader: () => void;
// Arbitrary element and listener for testing.
let someElement: HTMLDivElement;
let someListener: () => void;
// Keep track of whatever value is in `window.Hammer` before the test so it can be
// restored afterwards so that this test doesn't care whether Hammer is actually loaded.
let originalHammerGlobal: any;
// Fake Hammer instance ("mc") used to test the underlying event registration.
let fakeHammerInstance: {on: jasmine.Spy; off: jasmine.Spy};
// Inject the NgZone so that we can make it available to the plugin through a fake
// EventManager.
let ngZone: NgZone;
beforeEach(inject([NgZone], (z: NgZone) => {
ngZone = z;
}));
let loaderCalled = 0;
let loaderIsCalledInAngularZone: boolean | null = null;
beforeEach(() => {
originalHammerGlobal = (window as any).Hammer;
(window as any).Hammer = undefined;
fakeHammerInstance = {
on: jasmine.createSpy('mc.on'),
off: jasmine.createSpy('mc.off'),
};
loader = () => {
loaderCalled++;
loaderIsCalledInAngularZone = NgZone.isInAngularZone();
return new Promise((resolve, reject) => {
resolveLoader = resolve;
failLoader = reject;
});
};
// Make the hammer config return a fake hammer instance
const hammerConfig = new HammerGestureConfig();
spyOn(hammerConfig, 'buildHammer').and.returnValue(fakeHammerInstance);
plugin = new HammerGesturesPlugin(document, hammerConfig, TestBed.inject(Injector), loader);
// Use a fake EventManager that has access to the NgZone.
plugin.manager = {getZone: () => ngZone} as EventManager;
someElement = document.createElement('div');
someListener = () => {};
});
afterEach(() => {
loaderCalled = 0;
(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', () => {
const warnSpy = spyOn(console, 'warn');
plugin.addEventListener(someElement, 'swipe', () => {});
expect(warnSpy).not.toHaveBeenCalled();
});
it('should defer registering an event until Hammer is loaded', fakeAsync(() => {
plugin.addEventListener(someElement, 'swipe', someListener);
expect(fakeHammerInstance.on).not.toHaveBeenCalled();
(window as any).Hammer = {};
resolveLoader();
tick();
expect(fakeHammerInstance.on).toHaveBeenCalledWith('swipe', jasmine.any(Function));
}));
it('should cancel registration if an event is removed before being added', fakeAsync(() => {
const deregister = plugin.addEventListener(someElement, 'swipe', someListener);
deregister();
(window as any).Hammer = {};
resolveLoader();
tick();
expect(fakeHammerInstance.on).not.toHaveBeenCalled();
}));
it('should remove a listener after Hammer is loaded', fakeAsync(() => {
const removeListener = plugin.addEventListener(someElement, 'swipe', someListener);
(window as any).Hammer = {};
resolveLoader();
tick();
removeListener();
expect(fakeHammerInstance.off).toHaveBeenCalledWith('swipe', jasmine.any(Function));
}));
it('should log a warning when the loader fails', fakeAsync(() => {
const warnSpy = spyOn(console, 'warn');
plugin.addEventListener(someElement, 'swipe', () => {});
failLoader();
tick();
expect(warnSpy).toHaveBeenCalledWith(
`The "swipe" event cannot be bound because the custom Hammer.JS loader failed.`,
);
}));
it('should load a warning if the loader resolves and Hammer is not present', fakeAsync(() => {
const warnSpy = spyOn(console, 'warn');
plugin.addEventListener(someElement, 'swipe', () => {});
resolveLoader();
tick();
expect(warnSpy).toHaveBeenCalledWith(
`The custom HAMMER_LOADER completed, but Hammer.JS is not present.`,
);
}));
it('should call the loader outside of the Angular zone', fakeAsync(() => {
const ngZone = TestBed.inject(NgZone);
// Unit tests are being run in a ProxyZone, thus `addEventListener` is called within the
// ProxyZone. In real apps, `addEventListener` is called within the Angular zone; we
// mimic that behaviour by entering the Angular zone.
ngZone.run(() => plugin.addEventListener(someElement, 'swipe', () => {}));
const appRef = TestBed.inject(ApplicationRef);
spyOn(appRef, 'tick');
resolveLoader();
tick();
expect(appRef.tick).not.toHaveBeenCalled();
expect(loaderIsCalledInAngularZone).toEqual(false);
}));
});
});

1
packages/types.d.ts vendored
View file

@ -10,7 +10,6 @@
// This file contains all ambient imports needed to compile the packages/ source code
/// <reference types="hammerjs" />
/// <reference lib="es2015" />
/// <reference path="./goog.d.ts" />
/// <reference path="./system.d.ts" />

View file

@ -142,9 +142,6 @@ importers:
'@types/firefox-webext-browser':
specifier: ^143.0.0
version: 143.0.0
'@types/hammerjs':
specifier: 2.0.46
version: 2.0.46
'@types/jasmine':
specifier: ^6.0.0
version: 6.0.0
@ -5322,9 +5319,6 @@ packages:
'@types/geojson@7946.0.16':
resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==}
'@types/hammerjs@2.0.46':
resolution: {integrity: sha512-ynRvcq6wvqexJ9brDMS4BnBLzmr0e14d6ZJTEShTBWKymQiHwlAyGu0ZPEFI2Fh1U53F7tN9ufClWM5KvqkKOw==}
'@types/har-format@1.2.16':
resolution: {integrity: sha512-fluxdy7ryD3MV6h8pTfTYpy/xQzCFC7m89nOH9y94cNqJ1mDIDPut7MnRHI3F6qRmh/cT2fUjG1MLdCNb4hE9A==}
@ -5876,11 +5870,6 @@ packages:
ajv-formats@2.1.1:
resolution: {integrity: sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==}
peerDependencies:
ajv: ^8.0.0
peerDependenciesMeta:
ajv:
optional: true
ajv-formats@3.0.1:
resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==}
@ -17980,8 +17969,6 @@ snapshots:
'@types/geojson@7946.0.16': {}
'@types/hammerjs@2.0.46': {}
'@types/har-format@1.2.16': {}
'@types/hast@3.0.4':
@ -18575,8 +18562,8 @@ snapshots:
optionalDependencies:
ajv: 8.18.0
ajv-formats@2.1.1(ajv@8.18.0):
optionalDependencies:
ajv-formats@2.1.1:
dependencies:
ajv: 8.18.0
ajv-formats@3.0.1:
@ -20836,7 +20823,7 @@ snapshots:
dependencies:
'@apidevtools/json-schema-ref-parser': 9.1.2
ajv: 8.18.0
ajv-formats: 2.1.1(ajv@8.18.0)
ajv-formats: 2.1.1
body-parser: 1.20.4
content-type: 1.0.5
deep-freeze: 0.0.1
@ -25380,7 +25367,7 @@ snapshots:
dependencies:
'@types/json-schema': 7.0.15
ajv: 8.18.0
ajv-formats: 2.1.1(ajv@8.18.0)
ajv-formats: 2.1.1
ajv-keywords: 5.1.0(ajv@8.18.0)
secretlint@10.2.2: