Skip to main content

Reactivity with RxJS: force press

Implementing press and hold using RxJS

RxJS is a reactive programming library for JavaScript, which leverages Observable sequences to compose asynchronous or event-based programs. As part of the Reactive Extensions project, the architecture of RxJS combines the best parts from the Observerpattern, the Iterator pattern, and functional programming.
If you have used a JavaScript utility library like Lodash before, then you can think of RxJS as the Lodash for events.
RxJS is no longer a new JavaScript library. In fact, at the time of this writing, the most recent version of the library is 6.3.3, which is the latest of over 105 releases.
In this tutorial, we will leverage on reactive programming using RxJS to implement force press detection and handling for regular DOM mouse events.
Here is the force press demo on Code SandboxNavigate to the link and press and hold the volume controls to see the force press in action.
This tutorial should not be used as a substitute for a proper RxJS beginner’s guide, even though it briefly explains a couple of reactive programming concepts and operators.

Observables and operators

Observables are the core of the RxJS architecture. An observable can be likened to an invokable stream of values or events emanating from a source. The sources can be time intervals, AJAX requests, DOM events, etc.
An Observable:
  • is lazy (it doesn’t emit any value until it has been subscribed to)
  • may have one or more observers listening for its values
  • may be transformed into another observable by a chain of operators
Operators are pure functions that can return a new observable from an observable. This pattern makes it possible to chain operators since an observable is always returned at the end.
In fact, more recent versions of RxJS expose a .pipe() instance method on the <Observable> class, that can be used for chaining operators as function calls.
An operator basically listens for values from the source observable, implements some defined logic on the received values, and returns a new observable emitting values based on the logic.

Force press

Force press simply refers to a DOM press event like keydown and mousedown, sustained over a period of time before the corresponding DOM release event is activated, such as keyup and mouseup in this case.
In simple terms, a force press is synonymous to press and hold.
There are many areas in user interfaces where a force press might be applicable. Imagine having a set of volume controls for a music player widget, and you want to increase the volume from 30 to 70.
Basically, you can achieve this in two ways:
  1. press the VOLUME UP button several times until you reach the desired volume — this press could possibly be done 40 times
  2. force press (press and hold) the VOLUME UP button until you reach or are close to the desired volume, and then adjust until you reach the desired volume
Here is a simple demo of this illustration:
Comparing multiple presses to force press

Force press with vanilla JavaScript

Implementing force press with vanilla JavaScript, similar to what we have above, isn’t a Herculean task. This implementation will require:
  • listening for mousedown events on the volume control button
  • using setInterval() to continuously adjust the volume until a mouseup event happens
Let’s say the markup for our volume controls looks like the following:

<div id="volume-control">
  <button type="button" data-volume="decrease" aria-label="Decrease Volume"> - </button>
  <button type="button" data-volume="increase" aria-label="Increase Volume"> + </button>
</div>
The following code snippet shows what the force press implementation will look like using vanilla JavaScript. For brevity, the implementations of the increaseVolume() and decreaseVolume() functions have been left out:
const control = document.getElementById('volume-control');
const buttons = control.querySelectorAll('button');

let timeout = null;
let interval = null;

buttons.forEach($button => {
  const increase = $button.getAttribute('data-volume') === 'increase';
  const fn = increase ? increaseVolume : decreaseVolume;
  
  $button.addEventListener('mousedown', evt => {
    evt.preventDefault();
    fn();
    
    timeout = setTimeout(() => {
      interval = setInterval(fn, 100);
    }, 500);
    
    document.addEventListener('mouseup', resetForcePress);
  });
});

