Detect swipes with reactive programming
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 thedocs.
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 aSubscription
instance:
exportfunctioncreateSwipeSubscription({domElement,onSwipeMove,onSwipeEnd}:SwipeSubscriptionConfig):Subscription{// ...}
Where configuration object implements the following interface:
exportinterfaceSwipeSubscriptionConfig{domElement:HTMLElement;onSwipeMove?:(event:SwipeEvent)=>void;onSwipeEnd?:(event:SwipeEvent)=>void;}exportinterfaceSwipeEvent{direction:SwipeDirection;distance:number;}exportenumSwipeDirection{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. 😄
That being said, at the very top of ourcreateSwipeSubscription
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
oronSwipeEnd
or both). Otherwise, there is no point in swipe event detection if we don't report anything back.
if(!(domElementinstanceofHTMLElement)){thrownewError('Provided domElement should be instance of HTMLElement');}if((typeofonSwipeMove!=='function')&&(typeofonSwipeEnd!=='function')){thrownewError('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:
consttouchStarts$:Observable<SwipeCoordinates>=fromEvent(domElement,'touchstart').pipe(map(getTouchCoordinates));consttouchMoves$:Observable<SwipeCoordinates>=fromEvent(domElement,'touchmove').pipe(map(getTouchCoordinates));consttouchEnds$:Observable<SwipeCoordinates>=fromEvent(domElement,'touchend').pipe(map(getTouchCoordinates));consttouchCancels$:Observable<Event>=fromEvent(domElement,'touchcancel');
RxJS provides a useful utility functionfromEvent
that works similar to the nativeaddEventListener
but returns anObservable
instance which is exactly what we need.
We use thegetTouchCoordinates
helper function to transform touch events to the format we need:
functiongetTouchCoordinates(touchEvent:TouchEvent):SwipeCoordinates{return{x:touchEvent.changedTouches[0].clientX,y:touchEvent.changedTouches[0].clientY};}
Since we are only interested in event coordinates, we pickclientX
andclientY
fields and discard the rest. Note we only care about the first touchpoint as stated earlier, so we ignore elements in thechangedTouches
array other than the first one.
Detecting the swipe start
Next we need to detect the start and the direction of the swipe:
consttouchStartsWithDirection$:Observable<SwipeStartEvent>=touchStarts$.pipe(switchMap((touchStartEvent:SwipeCoordinates)=>touchMoves$.pipe(elementAt(3),map((touchMoveEvent:SwipeCoordinates)=>({x:touchStartEvent.x,y:touchStartEvent.y,direction:getTouchDirection(touchStartEvent,touchMoveEvent)})))));
ThetouchStartsWithDirection$
inner observable waits for the third consecutivetouchmove
event following thetouchstart
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 newtouchstart
is emitted before the 3rdtouchmove
was received, theswitchMap
inner observable will start waiting for three consecutivetouchmove
events again.
When the thirdtouchmove
event is received, we map the initially recordedtouchstart
event toSwipeStartEvent
form by taking thex
andy
coordinates of the original event and detecting the swipe direction with the following helper function:
functiongetTouchDirection(startCoordinates:SwipeCoordinates,moveCoordinates:SwipeCoordinates):SwipeDirection{const{x,y}=getTouchDistance(startCoordinates,moveCoordinates);returnMath.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 thetouchStartsWithDirection$
defined earlier to start trackingtouchmove
andtouchend
events:
returntouchStartsWithDirection$.pipe(switchMap(touchStartEvent=>touchMoves$.pipe(map(touchMoveEvent=>getTouchDistance(touchStartEvent,touchMoveEvent)),tap((coordinates:SwipeCoordinates)=>{if(typeofonSwipeMove!=='function'){return;}onSwipeMove(getSwipeEvent(touchStartEvent,coordinates));}),takeUntil(touchEnds$.pipe(map(touchEndEvent=>getTouchDistance(touchStartEvent,touchEndEvent)),tap((coordinates:SwipeCoordinates)=>{if(typeofonSwipeEnd!=='function'){return;}onSwipeEnd(getSwipeEvent(touchStartEvent,coordinates));})))))).subscribe();
We utilize theswitchMap
operator again to start listening totouchmove
events in an inner observable. If theonSwipeMove
event handler has been provided, it gets called on every event emission.
Thanks to thetakeUntil
operator our observable only lives until thetouchend
event is received. When this happens, if theonSwipeEnd
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 thetouchStartEvent
coordinates:
functiongetTouchDistance(startCoordinates:SwipeCoordinates,moveCoordinates:SwipeCoordinates):SwipeCoordinates{return{x:moveCoordinates.x-startCoordinates.x,y:moveCoordinates.y-startCoordinates.y};}
andgetSwipeEvent
creates library output events containing the information about swipe direction and distance:
functiongetSwipeEvent(touchStartEvent:SwipeStartEvent,coordinates:SwipeCoordinates):SwipeEvent{return{direction:touchStartEvent.direction,distance:coordinates[touchStartEvent.direction]};}
Handling edge cases
Thetouchend
event is not the only event that can signalize touch move interruption. As the documentation states, thetouchcancel
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 thetouchcancel
events:
And use it in ourtakeUntil
subscription:
takeUntil(race(touchEnds$.pipe(map(touchEndEvent=>getTouchDistance(touchStartEvent,touchEndEvent)),tap((coordinates:SwipeCoordinates)=>{if(typeofonSwipeEnd!=='function'){return;}onSwipeEnd(getSwipeEvent(touchStartEvent,coordinates));})),touchCancels$))
What happens here is we are creating a race with two participants:touchend
andtouchcancel
. We utilize the RxJSrace
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 innertouchmove
subscription. In case it istouchcancel
we don't need to emit theonSwipeEnd
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. 💪
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 RxJSSubscription
that in its turn extends theUnsubscribable
interface:
exportinterfaceUnsubscribable{unsubscribe():void;}
This way the consumer of the library will be able to hold the reference to the returnedSubscription
in a variable or a class property and call theunsubscribe
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 bythis link.
And thenpm
package bythis link.
Usage
Time to see the library in action. Why did we build one in the first place? 😄
import{createSwipeSubscription,SwipeEvent}from'ag-swipe-core';constdomElement:HTMLElement=document.querySelector('#swipe-element');constswipeSubscription=createSwipeSubscription({domElement,onSwipeEnd:(event:SwipeEvent)=>{console.log(`SwipeEnd direction:${event.direction} and distance:${event.distance}`);},});
If you want to trackonSwipeMove
as well, just add the corresponding handler function to thecreateSwipeSubscription
configuration object.
And when swipe events should no longer be tracked:
swipeSubscription?.unsubscribe?.();
Online preview:
https://typescript-hey3oq.stackblitz.io
Live editor to play around with:
💡 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!
Top comments(3)

- EducationKyiv National Linguistic University
- WorkApplication Developer
- Joined
Great article!
By the way, maybe the check blocks for function existence can be reduced with optional chaining?
swipeSubscription?.unsubscribe?.();
instead of
if(typeofswipeSubscription?.unsubscribe==='function'){swipeSubscription.unsubscribe();}
:)

- LocationRiga, Latvia
- EducationRiga Technical University, M.S. in Electronics Engineering
- Joined
Makes sense. As long as we have committed to use Typescript, no reason to not use it at its full power:)
Updated both in the article text and in the Stackblitz example.

- LocationIndia, Kerala
- EducationJawaharlal Nehru Institute of Arts and Science
- WorkFull Stack Engineer, Freelancer
- Joined
That was great (I mean it)
For further actions, you may consider blocking this person and/orreporting abuse