takeUntil failing to prevent emissions from observable - rxjs

I am trying to create my own click, hold and drag events using Rxjs and the mousedown, mouseup and mousemove events. My attempts use a number of streams that begin with a mousedown event, each with a takeUntil that listens for emissions from the other streams. Basically once one of the streams has "claimed" the action (i.e. passed all the requirements and emitted a value) the other observables should complete with no emissions.
I have looked at other answers and thought it might have something to do with the timer running async but it happens between streams that do not rely the timer e.g. drag and click. I have been playing around in codesandbox.io using rxjs v6.
The takeUntil's also have to sit on the inner observables as I don't want the outer observables to run once and complete.
The code is shown below:
const mouse_Down$ = fromEvent(document, "mousedown").pipe(
tap(event => event.preventDefault())
);
const mouse_Up$ = fromEvent(document, "mouseup").pipe(
tap(event => event.preventDefault())
);
const mouse_Move$ = fromEvent(document, "mousemove");
const mouse_drag$ = mouse_Down$
.pipe(
mergeMap(mouseDownEvent =>
mouse_Move$.pipe(takeUntil(merge(mouse_Up$, mouse_Hold$, mouse_drag$)))
)
).subscribe(event => console.log("Drag"));
const mouse_Hold$ = mouse_Down$
.pipe(
mergeMap(mouseDownEvent =>
timer(1000).pipe(takeUntil(merge(mouse_drag$, mouse_Click$)))
)
).subscribe(event => console.log("Hold"));
const mouse_Click$ = mouse_Down$
.pipe(
mergeMap(mouseDownEvent =>
mouse_Up$.pipe(takeUntil(mouse_drag$, mouse_Hold$))
)
).subscribe(event => console.log("Click"));
Expected behaviour:
If the user moves the mouse within 1s of the mousedown event the mouse_drag$ stream should begin emitting and the mouse_Click$/mouse_Hold$'s inner observables should complete (thanks to the takeUntil(mouse_drag$) without emitting and await the next mouse_down$ emmission.
If the mouse button remains down for more than 1s without moving the mouse_Hold$ should emit and mouse_drag$/mouse_click$'s inner observable should complete (thanks to the takeUntil(mouse_Hold$) without emitting and await the next mouse_down$ emmission.
Actual Behaviour: Currently the mouse_Drag$ will emit, the mouse_Hold$ will emit after one second and the mouse_Click$ will emit when the button is released.
My question is why doesn't the emitting mouse_Drag$ stream cause the mouse_Hold$ and mouse_Click$'s inner observable to complete without emitting?

