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

How to use Ngx-Charts in Angular ?

Charts helps us to visualize large amount of data in an easy to understand and interactive way. This helps businesses to grow more by taking important decisions from the data. For example, e-commerce can have charts or reports for product sales, with various categories like product type, year, etc. In angular, we have various charting libraries to create charts.  Ngx-charts  is one of them. Check out the list of  best angular chart libraries .  In this article, we will see data visualization with ngx-charts and how to use ngx-charts in angular application ? We will see, How to install ngx-charts in angular ? Create a vertical bar chart Create a pie chart, advanced pie chart and pie chart grid Introduction ngx-charts  is an open-source and declarative charting framework for angular2+. It is maintained by  Swimlane . It is using Angular to render and animate the SVG elements with all of its binding and speed goodness and uses d3 for the excellent math functio...

Understand Angular’s forRoot and forChild

  forRoot   /   forChild   is a pattern for singleton services that most of us know from routing. Routing is actually the main use case for it and as it is not commonly used outside of it, I wouldn’t be surprised if most Angular developers haven’t given it a second thought. However, as the official Angular documentation puts it: “Understanding how  forRoot()  works to make sure a service is a singleton will inform your development at a deeper level.” So let’s go. Providers & Injectors Angular comes with a dependency injection (DI) mechanism. When a component depends on a service, you don’t manually create an instance of the service. You  inject  the service and the dependency injection system takes care of providing an instance. import { Component, OnInit } from '@angular/core'; import { TestService } from 'src/app/services/test.service'; @Component({ selector: 'app-test', templateUrl: './test.component.html', styleUrls: ['./test.compon...

How to solve Puppeteer TimeoutError: Navigation timeout of 30000 ms exceeded

During the automation of multiple tasks on my job and personal projects, i decided to move on  Puppeteer  instead of the old school PhantomJS. One of the most usual problems with pages that contain a lot of content, because of the ads, images etc. is the load time, an exception is thrown (specifically the TimeoutError) after a page takes more than 30000ms (30 seconds) to load totally. To solve this problem, you will have 2 options, either to increase this timeout in the configuration or remove it at all. Personally, i prefer to remove the limit as i know that the pages that i work with will end up loading someday. In this article, i'll explain you briefly 2 ways to bypass this limitation. A. Globally on the tab The option that i prefer, as i browse multiple pages in the same tab, is to remove the timeout limit on the tab that i use to browse. For example, to remove the limit you should add: await page . setDefaultNavigationTimeout ( 0 ) ;  COPY SNIPPET The setDefaultNav...