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.
Related
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.
I have a test for mergemap, But it does not return the correct expect result. Can someone help me?
Thanks!
it("should maps to inner observable and flattens", () => {
const values = { a: "hello", b: "world", x: "hello world" };
const obs1 = cold( "-a---a|", values);
const obs2 = cold( "-b-b-b-|", values);
const expected = cold("--x-xxxx-x-|", values);
const result = obs1.pipe(mergeMap(x => obs2.pipe(map(y => x + " " + y))));
expect(result).toBeObservable(expected);
I will try to break down your test so you can understand what's going on and where you're doing things wrong. I'm really sorry, but I won't be using jasmine-marbles library, but a preferred way of writing tests (I'd also recommend you to avoid jasmine-marbles library).
When converted to plain, old marbles (not using jasmine-marbles library), your test looks like this:
import { mergeMap, map } from 'rxjs/operators';
import { TestScheduler } from 'rxjs/testing';
describe('mergeMap', () => {
let testScheduler: TestScheduler;
beforeEach(() => {
testScheduler = new TestScheduler(someMatcher);
});
it('should maps to inner observable and flattens', () => {
testScheduler.run(({ cold, expectObservable }) => {
const values = { a: 'hello', b: 'world', x: 'hello world' };
const obs1 = cold('-a---a| ', values);
const obs2 = cold('-b-b-b-| ', values);
const expected = ' --x-xxxx-x-|';
const result = obs1.pipe(mergeMap(x => obs2.pipe(map(y => x + ' ' + y))));
expectObservable(result).toBe(expected, values);
});
});
});
And this test is failing. Here is why: you're expecting your test to emit at frames 2, 4, 5, 6, 7, 9 and a complete at frame 11. But, the actual emissions happen at frames 2, 4, 6, 6, 8, 10 and a complete at frame 12. Now, to be able to visually understand why and how this is happening, I will write a test with couple of comments and I will align them a different way so you get a better filling of what happens:
const obs1 = cold('-a---a| ', values);
const obs2 = cold(' -b-b-b-| ', values);
// -b-b-b-|
const expected = ' --x-xxxx-x-| ';
// frames: 0123456789012
Basically, in mergeMap, you're returning an instance of obs2 when the source observable emits. In this case, the source is obs1. When it emits the first value (a), at frame 1, mergeMap internally subscribes to obs2 - this is why I aligned the start of obs2 emissions to be below a at frame 1. Emissions from obs2 are the ones that get to the consumer.
Similarly, when obs1 emits the second value, at frame 5, another subscribe to obs2 happens and, since obs2 is a cold observable, another producer is instantiated, so another stream starts to flow. This is why I added a comment to indicate when the second subscribe to obs2 happens. It starts at frame 5, right when the second a is emitted from obs1. Similarly, emissions from the second subscribe to obs2 are the ones that get to the consumer.
So, combining this, we get to conclusion where the expected frames should be:
-b-b-b-| emits at frames: 2, 4 and 6 and a complete at frame 8
-b-b-b-| emits at frames: 6, 8 and 10 and a complete at frame 12
0123456789012
Based on this, the final emissions happen at frames 2, 4, 6, 6, 8 and 10 and the complete happens at frame 12. The problem with this setup is that it is not possible to show an emission which comes early after two or more emissions that come at the same frame.
This is to say, emission at frame 8 is too close to the two emissions at frame 6. The reason being is that emissions that happen at the same frame are grouped with brackets () in marble diagrams and brackets are somewhat hiding some number of emissions. This is your case:
-b-b-b-|
-b-b-b-|
--x-x-(xx)x-| // brackets start at frame 6 and represent grouped emissions which all happen at frame 6
0123456666012 // the frames 7, 8 and 9 are "hidden"
Frames 7, 8 and 9 are hidden and can't be represented so emissions at these frames can't be shown in marble diagrams, no matter what. And, since there is an emission at frame 8 which gets lost, you can't create a proper marble diagram for this expected emissions.
In order for this test to pass, you could emit the second a from the obs1 one frame further, at frame 6. Now, your test could look like this (and it should pass now):
testScheduler.run(({ cold, expectObservable }) => {
const values = { a: 'hello', b: 'world', x: 'hello world' };
const obs1 = cold('-a----a| ', values);
const obs2 = cold(' -b-b-b-| ', values);
// -b-b-b-|
const expected = ' --x-x-xx-x-x-|';
const result = obs1.pipe(mergeMap(x => obs2.pipe(map(y => x + ' ' + y))));
expectObservable(result).toBe(expected, values);
});
Is there an easy way to create an observable that is an offset of another?
For example, let's say Observable1 emits [1,2,3,4,5].
How would I create Observable2 from Observable1 such that Observable2 emits [1,2,3,4] (so, it emits nothing when Observable1 emits 1, it emits 1 when Observable1 emits 2, etc.)
Obs1 Start
-------1--------2----------3------------4-------------5----------------->
Obs2 Start
-----------------1----------2------------3-------------4----------------->
Thanks!
You can use the bufferCount and map operators to do what you want:
const source = new Rx.Subject();
const offset = source.bufferCount(2, 1).map(values => values[0]);
source.subscribe(value => console.log(`source = ${value}`));
offset.subscribe(value => console.log(`offset = ${value}`));
source.next(1);
source.next(2);
source.next(3);
source.next(4);
source.next(5);
.as-console-wrapper { max-height: 100% !important; top: 0; }
<script src="https://unpkg.com/rxjs#5/bundles/Rx.min.js"></script>
bufferCount(2, 1) will emit a two-value buffer each time a value is received - once there are two values in the buffer, that is. So the first value in the buffer will be offset by one source emission.
You could substitute bufferCount(2, 1) with pairwise() which does the same thing.
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') );
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