RxJS Buffer, how to group multi click events as stream - rxjs

In RxJS version 2.2.26
The following code produced a stream that would emit the number of clicks (double click, triple click etc..)
var multiClickStream = clickStream
.buffer(function() { return clickStream.throttle(250); })
.map(function(list) { return list.length; })
.filter(function(x) { return x >= 2; });
In RxJS version 4.0.6
This code no longer produces the desired result.
How can I get that same functionality in RxJS 4.0.6?
Working 2.2.26 Example
Broken 4.0.6 Example

There is actually a much better way to find double clicks via RxJs:
var mouseDowns = Rx.Observable.fromEvent(button, "mousedown");
var doubleClicks = mouseDowns.timeInterval()
.scan<number>((acc, val) => val.interval < 250 ? acc + 1 : 0, 0)
.filter(val => val == 1);
The benefit of this is that you don't need to wait for the full 250ms to recognize the double click - if the user has only 120ms between clicks there is a noticable delay between the double click and the resulting action.
Notice, that through counting up (with .scan()) and filtering to the first count we can limit our stream to just double-clicks and ignore every fast click after that - so click click click will not result in two double clicks.

Just in case someone is wondering on how to do the same thing with RxJS 5 (5.0.0-beta.10), this is 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}`);
});
I spent a lot of time trying to figure this out as I'm also learning Reactive Programming (with RxJS) so, if you see a problem with this implementation or know of a better way to do the same thing, I'll be very glad to know!

Detect single or double-clicks, but not multi-clicks
Here's a solution I came up with that creates a stream of single or double clicks (not anything more, so no triple clicks). The purpose is to detect multiple double clicks in quick succession, but also to receive notifications of single clicks.
let click$ = Observable.fromEvent(theElement, "click");
let trigger$ = click$.exhaustMap(r =>
click$.merge(Observable.timer(250)).take(1));
let multiclick$ = click$.buffer(trigger$).map(r => r.length);
The reasoning is this, we use the buffer operator to group clicks, and design a buffer trigger event as follows: Every time a click happens, we start a race between two observables:
A 250 msec timer
The original click stream
Once this race concludes (we use take(1) because we are only interested in the first event) we emit the buffer trigger event. What does this do? It makes the buffering stop either when a 2nd click arrives, or when 250 msec have elapsed.
We use the exhaustMap operator to suppress the creation of another race while one is already going on. This would otherwise occur for the 2nd click in a double-click pair.

Here's another take (produces N doubleclicks if the user quickly clicks 2*N times):
const clicks = fromEvent(document, 'click');
const doubleclicks = clicks.pipe(
exhaustMap(() => clicks.pipe(takeUntil(interval(250)))),
);

This answer is inspired by #Aviad P.
I believe the following code is the right answer. It meets all these requirements.
It differentiates single-click, double-click, and triple-click.
No unrecognized clicks will be streamed. i.e. If you click the mouse 6 times quickly, you will get 2 triple-clicks. If you click 5 times, you get one triple-click and one double-click. etc
triple-click will be fired immediately, no need to wait for the whole time-window complete.
No clicks will be lost.
It's easy to make it accept four-click, five-click...
const btn = document.querySelector('#btn');
const click$ = Rx.Observable.fromEvent(btn, "click");
const trigger$ =
click$.exhaustMap(r =>
click$
.take(2)
.last()
.race(click$
.startWith(0)
.debounceTime(500)
.take(1))
);
click$
.buffer(trigger$)
.map(l => l.length)
.map(x => ({
1: 'single',
2: 'double',
3: 'tripple'
}[x]))
.map(c => c + '-click')
.subscribe(log);
Rx.Observable.fromEvent(document.querySelector('#reset'), 'click')
.subscribe(clearLog);
function log(message) {
document.querySelector('.log-content').innerHTML += ('<div>' + message + '</div>');
}
function clearLog() {
document.querySelector('.log-content').innerHTML = null;
}
.log {
padding: 1rem;
background-color: lightgrey;
margin: 0.5rem 0;
}
.log-content {
border-top: 1px solid grey;
margin-top: 0.5rem;
min-height: 2rem;
}
.log-header {
display: flex;
justify-content: space-between;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/5.5.7/Rx.js"></script>
<button id='btn'>Click Me</button>
<button type="button" id='reset'> Reset </button>
<div class='log' id='logpanel'>
<div class='log-header'>
Log:
</div>
<div class="log-content"></div>
</div>

A more generic solution: starting from a source observable, detect and propagate when this observable quickly emits the exact same value (at least) twice quickly in a row:
var scheduler = Rx.Scheduler.default; // replace with TestScheduler for unit tests
var events = // some observable
var doubleClickedEvents = events
/* Collect the current and previous value */
.startWith(null) // startWith null is necessary if the double click happens immediately
.pairwise()
.map(pair => ({ previous: pair[0], current: pair[1] })
/* Time the interval between selections. */
.timeInterval(scheduler)
/* Only accept two events when they happen within 500ms */
.filter(timedPair => timedPair.interval < 500)
.map(timedPair => timedPair.value)
/* Only accept two events when they contain the same value */
.filter(pair => pair.previous && pair.previous === pair.current)
.map(pair => pair.current)
/* Throttle output. This way, triple and quadruple clicks are filtered out */
.throttle(500, scheduler)
The difference with the provided solutions is:
Like Wolfgang's solution, this observable will immediately emit the value at the exact moment a 'double click' happens, not after a debounce time of 500ms
Does not needlessly accumulate the amount of clicks. If you ever need to implement 'Triple-click' behavior, then Wolfgang's solution is better. But for double click behavior, this can be replaced with startWith(null).pairwise()
Uses throttle to hold back multiple double clicks.
Actually propagates the values that are emitted from the original events, not just a 'signal'.

throttle was changed to debounce starting in RxJS 3 I believe.
There was a big debate about this way back when because the original naming scheme didn't actually match with other implementations of Rx or the actual definition.
https://github.com/Reactive-Extensions/RxJS/issues/284
And it even managed to confuse people further down the road in the reimplementation of RxJS 5:
https://github.com/ReactiveX/rxjs/issues/480

Found a way fit better to distinguish between single / double click. (So I can get either Single-Click or Double-Click, but not both)
For double click, this handled fast double click 2*N times produce N events:
Observable.prototype.doubleEvent = function(timing = 150, count = 2, evt = this) { /// flexibility to handle multiple events > 2
return this.exhaustMap( () => /// take over tapping event,
evt.takeUntil( Observable.timer(timing) ) /// until time up,
.take(count-1) /// or until matching count.
);
}
Note: argument evt = this make doubleEvent works with startWith(), to take this Observable correctly.
Usage:
this.onTap
.doubleEvent()
.subscribe( (result) => console.log('double click!') );
For distinguish between single / double click:
Observable.prototype.singleEvent = function(timing = 150) {
return this.mergeMap( /// parallel all single click event
() => Observable.timer(timing) /// fire event when time up
);
}
Observable.prototype.singleOrDoubleEvent = function(timing = 150) {
return this.exhaustMap( (e) => /// take over tapping event
Observable.race( /// wait until single or double event raise
this.startWith(e).doubleEvent(timing, 2, this)
.take(1).mapTo(1),
this.startWith(e).singleEvent(timing)
.take(1).mapTo(0),
)
);
}
Usage:
this.onTap
.singleOrDoubleEvent()
.subscribe( (result) => console.log('click result: ', result===1 ? 'double click' : 'single click') );

Related

Rxjs throttleTime - update the throttling in realtime?

Is there a way to update the throttleTime in Rxjs. You're scrolling near the bottom of the page, so we only need throttle every 400ms, while as you get nearer the top, we increate to 100ms as the target area is in that region
this.scrollSubscription$ = this.scrollSubject$
.asObservable()
.throttleTime(200, undefined, throttleTimeConfig)
.subscribe(event => this.throttledScrollHandler(event));
What I am going to do is have two separate subscripbtions, as I cannot find a way of doing it.
this.slowerSubscriber$ = this.scrollSubject$
.asObservable()
.throttleTime(400,.......
Thought I would ask as I can't think of another way of achieving it
You can use throttle to implement your own silencing duration:
// Make this function extract the position using scrollY, page offsets or whatever you want to use
const getScrollPositionFromEvent = event => 42;
this.scrollSubject$.asObservable()
.throttle(event => Observable.interval(
getScrollPositionFromEvent(event) <= 100 ? 100 : 400
))
.subscribe(console.log);
You can see a working example here. It simply emits by a given input, which in your case would be based on the scroll offset.

rxjs one sequence multiple subscriptions take x

Let's say I have an rxjs observable created from an array of values:
let obs = Observable.fromArray([1,2,3,4,5,6]);
on some button click, we do this:
obs.take(2).toArray().subscribe(x => {
console.log('the value is:', x); // and the value will be [1,2]
})
The button is clicked again, and again we get [1,2].
The desired behavior is, however, to receive [3,4], and then [5,6].
How would you go about doing this in rxjs?
Thanks!
Rather than taking out a subscription when the button is clicked, I would model the clicks as a stream with something like this:
const clicks = Rx.Observable.fromEvent(document.getElementById('trigger'), 'click');
You can use bufferWithCount(2) to reduce your obs stream in the desired way. You can then combine the streams as appropriate. For example, if you'd like to match each click with a new set (1:1, without skipping any), you could use zip:
const subscription = clicks.zip(obs.bufferWithCount(2), (x, y) => y)
.subscribe(x => console.log(x));
Here's a working example: http://jsbin.com/vonegoq/1/edit?js,console,output

Double tap recognition takes too long? (Hammer.js 2.0)

I am programming a highly responsive web application and came across the issue that most time is used to recognize double taps.
I am using this code from the website:
var singleTap = new Hammer.Tap({ event: 'singletap' });
var doubleTap = new Hammer.Tap({event: 'doubletap', taps: 2 });
hammer.add([doubleTap, singleTap]);
doubleTap.recognizeWith(singleTap);
singleTap.requireFailure(doubleTap);
This basically works quite fine. However, due to the timeouts/intervals the recognition of a double tap takes quite "long". I guess its about 2 times the interval - one for each tap.
The waiting for the last interval (waiting for a third tap) is senseless in my scenario.
Is there any "ok tapCount == 2, we fire now and don't wait any longer"-TapRecognizer option?
Update, I have done some logging:
First column: passed ms since first event
0 input: mousedown
74ms input: mouseup
145ms input: mousedown
218ms input: mouseup
520ms double tap
-
0 input: mousedown
64ms input: mouseup
366ms single tap
This confirms my theory that double tap is waiting for a third click but I don't think there's an option to disable this.
I share my solution to the problem:
I copied the TapRecognizer and named it DblTapRecognizer. The interesting source code lines are:
if (tapCount === 0) {
// no failing requirements, immediately trigger the tap event
// or wait as long as the multitap interval to trigger
if (!this.hasRequireFailures()) {
return STATE_RECOGNIZED;
} else {
this._timer = setTimeoutContext(function() {
this.state = STATE_RECOGNIZED;
this.tryEmit();
}, options.interval, this);
return STATE_BEGAN;
}
}
"if (!this.hasRequireFailures())" seems to misbehave in my situation, since the comment hints at immediate firing... So just "return STATE_RECOGNIZED;" and delete the rest for the DblTapRecognizer.
We ran into similar slowness issues. Apparently there is an inherent lag on tap action on touch devices.
We ended up using FastClick
All you need to do is FastClick.attach(document.body);
This improved the "tap performance" for us.

For loop with events

I'm working with along with an online tutorial and trying to understand the code below. What I don't get is why this works more than two times. When the loop has been executed two times i == len and the condition i < len isn't true anymore. So how come it's possible to toggle the different clases more than two times?
My guess is that when the condition is false i gets set to 0 again, did I understand that correctly? Hope someone can help me, I didn't find an explanation of this particular problem anywhere online.
HTML
<button>Normal</button>
<button>Changed</button>
CSS
.normal {background-color: white;color:black;}
.changed {background-color: black;color:white;}
JavaScript
(function() {
var buttons = document.getElementsByTagName("button");
for (var i = 0, len = buttons.length; i < len; i +=1)
buttons[i].onclick = function() {
var className = this.innerHTML.toLowerCase();
document.body.className = className;
}}
}());
The for loop gets excecuted only once and iterates through all buttons.
In the for loops body, you define an onclick function for each button.
So, before you can click anywhere the loop already has finished, and added an onclick function to each single button, which will be called everytime, you click on that button.
With button[i].onclick = function() {...} you add an event handler function to the buttons click event.
You should read more about event handlers in general.

Interpret "double tap" from TouchLocation stream using Rx

I have an IObservable<TouchLocation> and am trying to derive gestures from it.
Was able to pull out Taps but having trouble wrapping my head around Double Tap.
TouchLocation has Position and State so Tap is derived by aggregating these based upon two criteria.
1) Position doesn't move more than 10px in any direction (allow for some movement but isn't a drag)
2) Starts on an event with "Pressed" state and continues until one comes in with "Released" state
3) Where the "release" happens within 1 second of the "press"
Now I have an IObservable<List<TouchLocation>> (result of the aggregation) and need to produce the double tap.
How can I return the first gesture (to produce the initial tap) and then substitute the second for a double tap gesture, but only if a second Tap is received before a 1 second timer. If received after the timer then it should also be a simple Tap.
A fun problem.
// Setup some parameters
var singleTapInterval = TimeSpan.FromSeconds(0.5);
var dblTapInterval = TimeSpan.FromSeconds(1);
// the source of touches
IObservable<TouchLocation> txs;
// Get single taps
// 0, 1, but no more than
// 2 touches in the timespan
IObservable<TouchLocation> taps = txs.Buffer(2,singleTapInterval)
.Where(touches => touches.Count == 2 &&
touches[0].state == "Pressed" &&
touches[1].state == "Released" &&
distance(touches) < distanceThreshold)
.Select(touches => touches[1]);
// now detect double taps
IObservable<TapKind> dbltaps = taps.Buffer(2,dblTapInterval)
.Where(taps => taps.Count == 2)
.Select(_ => TapKind.SingleTap);
// detect single taps - taps must be _slower_ than the dblTapInterval
// for this to work
IObservable<TapKind> singletaps = taps.Throttle(dblTapInterval)
.Select(_ => TapKind.SingleTap);
// compbine the two, so we see either tap condition
IObservable<TapKind> interestingTaps = dbltaps.Merge(singletaps);
I think the best answer I can give you is "don't do that".
XNA has a built-in mechanism for providing tap, double-tap, and other gestures. These match the system-wide gesture behaviour and are vastly more preferable to implementing your own gesture recogniser.
See this document on MSDN for an explanation of how to use the built-in gesture functionality. It's basically a case of setting TouchPanel.EnabledGestures, polling IsGestureAvailable, and then calling ReadGesture.
Went back to the beginning and realized I'd been vastly overcomplicating it... thanks to Scott for helping reset my approach!
"target" is an IObservable<TouchLocation> (in an extension method).
This setup handles sending the first event (so we don't lose the first event while the zip is pending), ignores holds (tap that takes more than 1 second), replaces the second tap with double tap (but only if it happened within 1 second of the tap) and covers a new req that I didn't realize before which makes sure that double tap only follows a tap and never another double tap (4 taps was producing Tap | DTap | DTap | DTap and now is Tap | DTap | Tap | DTap which is correct behavior)
Will probably simplify this down a bit more but this is pre-refactor working version:
var grouped = (from t in target
group t by t.Id into groups
select groups);
var presses = (from g in grouped
from t in g
where t.State == TouchLocationState.Pressed
select t).Timestamp();
var releases = (from g in grouped
from t in g
where t.State == TouchLocationState.Released
select t).Timestamp();
var pressAndRelease = presses.Zip(releases, (press, release) =>
{
return new { Press = press, Release = release };
})
.Where(pr =>
{
var delta = (pr.Release.Timestamp - pr.Press.Timestamp).TotalSeconds;
return delta < 1;
})
.Timestamp();
var zipped = pressAndRelease.Zip(pressAndRelease.Skip(1), (prev, cur) =>
{
return new { Previous = prev, Current = cur };
});
pressAndRelease.TakeUntil(zipped).Subscribe(a =>
{
Debug.WriteLine("FIRST TAP!");
});
var wasDoubleTap = false;
zipped.Subscribe(a =>
{
var delta = (a.Current.Timestamp - a.Previous.Timestamp).TotalSeconds;
if (wasDoubleTap || delta > 1)
{
Debug.WriteLine("TAP");
wasDoubleTap = false;
}
else
{
Debug.WriteLine("DOUBLE TAP!");
wasDoubleTap = true;
}
});

Resources