Use an event stream of double clicks in RxJS

André Staltz
InstructorAndré Staltz
Share this video with your friends

Social Share Links

Send Tweet
Published 9 years ago
Updated 5 years ago

See a practical example of reactive programming in JavaScript and the DOM. Learn how to detect double clicks with a few operators in RxJS. We will use .bufferWhen to accumulate events into an array to determine if a double click occurred.

[00:00] We just saw previously how to transfer event streams of strings emitted over time using these maps and filters.

[00:09] Let's take a look at a more useful example. Say we have this button on the DOM, which can be clicked. Our challenge is to detect when double-clicks happen on this button. I know the DOM already has the DBL click type of event, but let's not use that. Let's actually suppose that the DOM wouldn't give that to us. The question is, "How would you typically solve this without event streams?"

[00:34] Maybe you would add an event listener to this button for the click events, and maybe when that event listener triggers, you would increment a counter, which was initialized to zero. Maybe you would set a time-out to later clear that counter back to zero, that type of approach. This is what typically most of us would do if we didn't know event streams.

[01:01] It wouldn't take three lines of code in this approach. It would take a bit more than that. Let's see the event stream approach instead, which is reactive programming. Say we somehow are able to get the event stream of sample clicks by just giving the DOM element and the event type. We just click, and this will create for us that event stream based on the DOM addEventListener.

[01:23] Now we just need to create double click stream. We add an event listener to that, and whenever we see a double-click event, we will set the label content to double click. After a second, we're going to clear that out, just for the UI purpose.

[01:44] It's actually pretty simple to achieve this with event streams. It's a matter of three operations. We buffer all of those clicks, and we end that buffer after 250 milliseconds of silence. Then these buffers return arrays for us, so we map each of these arrays to their lengths, and then we filter for those lengths, which are exactly size two.

[02:17] That's it. Double-click event stream is now ready. When we listen to this, we will see this happening. I double-clicked it, and it set the label to Double Click, and after one second it cleared it, and that's it. If I click just one, nothing happens. If I click three times or many times, then nothing happens. I need to double click, and then it sets. Nice.

[02:42] How did we do this? These operators, as you can see, they look important, and maybe you don't even know what they're doing here. How do we understand these operators? It's normally by using marble diagrams. What is that?

[02:56] Basically, imagine the simple click stream where each of these balls is the click event happening over time. The arrow indicates time. When we called buffer click stream throughout the whole 250 milliseconds, what it was it waited to 250 milliseconds of event silence to happen on this simple click event stream, and then it accumulated everything from the past into an array.

[03:24] Here we just had one. Then in this case, after 250 milliseconds of event silence happened, we accumulated all of these events into an array, and that's why we get that array, and so forth. Then we get, as a result, an event stream, which has arrays inside them, containing all of these accumulated clicks.

[03:48] Then what we do is just map the length of these arrays. Here there's just one in that array, here there's two, and then we're finally able to filter for only those lengths that are exactly two.

[04:03] These marble diagrams also come in their ASCII form. We might write them like this, so we might be using this type of notation also in some other examples. Even though you don't probably understand all of these operations now, the point is that you can create really powerful constructs with just simple operations in short amount of code.

[04:30] This is just the tip of the iceberg of what event streams can accomplish.

Michele Mendel
Michele Mendel
~ 8 years ago

I downloaded RxJS v4 and the double-click doesn't register. Switching to v2.3.22 - using https://cdnjs.cloudflare.com/ajax/libs/rxjs/2.3.22/rx.all.js - the example started work.

Michele Mendel
Michele Mendel
~ 8 years ago

Using v4 you need to use debounce instead of throttle

Olga Grabek
Olga Grabek
~ 8 years ago

Thanks!

Niklas
Niklas
~ 8 years ago

Cool, I'm not the first to stumble on the throttle/debounce thing. Thanks for already having it answered here :)

Richard Hoffmann
Richard Hoffmann
~ 8 years ago

+1 for thanks :)

Srigi
Srigi
~ 8 years ago

How this code can be modified, so the doubleClick stream emits rightaway and not after 250ms delay?

Niels Kristian Hansen Skovmand
Niels Kristian Hansen Skovmand
~ 8 years ago

Remember to use debounce() instead of throttle() -- see the code examples on the page.

Dominic Watson
Dominic Watson
~ 8 years ago

Just subscribed to pro and not cool to see out of date tutorials. Using 5-beta I expected something not to work, but this tutorial running on 2 is ancient. The JSBin itself references 4 and doesn't even work.

André Staltz
André Staltzinstructor
~ 8 years ago

Thanks Dominic for reporting this. We fixed the JSBin example by changing the use of throttle() to delay(), because in older versions of RxJS, throttle used to have some delaying behavior too.

