Click and Longclick with RxJS

It has been quite a while since I have written my last article. There has been tons of work, private things going on and overall too less time to do fun stuff. I've also joined Ranorex as a senior developer. Ranorex is providing a product for UI automation of desktop, web and mobile applications. The work has so far provided a lot of opportunities to try out new stuff and get in touch with other technologies.

Recently I've been working a lot with the Reactive Extensions (Rx) for JavaScript, a library for composing asynchronous and event-based apps using observable sequences. A rather important fact is the focus on events, which makes it an ideal candidate for working with modern web applications, powered by alternative gestures and usage of shortcut keys. One of those gestures I was working on was a longer lasting click, lets call it longclick.

Why RxJS

First of all lets discuss why RxJS is a good choice for this type of tasks. There are already tons of concepts like Promises, Event-Listeners, Callbacks and many more which help you to work with asynchronous tasks and occurring DOM events. RxJS essentially tries to unify all those concepts into something called Streams. Now instead of rephrasing other sources let me re-post here what I think is one of the best explanations of what is going on, which is part of the freely available Rx-Book:

In a way, this isn't anything new. Event buses or your typical click events are really an asynchronous event stream, on which you can observe and do some side effects. Reactive is that idea on steroids. You are able to create data streams of anything, not just from click and hover events. Streams are cheap and ubiquitous, anything can be a stream: variables, user inputs, properties, caches, data structures, etc. For example, imagine your Twitter feed would be a data stream in the same fashion that click events are. You can listen to that stream and react accordingly.

On top of that, you are given an amazing toolbox of functions to combine, create and filter any of those streams. That's where the "functional" magic kicks in ...

Reactive Programming is programming with asynchronous data streams: GitBook Rx-Book

How to capture a longclick

If we think of a longclick we quickly realize that it is a stream sequence as depicted by the following diagram.

        eventStream: ---md-------------------lc----|->
                           [ delay of 700ms]
interruptingMouseUp: ----------mu-| X |--------mu--|->

Longclick diagram

It starts with a trigger of a mousedown event. Now if the mouse is kept pressed for a duration of 700ms the result is a longclick. If on the other hand in the meantime a mouseup event occurs, the user didn't wait long enough and thus triggered a simple click.

The html we'll use for the demo is pretty simple, consisting of a header displaying the click or longclicked item, followed by three divs acting as click targets.

<h1>Captured event: <span id="captured_event">None</span></h1>
<div>Item 1</div>
<div>Item 2</div>
<div>Item 3</div>

Now lets focus on the code. First we get a reference to the clickable divs. Next we have a boolean stating whether a longClick has been detected which by default is not the case. The longclick is essentially the previously mentioned diagram. The primary source are mousedown events on the target divs. Note that we use Rx.DOM as a helper library to get up and running with DOM event listening quicker. Next we need to transform each item of the stream using a flatmap. We first add a class to the body called spinner which will set the cursor property to pointer, thus signaling the begin of a longclick. The flatmap returns observables, which themselve return the original MouseEvent. Now if a delay of 700 ms is reached before a mouseup event happens the actions counts a full longclick.

let divs = document.querySelectorAll("div");
let longClickDetected = false;

let longClick = Rx.DOM.mousedown(divs, null, true)
  .flatMap((e) => {
    document.body.classList.add("spinner");
    return Rx.Observable
      .return(e)
      .delay(700)
      .takeUntil(Rx.Observable.fromEvent(document.body, "mouseup"));
  });

After defining the longClick stream, we can subscribe. On each item we now declare a longClick as detected, remove the spinner and update the header element with the target items textContent.

longClick.subscribe((longClick) => {
  longClickDetected = true;
  document.body.classList.remove("spinner");
  document.getElementById("captured_event")
    .innerHTML = `Longclick on ${longClick.target.textContent}`;
});

At the same time we also subscribe to a simple click event and revert the longClickDetected boolean in case it was previously set by a longclick. This needs to be done because the longclick will actually also fire a normal click event as well. The next time though a simple click happens we can update the header element again and state that a normal click happened.

Rx.DOM.click(divs, null, true).subscribe((clickEvent) => {
  if (longClickDetected) {
    longClickDetected = false;

    return;
  }
  document.body.classList.remove("spinner");
  document.getElementById("captured_event")
    .innerHTML = `Click on ${clickEvent.target.textContent}`;
});

You can see the full code RxJS longclick and click on CodePen.

Conclusion

So I hope that you've seen the benefit of RxJS and how it can help you to achieve cool things with DOM Events. As always I'm happy about your feedback which you may just leave as a comment below.

photo credit: Maurizio Viroli: principi fondamentali via photopin (license)