function resetForcePress(evt) {
  evt.preventDefault();
  timeout && clearTimeout(timeout);
  interval && clearInterval(interval);
  
  timeout = null;
  interval = null;
  
  document.removeEventListener('mouseup', resetForcePress);
}
This force press implementation using vanilla JavaScript looks very simple, hence, a library like RxJS doesn’t seem necessary.
A quick observation of the code snippet will show that the volume will continuously be adjusted by an equal amount at equal time intervals until a mouseup event is fired. This is a linear progression.
However, the implementation starts becoming complex when we want some more advanced control over the force press. For example, let’s say we want some form of exponential progressionof the volume. This means the volume should be changing more rapidly for longer force press.
Here is a simple illustration showing the difference:
An implementation such as that of exponential volume progression will be quite challenging using vanilla JavaScript, since you may have to keep track of how long the force press lives in order to determine how fast the volume should change.
Cases like this are best suited for the RxJS library. With RxJS comes even more power to compose observable sequences in order to handle complex asynchronous tasks.

Force press with RxJS

Let’s go ahead and re-implement the force press with linear volume progression using RxJS. Here is what it would look like:
import { fromEvent, timer } from 'rxjs';
import { map, switchMap, startWith, takeUntil } from 'rxjs/operators';

const control = document.getElementById('volume-control');
const buttons = control.querySelectorAll('button');

const documentMouseup$ = fromEvent(document, 'mouseup');

const forcepress = fn => {
  return timer(500, 100).pipe(
    startWith(fn()),
    takeUntil(documentMouseup$),
    map(fn)
  );
};

buttons.forEach($button => {
  const increase = $button.getAttribute('data-volume') === 'increase';
  const fn = increase ? increaseVolume : decreaseVolume;
  
  fromEvent($button, 'mousedown').pipe(
    switchMap(evt => {
      evt.preventDefault();
      return forcepress(fn);
    })
  ).subscribe();
});
A careful observation of this code snippet will show that we have imported some functions and operators from the RxJS library. The assumption is that you already have RxJS installed as a dependency for your project.
There are some important parts of the code snippet that are worth highlighting.
Line 7
const documentMouseup$ = fromEvent(document, 'mouseup');
The fromEvent helper function creates a new observable that emits every time the specified event is fired on a DOM node.
For example, in the line above, fromEvent creates an observable that emits an event object every time a mouseup is fired on the document node. The fromEvent function is also used in Line 21to listen for mousedown events on a volume control button.
Notice that the observable is stored in a constant named documentMouseup$. It is common practice to attach a $ after the name of a variable used to store an observable.
Lines 9–15
const forcepress = fn => {
  return timer(500, 100).pipe(
    startWith(fn()),
    takeUntil(documentMouseup$),
    map(fn)
  );
};
The forcepress() function takes a handler function fn as its argument and returns an observable. The returned observable is created from a timer using the timer() function and transformed using a chain of operators.
Let’s break down the code line by line:
timer(500, 100)
This timer() function call creates a new observable that emits a count integer starting from zero (0). The first integer is emitted after 500ms and then subsequent integers are emitted at 100msintervals.
The .pipe() method on an observable is used to chain operators by applying them as regular functions from left to right.
startWith
timer(500, 100).pipe(
  startWith(fn())
)
The startWith() operator receives a value as an argument that should be emitted first by the observable. This is useful for emitting an initial value from an observable.
Here, the startWith() operator is used to execute the handler fnand emit the returned value.
takeUntil
timer(500, 100).pipe(
  takeUntil(documentMouseup$)
)
The takeUntil() operator is used to stop emitting values from the source observable based on another observable. It receives an observable as its argument. The moment this observable emits its first value, no more value is emitted from the source observable.
In our code snippet, the documentMouseup$ observable is passed to the takeUntil() operator. This ensures that no more value is emitted from the timer the moment a mouseup event is fired on the document node.
map
timer(500, 100).pipe(
  map(fn)
)
The map() operator is very similar to Array.map() for JavaScript arrays. It takes a mapping function as its argument that receives the emitted value from the source observable and returns a transformed value.
Here, we simply pass the fn function as the mapping function to the map() operator.
Lines 21–26
fromEvent($button, 'mousedown').pipe(
  switchMap(evt => {
    evt.preventDefault();
    return forcepress(fn);
  })
).subscribe();
These lines simply map the mousedown event on a volume control button to the force press action using the switchMap() operator.
It first creates an observable of mousedown events on the button element. Next, it uses the switchMap() operator to map the emitted value to an inner observable whose values will be emitted. In our code snippet, the inner observable is returned from executing the forcepress() function.
Notice that we passed fn to the forcepress() function as defined. It is also very important to note that we subscribed to the observable using the subscribe() method. Remember that observables are lazy. If they are not subscribed, they don’t emit any value.