Rafael Bitencourt
Rafael Bitencourt
~ 8 years ago

I spent quite some time trying to figure out how to do the same thing with the latest version of RxJS (5.0.0-beta.10) so I'd like to share here how I got it working:

const single$ = Rx.Observable.fromEvent(button, 'click');
single$
	.bufferWhen(() => single$.debounceTime(250))
	.map(list => list.length)
	.filter(length => length >= 2)
	.subscribe(totalClicks => {
		console.log(`multi clicks total: ${totalClicks}`);
	});

Thanks for this intro course Andre and please let me know if there's something wrong with this implementation or if there's a better way to do the same thing.

PJ
PJ
~ 7 years ago

Here is the version which works on rxjs 5.x:

const timeout = 250;
const clicks$ = Rx.Observable.fromEvent(button, "click");
const doubleClicks$ = clicks$
    .map(() => new Date())
    .bufferCount(2, 1)
    .map(([prev, next]) => (next - prev) < timeout)
    .scan((prev, next) => !prev && next, false)
    .filter(v => v)
Kostiantyn Hryshyn
Kostiantyn Hryshyn
~ 7 years ago

Would work with that code too:

    .bufferWhen(() => clickStream.debounceTime(250))
    .filter(arr => arr.length === 2);
Shaun
Shaun
~ 7 years ago

That's what I ended up with in v5 too. I'm still bothered by the fact that the 250 ms delay applies even after the second click. Native double click events fire at mouse up, not 250 ms later.

Vincent
Vincent
~ 7 years ago

@Kostiantyn that's what I found out after much time researching the doc (at least now I know what the doc looks like :) ) @andre: maybe a foreword that the library is changing a lot and that your code isn't up to date with the latest version would help.

Vincent
Vincent
~ 7 years ago

@shaun that's necessary if you want to rule out "triple clicks". If you don't, and want to fire immediately after the second click, @PJ 's implementation works well

Brendan Whiting
Brendan Whiting
~ 7 years ago

I was working in my own code editor, not jsbin, and I couldn't figure out how to get it to work with the <a> element. It kept refreshing the page, and I tried event.preventDefault() but couldn't get that to work. So I just changed it to something other than an <a> element and it worked.

jin5354
jin5354
~ 7 years ago

I think the stream need a share() operation at the end

Vamshi
Vamshi
~ 6 years ago
Khushbu
Khushbu
~ 6 years ago

why the signature of throttle and buffer is different?

throttle signature :- throttle(durationSelector: function(value): Observable | Promise)

buffer signature :- buffer(closingNotifier: Observable): Observable

buffer is used to buffer until closingNotifier emits a value.

throttle is also same as buffer where it passes the new value until observable emits a value (duration selector).

why throttle needs a function which returns observable and not taking the observable directly as in case of buffer.

Thanks for the help

Fabio Cortes
Fabio Cortes
~ 6 years ago

This is working on 6.2.1:

const { Observable, fromEvent } = rxjs
const { bufferWhen, filter, delay, debounceTime } = rxjs.operators;

const button = document.querySelector('button')
const label = document.getElementById('label')

const clickStream = fromEvent(button, 'click')
const dblClickStream = clickStream
                          .pipe(bufferWhen(() => clickStream.pipe(debounceTime(250))))
                          .pipe(filter(arr => arr.length === 2))

dblClickStream.subscribe(event => {
  label.textContent = 'double click';
  console.log('dbl click')
});

dblClickStream
  .pipe(delay(1000))
  .subscribe(suggestion => {
    label.textContent = '-';
  })
Carl
Carl
~ 6 years ago

@Fabio Thank you so much, this really helped me out! I ended up, writing it like this

var clickStream = fromEvent(button, "click");
var dblClickStream = clickStream.pipe(
  buffer(clickStream.pipe(debounceTime(250))),
  filter(clicks => clicks.length === 2)
);

One might also refactor it into a separate operator, just to demonstrate how easy it is with the new syntax.

var dblClickStream = clickStream.pipe(filterConcurrentGroupsOf(2));
function filterConcurrentGroupsOf(n, eventSilenceTime = 250) {
  return source =>
    source.pipe(
      buffer(source.pipe(debounceTime(eventSilenceTime))),
      filter(list => list.length === n)
    );
}
Tony Brown
Tony Brown
~ 2 years ago

Had to use

const clicks$ = Rx.Observable.fromEvent(button, 'click')
clicks$
	.bufferWhen(() => clicks$.debounceTime(250))
	.map(list => list.length)
	.filter(length => length >= 2)
	.subscribe(totalClicks => {
		console.log(`total clicks: ${totalClicks}`);
	});
Tony Brown
Tony Brown
~ 2 years ago

Too bad the course is outdated :(

Markdown supported.
Become a member to join the discussionEnroll Today