This post explores how to work with hammerjs gesture recognizers exposed through the @angular/platform-browser package. Code samples reference @angular/platform-browser@5.2.0, though several modifications arriving in 6.0.0 are mentioned later on.

Context

For mobile projects demanding gesture support, hammerjs provides the necessary building blocks. The library ships with recognition for pan, pinch, press, rotate, swipe, and tap. Each recognizer can be attached to any DOM element to catch a specific gesture and let you react to it.

All gesture detection in hammerjs depends on pointer events (pointermove, pointerup, pointerdown, and pointercancel). In Internet Explorer, MSPointer events take the place of standard pointer events.

@angular/platform-browser takes care of wiring up hammerjs gestures automatically inside the HammerGestureConfig addEventListener method. That call initializes hammerjs outside Angular and runs the required checks to confirm the library is available within the application.

Setting up hammerjs

hammerjs is straightforward to install through npm with this command inside your Angular project:

npm install hammerjs

After installation, add import 'hammerjs'; to your main.ts file. Omitting this import will produce a console error that prevents your application from starting.

Error: Hammer.js is not loaded, can not bind to x event

Challenges with hammerjs

In version 5.2.0, forgetting to include hammerjs triggers an exception that halts the rest of the Angular project from loading. @angular/platform-browser@6.0.0 replaces that thrown error with a console.warn, allowing the project to continue running even without gesture support.

The relevant issue is tracked in the github issue tracker.

hammerjs Gestures

A collection of events for attaching to DOM elements is included with @angular/platform-browser.

The events listed below are provided, though the most current list is available on github:

  • pan
  • panstart
  • panmove
  • panend
  • pancancel
  • panleft
  • panright
  • panup
  • pandown
  • pinch
  • pinchstart
  • pinchmove
  • pinchend
  • pinchcancel
  • pinchin
  • pinchout
  • press
  • pressup
  • rotate
  • rotatestart
  • rotatemove
  • rotateend
  • rotatecancel
  • swipe
  • swipeleft
  • swiperight
  • swipeup
  • swipedown
  • tap

Gesture Recognizers

  • Pan : A Pan gesture is detected when a pointer is held down and moves along a particular direction. This gesture is typical for scrolling through a list of items.
  • Pinch : A Pinch gesture is detected when two or more pointers move either closer together or further apart. Zooming in or out is the typical use case.
  • Press : A Press gesture is detected when the pointer remains held down for a specified duration. This is often used for long-press actions.
  • Rotate : A Rotate gesture is detected when a minimum of two pointers move in a circular pattern. Rotating elements is the standard application.
  • Swipe : A Swipe gesture is detected when a pointer moves at or above a certain speed over a minimum distance. This is common for flipping between UI elements. Rather than scrolling, it works well for swapping items in a given direction.
  • Tap : A Tap gesture is detected when the user taps the screen. Button presses are a typical use.

Swipe vs Pan

Swipe and Pan can often serve similar purposes, but the key distinction is that a pan event fires continuously while the panning occurs, whereas the swipe event only triggers once the swipe has finished. Pan is better suited for smoothly moving an item while the cursor remains down, while swipe is more appropriate for moving an item after the gesture completes.

hammerjs without @angular/platform-browser

In the absence of @angular/platform-browser, you have to build your own custom directives to bring gesture support into your application.

At minimum, you need to bind to the window’s hammerjs manager and then hook into the tap event that hammerjs exposes.

import { Directive, ElementRef, EventEmitter, Input, NgZone, OnDestroy, OnInit, Output } from '@angular/core';

interface HammerManager {
  new (element: HTMLElement | SVGElement, options?: any): HammerManager;
  destroy(): void;
  add(recognizer: Recognizer): void;
  on(eventName: string, callback: Function): void;
}

interface Recognizer {
  new (options?: any): Recognizer;
  recognizeWith(otherRecognizer: Recognizer | string): Recognizer;
}

@Directive({
  selector: '[customTapGesture]',
})
export class TapGestureDirective implements OnInit, OnDestroy {
  constructor(private elementRef: ElementRef, private zone: NgZone) {}

  /**
   * Return the hammerjs library if it's available
   */
  private get hammerLib() {
    return typeof window !== 'undefined' ? (window as any).Hammer : undefined;
  }

  private manager?: HammerManager;

  /**
   * Event fired when the element is tapped
   */
  @Output() cTap = new EventEmitter<any>();

  /**
   * Binds HammerJS Instances
   */
  ngOnInit() {
    if (this.hammerLib) {
      this.manager = this.bindHammer();
    }
  }

  /**
   * Unbinds HammerJS Instances
   */
  ngOnDestroy() {
    if (this.manager) {
      this.manager.destroy();
    }
  }