Improving the force press

A few things can be done to improve the force press using RxJS operators. One improvement will be to implement an exponential volume progression instead of the linear progression as we saw before.

Exponential volume progression

Doing this with RxJS is very simple. Let’s assume the current implementation of our volume adjustment functions looks like this:
let VOLUME = 0;

const boundedVolume = volume => {
  return Math.max(0, Math.min(volume, 100));
};

const increaseVolume = () => {
  VOLUME = boundedVolume(VOLUME + 1);
  return VOLUME;
};

const decreaseVolume = () => {
  VOLUME = boundedVolume(VOLUME - 1);
  return VOLUME;
};
We can modify the volume adjustment functions slightly to accept a volume step factor. These modifications will make it possible for us to achieve the exponential progression as we will see in a moment.
The following code snippet shows the modifications:
const increaseVolume = (factor = 1) => {
  VOLUME = boundedVolume(VOLUME + 1 * factor);
  return VOLUME;
};

const decreaseVolume = (factor = 1) => {
  VOLUME = boundedVolume(VOLUME - 1 * factor);
  return VOLUME;
};
With these modifications, we can now pass a factor to the volume adjustment functions to specify how much the volume should be adjusted. Calling these functions without passing a factor will simply adjust the volume one step at a time.
Now, we can modify the forcepress() function we created earlier as follows:
import { fromEvent, timer } from 'rxjs';
import { map, switchMap, startWith, takeUntil, withLatestFrom } from 'rxjs/operators';

const computedFactor = n => Math.round(
  Math.pow(1.25 + n / 10, 1 + n / 5)
);

const forcepress = fn => {
  return timer(500, 100).pipe(
    startWith(fn()),
    takeUntil(documentMouseup$),
    withLatestFrom(
      timer(1000, 500).pipe(startWith(0))
    ),
    map(([t, n]) => fn(computedFactor(n)))
  );
};
With this modification, we have successfully implemented force press on the volume control buttons with an exponential volume progression.
computedFactor
Here we have added a simple function named computedFactor for computing the volume adjustment factor. This function takes an integer argument n with which it computes the factor.
We are simply computing this expression:
Math.round(Math.pow(1.25 + n / 10, 1 + n / 5));
Here, we are using Math.pow() to progressively compute exponents based on the value of n. This expression can be modified to suit the exponential progression required. For example, it can be as simple as this:
Math.pow(2, n);
Also, notice that we are using Math.round() here to ensure that we get an integer factor since the computation involves a lot of floating-point numbers.
Here is a summary of the first ten values returned by the computedFactor() function. It seems like the perfect function for computing the factors:
0 => Math.round(Math.pow(1.25, 1.0)) => 1
1 => Math.round(Math.pow(1.35, 1.2)) => 1
2 => Math.round(Math.pow(1.45, 1.4)) => 2
3 => Math.round(Math.pow(1.55, 1.6)) => 2
4 => Math.round(Math.pow(1.65, 1.8)) => 2
5 => Math.round(Math.pow(1.75, 2.0)) => 3
6 => Math.round(Math.pow(1.85, 2.2)) => 4
7 => Math.round(Math.pow(1.95, 2.4)) => 5
8 => Math.round(Math.pow(2.05, 2.6)) => 6
9 => Math.round(Math.pow(2.15, 2.8)) => 9
withLatestFrom
A careful observation of the forcepress() function will show that this line:
map(fn)
has been replaced with these lines:
withLatestFrom(
  timer(1000, 500).pipe(startWith(0))
),
map(([t, n]) => fn(computedFactor(n)))
Here, we have introduced another RxJS operator withLatestFrom(). It takes another observable as its first argument. This operator is useful for emitting values from multiple observables as an array of values.
However, it only emits every time the source observable emits, emitting the latest values from all of the observables in order each time.
In our example, we passed in another observable created with the timer() function to the withLatestFrom() operator.
The timer observable emits an integer first after 1000ms and then subsequently every 500ms. The startWith() operator is piped to the timer observable causing it to start with an initial value of 0.
The mapper function passed to the map() operator expects an array as its first argument, since the withLatestFrom() operator emits an array of values.
Here is the map operator again:
map(([t, n]) => fn(computedFactor(n)))
In this code snippet, the t represents the value emitted by the first observable, which in this case is the source observable. The n represents the value emitted by the second observable, which is the timer.
Finally, we call fn() like before, only this time we pass a computed volume adjustment factor derived from calling the computedFactor() function with n.
Now here is the comparison between the linear and exponential progressions showing the duration of increasing the volume from 0 to 100:

