I want to change the size of an element while it is being scrolled (touch pan, etc). In order to do that I intend to use touch events as observable sources. The size should grow when user starts to pan the element and should shrink 5s after user stopped.
In other words:
Given the streams of 'start' and 'end' events, immediately do the action on 'start' and delay the action on 'end'. But if during that delay new event 'start' come, the delay should be cancelled.
Here is marble, the best I could do.
PS
Feel free to suggest a better title. I struggle to come up with something descriptive enough.
Assuming that start$ and end$ are the Observable streams representing the start and end events, than I would probably proceed like this
start$.pipe(
// use tap to implement the side effect of growing the size of an element
// the event is passed assuming that it contains somehow the reference to the element
// whose size has to grow
tap(event => growSize(event)),
// any time a start event is notified, we pass the control to the end$ stream
// if a new start event is notified, the subscription to end$ will be unsubscribed
// a new subscription will be establishes, which means that a new delay will start
switchMap(() =>
end$.pipe(
// delay 5 seconds
delay(5000),
// decrease the size of the element as a side effect
tap(event => decreaseSize(event)),
)
)
)
.subscribe()
You could merge your start and end triggers and use debounceTime with filter:
merge(start$, end$).pipe(
debounceTime(5000),
filter(event => event === 'end')
);
Here's a little StackBlitz to play with.
Related
I am trying to understand behavior of concatMap() operator. I wrote some example code
import './style.css';
import { concatMap, fromEvent, tap } from 'rxjs';
import { first } from 'rxjs/operators';
const button1 = document.getElementById('button1');
const button2 = document.getElementById('button2');
const button3 = document.getElementById('button3');
const obs1 = fromEvent(button1, 'click');
const obs2 = fromEvent(button2, 'click');
const obs3 = fromEvent(button3, 'click');
obs1
.pipe(
concatMap((obs1value) => {
console.log('obs1:', obs1value);
return obs2; // wait for this to complete before merging next value
}),
concatMap((obs2value) => {
console.log('obs2', obs2value);
return obs3; // wait for this to complete before merging next value
})
)
.subscribe((obs3value) => {
console.log('obs3:', obs3value);
});
// button2.click() // nothing
// button3.click() // nothing
button1.click(); // obs1
button2.click(); // obs2
button3.click(); // obs3
button1.click(); // nothing -???
button2.click(); // nothing -???
button3.click(); // obs3
button3.click(); // obs3
button3.click(); // obs3
Initially after button 1, button 2 and button 3 are clicked, I can see the print statements. However after that for button1 and button2, there are no print statements, it is observed only in case of button3. I see RxJS documentation says concatMap() would be "...waiting for each one to complete before merging the next". But since obs1, obs2 and obs3 have not completed yet, why do I see the print statement even once?
If I change the code slightly https://stackblitz.com/edit/rxjs-zggqxc, to complete observable as below
obs3.pipe(first());
Now licking button1, why is nothing is printed?
button1.click(); // obs1
button2.click(); // obs2
button3.click(); // obs3
button1.click(); // nothing -???
button2.click(); // obs2
button3.click(); // obs3
button3.click(); // nothing
button3.click(); // nothing
button2.click(); // obs2
When you call button1.click(); second time the next notification is emitted but obs2 hasn't completed yet so the project function in concatMap() isn't invoked. Only if obs2 completed then concatMap() would pop the oldest notification stacked and passed it to its project function.
If you log next notifications comming from obs1 you'll see the event being emitted.
...
obs1
.pipe(
tap(console.log),
concatMap((obs1value) => {
console.log('obs1:', obs1value);
return obs2; // wait for this to complete before merging next value
}),
...
Notice you are using concatMap, with the emphasis on map.
Mapping will not just concatenate all observables but replace (map) items from the previous observable with a sequence of other items.
For each item from your "outer" observable which is actually mapped to another "inner" observable, your map callback (parameter of concatMap(...)) is executed once when the mapping actually occurs.
When are those mappings performed? - concatMap will take one item from the outer observable and then defers handling all other items from the outer observable until the inner observable (returned by the map callback) completes. The outer observable's items are indeed buffered until the inner observable completes.
Side note: In a synchronous scenario this is roughly comparable to SelectMany from C# or flatMap from Java Streams API.
Let me describe your first use scenario:
Each item in obs1 will be replaced by the sequence of items from obs2.
When this replacement is evaluated by pressing button1 (you might have implemented it in a way which depends on the value you received from obs1!), the first log output becomes visible.
Further clicks are on button1 will be ignored until obs2 has completed.
Each item of obs2 will be replaced by the sequence of items from obs3.
Again, when the replacement gets evaluated by pressing button2, the second log output becomes visible.
Further clicks on button2 will be ignored until obs3 has completed.
Therefore, when pressing button1, button2 and button3 you are essentially just subscribed to obs3.
As obs3 never finishes, you will indeed never see any other output.
In your second scenario the observable you map to obs2 is obs3.pipe(take(1)) which completes after one click on button3.
As soon as that happens, the pipeline will will continue to map items from obs2. The next mapping happens as soon as one item in obs2 becomes available (which is immediately, if button2 has been clicked intermediately or later as soon as it gets clicked) and then one click to button3 is awaited again.
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.
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') );
I was wondering how to handle the fact that an interrupted transition within d3.js does not trigger an end event. As the API doc says
Note that if the transition is superseded by a later-scheduled
transition on a given element, no end event will be dispatched for
that element; interrupted transitions do not trigger end events.
from: https://github.com/mbostock/d3/wiki/Transitions#control
In my case transitions are triggered by user interaction. These transitions might be interrupted when the user triggers a new transition through mouse click. Let's say in the first transition an element was meant to fade out and be removed at the end of the transition. If this transition is interrupted the element will never be removed. I could disallow further user interaction during the time a transition happens yet that is not really what I want (particular as i have back and forward buttons which allow the user to click through previous states of my svg graph quickly ... ) Basically I would need an "Interruption Event"
Thanks
martin
I think there is no really satisfactory way to do this. A little bit painful workaround would be counting the number of transitions currently taking place and reasoning from that.
So, initialize:
var transitionCount = 0;
And whenever you schedule new transitions:
if ( transitionCount != 0 ) {
// handle interrupted transitions here somehow
transitionCount = 0;
}
var myTransition = selection.transition().... ;
transitionCount += myTransition.size();
myTransition.each('end', function() { transitionCount --; });
If you can handle manually cleaning up interrupted transitions like this, this would be fine. Notice, that you can't use 'start' events to increment the counter as there is a delay between scheduling a transition and it being started so you'd get a race condition there.
I have very large conten (a lot of pictures). I want to use css3 transition transform.
I bind to touchmove event, and calculate new x position.
But I have issue: the position by x does not change smoothly on mobile (on desctop all ok).
Event Touch comes with delays.
I try hidde don't used content (vissability: hidden) its not help.
Try throttling how fast the event handler makes the changes. It's counter intuitive, but by slowing things down, you will make them appear faster. The touchmove events are firing incredibly fast, far faster than the refresh rate of the screen.
Ideally, you would use requestAnimationFrame to do that.
Or if that is too experimental for your taste, simply make sure you are not performing your event handler actions faster than 60 frames per second (1 event handler action every 1000/60 milliseconds, the screen's refresh rate). If you still have problems at 1000/60 milliseconds, try a slower rate. The frame rate of video is 30 frames per second. Something between 30 and 60 frames should look good.
So that looks like this:
var lastFrameTime;
var myTouchMoveHandler = function (e) {
var now = Date.now();
if (now < lastFrameTime + 1000/60) {
return;
}
// your event handler code afterward
// ...
// then update the lastFrameTime
lastFrameTime = now;
}