The take until needs to be at the end of your chain
This will cancel the whole chain.
const { fromEvent } = rxjs;
const { tap, takeUntil, mergeMap, merge } = rxjs.operators;
const mouse_Down$ = fromEvent(document, "mousedown").pipe(
tap(event => event.preventDefault())
);
const mouse_Up$ = fromEvent(document, "mouseup").pipe(
tap(event => event.preventDefault())
);
const mouse_Move$ = fromEvent(document, "mousemove");
const mouse_drag$ = mouse_Down$
.pipe(
mergeMap(mouseDownEvent =>
mouse_Move$
),
takeUntil(mouse_Up$)
).subscribe(event => console.log("Drag"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.4.0/rxjs.umd.min.js"></script>

To clarify:
that you want emit from mouse_Hold$ if mouse is hold more then 1 second.
You want to get values from mouse_drag$ if LESS then 1 second pass after mouse dropdown and mouseMove.
You do not have to complete anything since otherwise all behaviour will work only once.
So plan:
3. mouse_drag$ - If mousedown - check mouseMove for 1 second. If mouseMove emits - switch to mouseMove values
4. mouse_Hold$ - if mouseDown - check mouseMove for 1 second. If mouseMove doesn't emit - switch to mouseHold and make it emit 'Hold'
let Rx = window['rxjs'];
const {defer, of, timer, fromEvent, merge, race} = Rx;
const {switchMap, repeat, tap, takeUntil, filter} = Rx.operators;
const {ajax} = Rx.ajax;
console.clear();
const mouse_Down$ = fromEvent(document, "mousedown");
const mouse_Up$ = fromEvent(document, "mouseup");
const mouse_Move$ = fromEvent(document, "mousemove");
const timer$ = timer(2000);
mouse_Hold$ = mouse_Down$.pipe(
switchMap((downEvent) => {
return timer$.pipe(
switchMap((time) => of('HOLD'))
);
}),
takeUntil(merge(mouse_Up$, mouse_Move$)),
repeat(mouse_Down$)
)
mouse_Hold$.subscribe(console.warn);
mouse_drags$ = mouse_Down$.pipe(
switchMap(() => mouse_Move$),
takeUntil(mouse_Up$, $mouse_Hold),
repeat(mouse_Down$)
)
mouse_drags$.subscribe(console.log);
Here is a codepen: https://codepen.io/kievsash/pen/oOmMwp?editors=0010

Related

Emit the combine observable when any one of the inner one emit

I've two observables obs1 and obs2. I'm looking for an operator that emit when atleast one of them emit. combineLatest needs both of them should emit atleast once.
import { combineLatest, forkJoin, of, zip, } from 'rxjs';
import { map } from 'rxjs/operators';
const obs1 = of();
const obs2 = of(1,2,3);
let s = null;
s = combineLatest(obs1, obs2)
.pipe(map(([a, b]) => {
console.log(a, b);
})).subscribe();
I believe you are searching for merge.
const obs1$ = timer(1000).pipe(map(() => 'Observable 1 emitted'));
const obs2$ = timer(10000).pipe(map(() => 'Observable 2 emitted'));
merge(obs1$, obs2$).subscribe(console.log);
See Stackblitz
https://stackblitz.com/edit/rxjs-kp8od3?file=index.ts

How to use rxjs buffer with takeWhile

I am working on webrtc. The application sends icecandidates to backend firestore server.
The problem is the call to signaling server is made multiple times as onicecandidate is triggered multiple time. I want collect all the icecandidates and make a single call to signaling server.
The idea is to buffer all the events untill iceGathering is finished. This below attempt does not work
this.pc = new RTCPeerConnection(iceServers);
const source: Observable<any> = fromEvent(this.pc, 'icecandidate');
const takeWhile$ = source
.pipe(
takeWhile(val=> val.currentTarget.iceGatheringState === 'gathering'
))
const buff = source.pipe(buffer(takeWhile$));
buff.subscribe(() => {
// this.pc.onicecandidate = onicecandidateCallback;
})
Method 1:
You are almost there.
The takeWhile$ takes values and emits them while condition is met. So in buff, whenever takeWhile$ emits a value, buff emits a buffer of icecandidate events.
So you only need to emit one value in takeWhile$.
So what you need is takeLast() operator to only emit the last value.
When you put takeLast(1) in takeWhile$, it only emits last value and in buff, last emitted value leads to creation of buffer of icecandidate events.
this.pc = new RTCPeerConnection(iceServers);
const source: Observable<any> = fromEvent(this.pc, "icecandidate");
const takeWhile$ = source.pipe(
takeWhile(val => val.currentTarget.iceGatheringState === "gathering"),
takeLast(1)
);
const buff = source.pipe(buffer(takeWhile$));
buff.subscribe((bufferValues) => {
// bufferValues has a buffer of icecandidate events
// this.pc.onicecandidate = onicecandidateCallback;
});
You'll have access to buffer of icecandidate events in the subscription as bufferValues in above code.
Method 2:
You can also use reduce operator to achieve same scenario
this.pc = new RTCPeerConnection(iceServers);
const source: Observable<any> = fromEvent(this.pc, "icecandidate");
const takeWhile$ = source.pipe(
takeWhile(val => val.currentTarget.iceGatheringState === "gathering"),
reduce((acc, val) => [...acc,val], [])
);
takeWhile$.subscribe((bufferValues) => {
// bufferValues has a buffer of icecandidate events
// this.pc.onicecandidate = onicecandidateCallback;
})

Rxjs zip-like operator which returns only when observable fires in specific order

Let's say I have two Observables A and B, and I want to combine them to produce this behaviour: the subscription combination returns only if A fires when a B has already been fired. It differs from the zip because I don't want any return if A has already been fired and then a B fires. In other words: ignores A until B fires then return on the next A, then ignore any other A until B fires ... so on
I believe you need withLatestFrom operator:
import { withLatestFrom, map } from 'rxjs/operators';
import { interval } from 'rxjs';
const obsA = interval(2000);
const obsB = interval(1000);
const resultingObs = obsB.pipe(
withLatestFrom(obsA),
map(([bValue, aValue]) => {
return aValue
})
);
// This should emit values from obsA only when obsB has been fired.
const subscribe = resultingObs .subscribe(val => console.log(val));

RxJS Emitting Subscribe Twice

I have a RXJS function that will create an empty Observable, tap into the result and return that new observable. I want the observable to always run the tap so I noop subscribe (in the real case it might not ever be subscribed to).
function that() {
let obs = of({});
obs = obs.pipe(tap(() => console.log('here')))
obs.subscribe();
return obs;
}
const res = that();
res.subscribe(() => console.log('finished'))
If you run this code on StackBlitz, you will notice that here is fired twice. The output looks like this:
here
here
finished
I've tried several different approaches but I can't ever seem to get it to work where it doesn't emit twice.
You subscribe TWICE:
function that() {
let obs = of({});
obs = obs.pipe(tap(() => console.log('here')))
obs.subscribe(); // first subscription
return obs;
}
const res = that();
res.subscribe(() => console.log('finished')) // second subscription
This is the same observable you subscribe to, once in the function, then on the returned value.
Just don't subscribe in the function
function that() {
let obs = of({});
obs = obs.pipe(tap(() => console.log('here')))
return obs;
}
const res = that();
res.subscribe(() => console.log('finished')) // subscribe from here only
See the updated StackBlitz.
Is it just a case of only tapping only the inner subscription?
function that() {
let obs = of({});
obs.pipe(tap(() => console.log('here'))).subscribe();
return obs;
}
const res = that();
res.subscribe(() => console.log('finished'))

RXJS drag and drop onDrop/onDragOver

I was trying to do the drag and drop by using RXJS.
Does anybody know how to do the onDrop or onDragOver properly with the RXJS?
Here is what I have tried. But not working...
let mouseup = Observable.fromEvent(document, 'mouseup');
let mousemove = Observable.fromEvent(document, 'mousemove');
let mousedown = Observable.fromEvent(target, 'mousedown');
const drop = mousedown
.flatMap((md) => {
return Observable
.concat(
[
mousemove.take(1).ignoreElements(),
mouseup.take(1)
]
);
});
Here is another try, which seems to be working. But it will fire whenever mouseup happens, not only fire after the drag.
const drop = mousemove
.switchMap((mm) => mouseup.take(1));
If you want to have all your drag events, you can use this here
let up$ = Observable.fromEvent(document, 'mouseup');
let move$ = Observable.fromEvent(document, 'mousemove');
let down$ = Observable.fromEvent(document, 'mousedown');
let drag$ = down$.switchMap(down => move$.takeUntil(up$));
This will emit every mousemove event which was triggered while you 'dragged'.
It's also very easy to adapt to your needs. Let's say you need the x-distance of your drag:
let distance$ = down$.switchMap((down: MouseEvent) =>
move$.map((move: MouseEvent) => (move.clientX - down.clientX))
.takeUntil(up$));
You want the same, but also support touch? No problem! Just merge your three input streams with their touch-equivalent and you are good to go.
I hope this answers your question,
Ben

Resources