Enhanced force press termination

So far, we are terminating the force pressed volume adjustment once a mouseup event is fired on the document node. However, we can enhance it further to allow termination of the force press when the volume reaches any of the limits, either 0 or 100.
We can create a custom operator function that we can pipe to the source observable to prevent it from emitting the moment any of these happens:
  • mouseup event is fired on the document node
  • the volume reaches either 0 or 100
Here is the custom operator function named limitVolume():
import { timer } from 'rxjs';
import { takeUntil, takeWhile, zip, last } from 'rxjs/operators';

const timerUntilMouseup$ = timer(10, 10).pipe(
  takeUntil(documentMouseup$)
);

const timerWithinLimits$ = timer(10, 10).pipe(
  takeWhile(() => VOLUME > 0 && VOLUME < 100)
);

const volumeStop$ = timerUntilMouseup$.pipe(
  zip(timerWithinLimits$),
  last()
);

const limitVolume = () => source$ => {
  return source$.pipe(
    takeUntil(volumeStop$)
  );
};
Here, we created two timer observables namely timerUntilMouseup$ and timerWithinLimits$ that terminate based on the two conditions we stated respectively.
Then we composed the volumeStop$ observable from the two observables using the zip() and last() operators to ensure that this observable only emits one value for the first of the two observables that are terminated.
Finally, we use the takeUntil() operator in the limitVolume()custom operator function to ensure that the source$ observable is terminated when the volumeStop$ observable emits its first value.
Notice that limitVolume() returns a function that takes an observable as its argument and returns another observable. This implementation is critical for it to be used as an RxJS operator.
With the limitVolume() custom operator, we can now modify forcepress() as follows:
const forcepress = fn => {
  return timer(500, 100).pipe(
    startWith(fn()),
    limitVolume(),
    withLatestFrom(
      timer(1000, 500).pipe(startWith(0))
    ),
    map(([t, n]) => fn(computedFactor(n)))
  );
};

More force press for the calendar

A lot has been done already in implementing force press. However, let’s consider another force press demo that involves cycling through calendar months and years.
Imagine you were building a calendar widget and you wanted the user to cycle through months and years on the calendar. This sounds like a pretty nice use case for force pressing.
Here is a screenshot of the demo:
In this demo, a little spice has been added to the force press to enable key detection. Notice that whenever the SHIFT key is being pressed, the cycling switches from months to years.
Also, notice that the speed of cycling through the months is more rapid than that of cycling through the years.
Implementing something like this with setTimeout() and vanilla JavaScript will be quite complex. However, it is a lot easier with RxJS.
The following code snippet shows the implementation. The month and year cycling functions have been omitted for brevity:
import { fromEvent, timer, merge } from 'rxjs';
import { map, switchMap, startWith, takeUntil, filter, distinctUntilChanged } from 'rxjs/operators';

const control = document.getElementById('calendar-month-control');
const buttons = control.querySelectorAll('button');

const documentMouseup$ = fromEvent(document, 'mouseup');

const documentKeydownShifting$ = fromEvent(document, 'keydown').pipe(
  map(evt => {
    evt.preventDefault();
    return evt.shiftKey ? true : null;
  })
);