  protected bindHammer(): HammerManager {
    return this.zone.run(_ => {
      const hostElement = this.elementRef.nativeElement;
      const manager = new this.hammerLib.Manager(hostElement, {
        touchAction: 'tap',
      });

      manager.add(new this.hammerLib.Tap({}));

      manager.on('tap', (ev: any) => {
        this.cTap.emit(ev);
        ev.preventDefault();
      });

      return manager;
    });
  }
}

A live demo can be found at https://stackblitz.com/edit/tap-gesture-directive.

HammerJS with @angular/platform-browser

Utilizing hammerjs through @angular/platform-browser lets developers set up mobile gestures without needing custom directives. Every gesture event depends on custom DOM event plug-ins. These events run outside Angular’s Zone.js instance and only re-enter the zone when the relevant event is dispatched. Additional details on DOM event plug-ins are covered in Ben Nadel’s blog. The examples below wire up each gesture recognizer on a simple div.

Information about the event object returned by hammerjs triggers is available here.

Pan

Binding the DOM element inside the Angular Component:

<div
    (pan)="onPan($event)"
    (panstart)="onPanStart($event)"
    (panmove)="onPanMove($event)"
    (panend)="onPanEnd($event)"
    (pancancel)="onPanCancel($event)"
    (panleft)="onPanLeft($event)"
    (panright)="onPanRight($event)"
    (panup)="onPanUp($event)"
    (pandown)="onPanDown($event)">
 </div>

A live stackblitz is avaiable at https://stackblitz.com/edit/pan-gesture.

Pinch

Binding the DOM element inside the Angular Component:

<div
    (pinch)="onPinch($event)"
    (pinchstart)="onPinchStart($event)"
    (pinchmove)="onPinchMove($event)"
    (pinchend)="onPinchEnd($event)"
    (pinchcancel)="onPinchCancel($event)"
    (pinchin)="onPinchIn($event)"
    (pinchout)="onPinchOut($event)">
</div>

A live stackblitz is avaiable at https://stackblitz.com/edit/pinch-gesture.

Press

Binding the DOM element inside the Angular Component:

<div
    (press)="onPress($event)"
    (pressup)="onPressUp($event)">
</div>

A live stackblitz is avaiable at https://stackblitz.com/edit/press-gesture.

Rotate

Binding the DOM element inside the Angular Component:

<div
    (rotate)="onRotate($event)"
    (rotatestart)="onRotateStart($event)"
    (rotatemove)="onRotateMove($event)"
    (rotateend)="onRotateEnd($event)"
    (rotatecancel)="onRotateCancel($event)">
</div>

A live stackblitz is avaiable at https://stackblitz.com/edit/rotate-gesture.

Swipe

Binding the DOM element inside the Angular Component:

<div
    (swipe)="onSwipe($event)"
    (swipeleft)="onSwipeLeft($event)"
    (swiperight)="onSwipeRight($event)"
    (swipeup)="onSwipeUp($event)"
    (swipedown)="onSwipeDown($event)">
</div>

A live stackblitz is avaiable at https://stackblitz.com/edit/swipe-gesture.

Update: 09/03/2019

It was pointed out that vertical swipes (up/down) were not being detected correctly in the swipe-related stackblitz. The cause is that swiping up and down requires the gesture config to be overridden to enable vertical all.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import * as Hammer from 'hammerjs';
import { HammerGestureConfig, HAMMER_GESTURE_CONFIG } from '@angular/platform-browser';

import { AppComponent } from './app.component';

export class MyHammerConfig extends HammerGestureConfig {
  overrides = <any> {
    swipe: { direction: Hammer.DIRECTION_ALL },
  };
}


@NgModule({
  imports:      [ BrowserModule, FormsModule ],
  declarations: [ AppComponent ],
  bootstrap:    [ AppComponent ],
  providers: [
    {
      provide: HAMMER_GESTURE_CONFIG,
      useClass: MyHammerConfig,
    },
  ],
})
export class AppModule { }

Tap

Binding the DOM element inside the Angular Component:

<div
    (tap)="onTap($event)">
 </div>

A live stackblitz is avaiable at https://stackblitz.com/edit/tap-gesture.

Configuring the Gestures

To change any default gesture settings within a module, you need to supply a custom HammerGestureConfig class.

The HammerGestureConfig is set up in this way:

import * as Hammer from 'hammerjs';
import { HammerGestureConfig } from '@angular/platform-browser';

export class MyHammerConfig extends HammerGestureConfig {
  overrides = <any> {
    pan: { direction: Hammer.DIRECTION_ALL },
    swipe: { direction: Hammer.DIRECTION_VERTICAL },
};

The MyHammerConfig class shown above sets the direction for the pan and swipe gesture recognizers. Each recognizer's settings are documented in the hammerjs documentation.

To finish, add your gesture configuration to the module using this provider:

{
    provide: HAMMER_GESTURE_CONFIG,
    useClass: MyHammerConfig
}

Spotted something I missed? Drop your feedback in the comments or reach out on Twitter.