Detect swipes with reactive programming
Tags: rxjs, javascript, typescript, swipe
Published: 2022-06-25 13:25:37

---
tags: rxjs, javascript, typescript, swipe
series: A JS library for every framework taste
--- 


In this article, we are going to build a library for swipe detection on touchscreen devices with help of a popular library RxJS that brings functional-reactive programming to the Javascript world. I would like to show you the power of reactive programming that provides the right tools to deal with event streams in a very elegant manner.

Please note this article implies that you are familiar with some basic concepts of RxJS like observables, subscriptions, and operators.

The library will be built in Typescript and will be framework-agnostic i.e. can be used in any Typescript/Javascript project. But we will follow some good practices to make it easy to use our library in framework-specific wrappers that we are going to create later.

### Defining the public interface of the library 

We will get to the reactive part soon enough. But first, let's address some general things we should take care of when designing a library.

A good starting point for building any library (or any reusable module in your codebase) is to define the public interface i.e. how our library is going to be used by consumers.

The library we are building will only detect simple swipe events. That means we are not going to handle multitouch interactions such as two-finger or three-finger gestures but only react to the first point of contact in touch events. You can read more about touch events WEB API in the [docs](https://developer.mozilla.org/en-US/docs/Web/API/Touch_events).

Our library will expose only one public function: `createSwipeSubscription`. We want to attach the swipe listener to an HTML element and react to the following events emitted by the library

- `onSwipeMove` - fires on every touch move event during the user swiping the element.
- `onSwipeEnd` - fires when swipe ends.

The function will accept a configuration object with three parameters and return a `Subscription` instance:

```typescript
export function createSwipeSubscription({
    domElement,
    onSwipeMove,
    onSwipeEnd
  }: SwipeSubscriptionConfig): Subscription {
	// ...
}
```

Where configuration object implements the following interface:

```typescript
export interface SwipeSubscriptionConfig {
  domElement: HTMLElement;
  onSwipeMove?: (event: SwipeEvent) => void;
  onSwipeEnd?: (event: SwipeEvent) => void;
}

export interface SwipeEvent {
  direction: SwipeDirection;
  distance: number;
}

export enum SwipeDirection {
  X = 'x',
  Y = 'y'
}
```

And last but not least, as we are dealing with observables here, we have to think about the unsubscription logic. Whenever the swipe listener is no longer needed the subscription should be terminated. The right approach will be to delegate this action to the consumer of the library as the consumer will know when it is the right time to execute it. This is not part of the configuration object but is an important part of the public interface our library should expose. We will cover the unsubscription part in more detail in the dedicated section below.

### Validating the user input

When the public interface of the library expects some input parameters to be passed from the library consumer side, we should treat those just like we would treat user input. Developers are library users after all, as well as human beings. :smile:

That being said, at the very top of our `createSwipeSubscription` method we want to check two things:

- Provided `domElement` should be a valid HTML element. Otherwise, we cannot attach any listeners to it.
- At least one of the event handlers should be provided (`onSwipeMove` or `onSwipeEnd` or both). Otherwise, there is no point in swipe event detection if we don't report anything back.

```typescript
if (!(domElement instanceof HTMLElement)) {
  throw new Error('Provided domElement should be instance of HTMLElement');
}

if ((typeof onSwipeMove !== 'function') && (typeof onSwipeEnd !== 'function')) {
  throw new Error('At least one of the following swipe event handler functions should be provided: onSwipeMove and/or onSwipeEnd');
}
```

### Tracking touch events

Here are all four event types we need to track:

```typescript
const touchStarts$: Observable<SwipeCoordinates> = fromEvent(domElement, 'touchstart').pipe(map(getTouchCoordinates));
const touchMoves$: Observable<SwipeCoordinates> = fromEvent(domElement, 'touchmove').pipe(map(getTouchCoordinates));
const touchEnds$: Observable<SwipeCoordinates> = fromEvent(domElement, 'touchend').pipe(map(getTouchCoordinates));
const touchCancels$: Observable<Event> = fromEvent(domElement, 'touchcancel');
```

RxJS provides a useful utility function `fromEvent` that works similar to the native `addEventListener` but returns an `Observable` instance which is exactly what we need.

We use the `getTouchCoordinates` helper function to transform touch events to the format we need:

```typescript
function getTouchCoordinates(touchEvent: TouchEvent): SwipeCoordinates {
  return {
    x: touchEvent.changedTouches[0].clientX,
    y: touchEvent.changedTouches[0].clientY
  };
}
```

Since we are only interested in event coordinates, we pick `clientX` and `clientY` fields and discard the rest. Note we only care about the first touchpoint as stated earlier, so we ignore elements in the `changedTouches` array other than the first one.

### Detecting the swipe start

Next we need to detect the start and the direction of the swipe:

```typescript
const touchStartsWithDirection$: Observable<SwipeStartEvent> = touchStarts$.pipe(
  switchMap((touchStartEvent: SwipeCoordinates) => touchMoves$.pipe(
    elementAt(3),
    map((touchMoveEvent: SwipeCoordinates) => ({
        x: touchStartEvent.x,
        y: touchStartEvent.y,
        direction: getTouchDirection(touchStartEvent, touchMoveEvent)
      })
    ))
  )
);
```

The `touchStartsWithDirection$` inner observable waits for the third consecutive `touchmove` event following the `touchstart` event. We do this to filter out accidental touches that we don't want to process. The third emission was picked experimentally as a reasonable threshold. If a new `touchstart` is emitted before the 3rd `touchmove` was received, the `switchMap` inner observable will start waiting for three consecutive `touchmove` events again.

When the third `touchmove` event is received, we map the initially recorded `touchstart` event to `SwipeStartEvent` form by taking the `x` and `y` coordinates of the original event and detecting the swipe direction with the following helper function:

```typescript
function getTouchDirection(startCoordinates: SwipeCoordinates, moveCoordinates: SwipeCoordinates): SwipeDirection {
  const { x,y } = getTouchDistance(startCoordinates, moveCoordinates);
  return Math.abs(x) < Math.abs(y) ? SwipeDirection.Y : SwipeDirection.X;
}
```

We will use this object further to calculate swipe move and swipe end events properties.

### Handling touch move and touch end events

Now we can subscribe to the `touchStartsWithDirection$`defined earlier to start tracking `touchmove` and `touchend` events:

```typescript
return touchStartsWithDirection$.pipe(
  switchMap(touchStartEvent => touchMoves$.pipe(
    map(touchMoveEvent => getTouchDistance(touchStartEvent, touchMoveEvent)),
    tap((coordinates: SwipeCoordinates) => {
      if (typeof onSwipeMove !== 'function') {
        return;
      }
      onSwipeMove(getSwipeEvent(touchStartEvent, coordinates));
    }),
    takeUntil(touchEnds$.pipe(
      map(touchEndEvent => getTouchDistance(touchStartEvent, touchEndEvent)),
      tap((coordinates: SwipeCoordinates) => {
        if (typeof onSwipeEnd !== 'function') {
          return;
        }
        onSwipeEnd(getSwipeEvent(touchStartEvent, coordinates));
      })
    ))
  ))
).subscribe();
```

We utilize the `switchMap` operator again to start listening to `touchmove` events in an inner observable. If the `onSwipeMove` event handler has been provided, it gets called on every event emission.

Thanks to the `takeUntil` operator our observable only lives until the `touchend` event is received. When this happens, if the `onSwipeEnd` event handler has been provided, it gets called.

In both cases we use two helper functions:

`getTouchDistance` calculates the swipe distance comparing the processed event's coordinates with the `touchStartEvent` coordinates:

```typescript
function getTouchDistance(startCoordinates: SwipeCoordinates, moveCoordinates: SwipeCoordinates): SwipeCoordinates {
  return {
    x: moveCoordinates.x - startCoordinates.x,
    y: moveCoordinates.y - startCoordinates.y
  };
}
```

and `getSwipeEvent` creates library output events containing the information about swipe direction and distance:

```typescript
function getSwipeEvent(touchStartEvent: SwipeStartEvent, coordinates: SwipeCoordinates): SwipeEvent {
  return {
    direction: touchStartEvent.direction,
    distance: coordinates[touchStartEvent.direction]
  };
}
```

### Handling edge cases

The `touchend` event is not the only event that can signalize touch move interruption. As the documentation states, the `touchcancel` event will be fired when:

> one or more touch points have been disrupted in an implementation-specific manner (for example, too many touch points are created).

We want to be prepared for this. That means we need to create one more event listener to capture the `touchcancel` events:

And use it in our `takeUntil` subscription:

```typescript
takeUntil(race(
  touchEnds$.pipe(
    map(touchEndEvent => getTouchDistance(touchStartEvent, touchEndEvent)),
    tap((coordinates: SwipeCoordinates) => {
      if (typeof onSwipeEnd !== 'function') {
        return;
      }
      onSwipeEnd(getSwipeEvent(touchStartEvent, coordinates));
    })
  ),
  touchCancels$
))
```

What happens here is we are creating a race with two participants: `touchend` and `touchcancel`. We utilize the RxJS `race` operator for this:

> `race` returns an observable, that when subscribed to, subscribes to all source observables immediately. As soon as one of the source observables emits a value, the result unsubscribes from the other sources. The resulting observable will forward all notifications, including error and completion, from the "winning" source observable.

So whichever event fires first will win the race and terminate the inner `touchmove` subscription. In case it is `touchcancel` we don't need to emit the `onSwipeEnd` event as we consider the swipe to be interrupted and don't want to handle this as a successful swipe end.

Let's stop here for a second to give some credit to Rx. Out of the box we have the right operator to solve the problem in one line. :muscle:

### Unsubscribing

As it was mentioned earlier, the consumer of our library should be able to unsubscribe from swipe event listeners when the subscription is no longer needed. For example, when the component's destroy hook is called.

We achieve this by returning the instance of RxJS `Subscription` that in its turn extends the `Unsubscribable` interface:

```typescript
export interface Unsubscribable {
  unsubscribe(): void;
}
```

This way the consumer of the library will be able to hold the reference to the returned `Subscription` in a variable or a class property and call the `unsubscribe` method when it should be called.

We will make sure this happens automatically when creating framework-specific wrappers for our library.

### Complete solution

You can find the complete library code on GitHub by [this link](https://github.com/aGoncharuks/angular-swipe-directive/blob/master/libs/swipe-core/src/lib/swipe-core.ts).

And the `npm` package by [this link](https://www.npmjs.com/package/ag-swipe-core).

### Usage
Time to see the library in action. Why did we build one in the first place? :smile:

```typescript
import { createSwipeSubscription, SwipeEvent } from 'ag-swipe-core';

const domElement: HTMLElement = document.querySelector('#swipe-element');

const swipeSubscription = createSwipeSubscription({
  domElement,
  onSwipeEnd: (event: SwipeEvent) => {
    console.log(`SwipeEnd direction: ${event.direction} and distance: ${event.distance}`);
  },
});
```

If you want to track `onSwipeMove` as well, just add the corresponding handler function to the `createSwipeSubscription` configuration object. 

And when swipe events should no longer be tracked:

```typescript
swipeSubscription?.unsubscribe?.();
```

Online preview:

https://typescript-hey3oq.stackblitz.io

Live editor to play around with:

{% embed https://stackblitz.com/edit/typescript-hey3oq?file=index.ts %}

:bulb: Don't forget to choose the right device type in DevTools if opening on the desktop. 

### Conclusion

This article covered the core logic of the swipe detection library. We used some of the RxJS powers to implement it in a neat reactive manner.

In the next articles, we are going to create wrappers around the library to make it a first-class citizen in popular Javascript frameworks.

Hope this one was useful to you. Thanks for reading and stay tuned!