const documentKeyupShifting$ = fromEvent(document, 'keyup').pipe(
  map(evt => {
    evt.preventDefault();
    return evt.shiftKey ? null : false;
  })
);

const shifting = (initial = false) => {
  return merge(documentKeydownShifting$, documentKeyupShifting$).pipe(
    startWith(initial),
    filter(pressed => typeof pressed === 'boolean')
  );
};

const forcepress = evt => {
  evt.preventDefault();
  const next = evt.target.getAttribute('data-direction') === 'next';
  
  return shifting(evt.shiftKey).pipe(
    distinctUntilChanged(),
    switchMap(shift => {
      const period = shift ? 200 : 150;
      
      const fn = shift
        ? next ? nextYear : previousYear
        : next ? nextMonth : previousMonth;
      
      return timer(100, period).pipe(
        map(fn)
      );
    }),
    takeUntil(documentMouseup$)
  );
};

buttons.forEach($button => {
  fromEvent($button, 'mousedown').pipe(
    switchMap(forcepress)
  ).subscribe();
});
I’ll leave you to figure out how the code snippet works in this example. However, you can get a live demo on Code Sandbox.

Conclusion

RxJS is a very powerful library for composing asynchronous events and sequences. It can be used to build complex asynchronous programs that cannot be built easily using just plain JavaScript.
In this tutorial, we have learned how to implement improved force pressing (press and hold) using RxJS. Although we focused on force pressing on mouse events, the same can also be implemented for keyboard events.

Comments

Popular posts from this blog

4 Ways to Communicate Across Browser Tabs in Realtime

1. Local Storage Events You might have already used LocalStorage, which is accessible across Tabs within the same application origin. But do you know that it also supports events? You can use this feature to communicate across Browser Tabs, where other Tabs will receive the event once the storage is updated. For example, let’s say in one Tab, we execute the following JavaScript code. window.localStorage.setItem("loggedIn", "true"); The other Tabs which listen to the event will receive it, as shown below. window.addEventListener('storage', (event) => { if (event.storageArea != localStorage) return; if (event.key === 'loggedIn') { // Do something with event.newValue } }); 2. Broadcast Channel API The Broadcast Channel API allows communication between Tabs, Windows, Frames, Iframes, and  Web Workers . One Tab can create and post to a channel as follows. const channel = new BroadcastChannel('app-data'); channel.postMessage(data); And oth...

Certbot SSL configuration in ubuntu

  Introduction Let’s Encrypt is a Certificate Authority (CA) that provides an easy way to obtain and install free  TLS/SSL certificates , thereby enabling encrypted HTTPS on web servers. It simplifies the process by providing a software client, Certbot, that attempts to automate most (if not all) of the required steps. Currently, the entire process of obtaining and installing a certificate is fully automated on both Apache and Nginx. In this tutorial, you will use Certbot to obtain a free SSL certificate for Apache on Ubuntu 18.04 and set up your certificate to renew automatically. This tutorial will use a separate Apache virtual host file instead of the default configuration file.  We recommend  creating new Apache virtual host files for each domain because it helps to avoid common mistakes and maintains the default files as a fallback configuration. Prerequisites To follow this tutorial, you will need: One Ubuntu 18.04 server set up by following this  initial ...

Working with Node.js streams

  Introduction Streams are one of the major features that most Node.js applications rely on, especially when handling HTTP requests, reading/writing files, and making socket communications. Streams are very predictable since we can always expect data, error, and end events when using streams. This article will teach Node developers how to use streams to efficiently handle large amounts of data. This is a typical real-world challenge faced by Node developers when they have to deal with a large data source, and it may not be feasible to process this data all at once. This article will cover the following topics: Types of streams When to adopt Node.js streams Batching Composing streams in Node.js Transforming data with transform streams Piping streams Error handling Node.js streams Types of streams The following are four main types of streams in Node.js: Readable streams: The readable stream is responsible for reading data from a source file Writable streams: The writable stream is re...