I want to implement a progress bar with rxjs,so I need to get the position of mousedown and the position of mousemove after mousedown.here is the test code I write.
useEffect(() => {
const start$ = fromEvent(divRef.current, 'mousedown').pipe(
tap(() => console.log('start')),
map(event => [event.clientX, event.clientY])
)
const move$ = fromEvent(divRef.current, 'mousemove').pipe(
tap(() => console.log('move')),
map(event => [event.clientX, event.clientY])
)
const end$ = fromEvent(divRef.current, 'mouseup')
const drag$ = concat(start$, move$).pipe(takeUntil(end$), repeat())
const subscription = drag$.subscribe(([newX, newY]) => {
setX(newX)
setY(newY)
})
return () => {
subscription.unsubscribe()
}
})
what I think is that when I click down the mouse,I will subscribe the Start$,and then if I move,I will subscribe the move$.But the Phenomenon is quiet different from what I thought.The log in the console just output 'start'.
enter image description here
as you can see from the picture,when I click down and move,I just can subscribe the start$,if I use the concat method wrong.Hope someone can do me a favor.
I assume you want the mouse movements in the order they happen. Here is a small snippet:
import './style.css';
import {
tap,also
fromEvent,
takeUntil,
exhaustMap,
} from 'rxjs';
const mouseDown$ = fromEvent(document, 'mousedown');
const mouseMove$ = fromEvent(document, 'mousemove');
const mouseUp$ = fromEvent(document, 'mouseup');
mouseDown$
.pipe(
tap(console.log),
exhaustMap((start) =>
mouseMove$.pipe(tap(console.log), takeUntil(mouseUp$))
)
)
.subscribe();
Please have a look here: Stackblitz
Related
I have two subjects:
const foo = new Subject();
const bar = new Subject();
And each of them acting different:
const foo$ = foo.pipe(
tap(() => {
console.log('in foo');
})
);
const bar$ = bar.pipe(
tap(() => {
console.log('in bar');
}),
);
When I call bar.next to trigger the bar$ pipeline and it's of course works as expected.
But I want to trigger the foo$ pipeline within the bar$ pipeline like that:
const foo$ = foo.pipe(
tap(() => {
console.log('in foo');
})
);
const bar$ = bar.pipe(
tap(() => {
console.log('in bar');
}),
switchMap(() => foo$)
);
This not working.
I have some limitations:
I don't want to trigger the subject because it trigger every subscribers somewhere in the app.
I can't use foo$.next because it's next don't exist in foo$.
I want to use foo$ in the same pipeline of bar$ using the pipeline.
I try to wrap it in from, switchMap, map, switchMapTo - those not working.
Any idea?
stackblitz
import { Subject, tap } from 'rxjs';
import { switchMap } from 'rxjs/operators';
console.clear();
const foo = new Subject();
const bar = new Subject();
const foo$ = foo.pipe(
tap(() => {
console.log('in foo');
})
);
const bar$ = bar.pipe(
tap(() => {
console.log('in bar');
}),
switchMap(() => foo$)
);
bar$.subscribe();
bar.next(1);
You have an active inner subscription to foo$, but it's not emitting anything because that can only be done with your bar Subject. (Which you say that you don't want to trigger.)
So the next best thing is to combine both your foo subject and the bar$ observable as the source of your foo$ observable. Because we don't know when either observable will emit or complete, we'll use the mergeWith() operator.
import { Subject, tap } from 'rxjs';
import { merge } from 'rxjs/operators';
console.clear();
const foo = new Subject();
const bar = new Subject();
const fooSource$ = foo.pipe(
tap(() => {
console.log('in foo');
})
);
const barSource$ = bar$.pipe(
tap(() => {
// do whatever before bar$ emits to foo$
})
);
const foo$ = fooSource$.pipe(
mergeWith(barSource$),
tap(() => {
// do whatever before foo$ emits its value
})
);
const bar$ = bar.pipe(
tap(() => {
console.log('in bar');
})
);
bar$.subscribe();
bar.next(1);
Note that with mergeWith() you cannot distinguish which observable is triggering the emit. So it's a good idea to have both fooSource$ and barSource$ emit the same value type.
Question about rxjs puzzle.
I have the input observable stream and it will emit after 3 secs when I type some.
import { fromEvent, interval } from "rxjs";
import { debounce } from "rxjs/operators";
// input is HTMLInputElement
const input$ = fromEvent(input, "input");
input$
.pipe(debounce(() => interval(3000)))
.subscribe(e => console.log(e.target.value));
I would like to make a change to cancel the debounce and emit immediately once the button is clicked. But, if I don't click the button, it will wait 3 secs.
import { fromEvent, interval } from "rxjs";
import { debounce } from "rxjs/operators";
const input$ = fromEvent(input, "input");
// add click observable stream
const click$ = fromEvent(button, "click");
input$
.pipe(debounce(() => interval(3000)))
// I can't get this to work in the mix!!
// .pipe(debounce(() => click$))
.subscribe(e => console.log(e.target.value));
How can this be achieved?
sounds like a race operator.
const input$ = fromEvent(input, "input");
const click$ = fromEvent(button, "click");
input$
.pipe(
switchMap(value => race(
click$,
timer(3000),
).pipe(
take(1),
mapTo(value),
)),
.subscribe(e => console.log(e.target.value));
Here is the solution to toggle debounce, what you have to do is to convert interval() to a stream that change interval time base on button click
Js
import { fromEvent, interval,timer} from 'rxjs';
import { debounce,scan,shareReplay,map,startWith,tap,switchMap} from 'rxjs/operators';
const input = fromEvent(document.getElementById('text'), 'input');
const debounceToggle=fromEvent(document.getElementById('toggle'),'click').pipe(
scan((acc,curr)=>!acc,false),
map(on=>on?0:3000),
startWith(3000),
shareReplay(1),
switchMap(value=>interval(value))
)
const result = input.pipe(debounce(() => {
return debounceToggle
}));
result.subscribe(x => console.log(x.target.value));
HTML
<button id="toggle">toggle debounce</button>
<input type="text" id="text"/>
Here could be another solution I think:
input$
.pipe(
debounce(
() => interval(3000).pipe(takeUntil(buttonClick$))
)
)
.subscribe(e => console.log(e.target.value));
debounce will emit the value that caused the inner observable's subscription, when it either completes/emits a value
// Called when the inner observable emits a value
// The inner obs. will complete after this as well
notifyNext(outerValue: T, innerValue: R,
outerIndex: number, innerIndex: number,
innerSub: InnerSubscriber<T, R>): void {
this.emitValue();
}
// Called when the inner observable completes
notifyComplete(): void {
this.emitValue();
}
Source code
The following would be the simplest in my opinion:
const input$ = fromEvent(input, "input");
const click$ = fromEvent(button, "click");
merge(
input$.pipe(debounceTime(3000)),
click$
).pipe(
map(() => input.value)
).subscribe(val => console.log(val));
https://stackblitz.com/edit/rxjs-8bnhxd
Also, you are essentially "combining" 2 different events here, it doesn't make sense to me to rely on event.target.value, as it could be referring to different things which makes it hard to read.
Here here the requirement:
When click start button, emit event x times every 100ms, each emit correspond an UI update. When x times emit complete, it will trigger a final UI update, look simple right?
Here is my code:
const start$ = fromEvent(document.getElementById('start'), 'click')
const intervel$ = interval(100)
.pipe(
take(x),
share()
)
var startLight$ = start$
.pipe(
switchMap(() => {
intervel$
.pipe(last())
.subscribe(() => {
// Update UI
})
return intervel$
}),
share()
)
startLight$
.subscribe(function (e) {
//Update UI
})
Obviously, subscribe inside switchMap is anti-pattern, so I tried to refactor my code:
const startInterval$ = start$
.pipe(
switchMapTo(intervel$),
)
startInterval$.pipe(last())
.subscribe(() => {
//NEVER Receive value
})
const startLight$ = startInterval$.pipe(share())
The problem is that intervel$ stream is generated inside switchMap and can not be accessed outside, you can only access the stream who generate interval$, i.e. start$ which never complete!
Is there is smarter way to handle such kind of problem or it was an inherent limitation of rxjs?
You were very close. Use last() inside intervel$ to only emit the final one to the subscribe below. Working StackBlitz. Here are details from the StackBlitz:
const start$ = fromEvent(document.getElementById('start'), 'click');
const intervel$ = interval(100)
.pipe(
tap(() => console.log('update UI')), // Update UI here
take(x),
last()
);
const startInterval$ = start$
.pipe( switchMapTo(intervel$));
startInterval$
.subscribe(() => {
console.log('will run once');
});
Update
If you do not wish to use tap(), then you can simply cause start$ to finish by taking only the first emission and then completing with either take(1) or first(). Here is a new StackBlitz showing this.
const start$ = fromEvent(document.getElementById('start'), 'click')
.pipe(
first()
);
const intervel$ = interval(100)
.pipe(
take(x)
);
const startInterval$ = start$
.pipe(
switchMapTo(intervel$)
);
startInterval$
.subscribe(
() => console.log('Update UI'),
err => console.log('Error ', err),
() => console.log('Run once at the end')
);
The downside to this approach (or any approach that completes the Observable) is that once completed it won't be reused. So for example, clicking multiple times on the button in the new StackBlitz won't work. Which approach to use (the first one that can be clicked over and over or the one that completes) depends on the results you need.
Yet Another Option
Create two intervel$ observables, one for the intermediate UI updates and one for the final one. Merge them together and only do the UI updating in the subscribe. StackBlitz for this option
code:
const start$ = fromEvent(document.getElementById('start'), 'click')
const intervel1$ = interval(100)
.pipe(
take(x)
);
const intervel2$ = interval(100)
.pipe(
take(x+1),
last(),
mapTo('Final')
);
const startInterval$ = start$
.pipe(
switchMapTo(merge(intervel1$, intervel2$))
);
startInterval$
.subscribe(
val => console.log('Update UI: ', val)
);
A more idiomatic way, same logic as previous one (By Guichi)
import { switchMapTo, tap, take, last, share, mapTo } from 'rxjs/operators';
import { fromEvent, interval, merge } from 'rxjs';
const x = 5;
const start$ = fromEvent(document.getElementById('start'), 'click');
const intervel$ = interval(100);
const intervel1$ = intervel$
.pipe(
take(x)
);
const intervel2$ = intervel1$
.pipe(
last(),
mapTo('Final')
);
const startInterval$ = start$
.pipe(
switchMapTo(merge(intervel1$, intervel2$))
);
startInterval$
.subscribe(
val => console.log('Update UI: ', val)
);
Reflection
The key problem of the original question is to 'use the same observable in different ways', i.e. during the progress and the final. So merge is an pretty decent logic pattern to target this kind of problem
Put your update logic inside the switchMap and tap() , tap will run multiple time and only last emission will be taken by subscribe()
const startInterval$ = start$
.pipe(
switchMap(()=>intervel$.pipe(tap(()=>//update UI),last()),
)
startInterval$
.subscribe(() => {
// will run one time
})
Is there a way to get the first and last emitted values from an observable?
const down$ = fromEvent(this.canvas, 'mousedown');
const up$ = fromEvent(this.canvas, 'mouseup');
const move$ = fromEvent(this.canvas, 'mousemove');
const drag$ = move$.pipe(
skipUntil(down$),
takeUntil(up$)
);
drag$.subscribe((e: MouseEvent) => {
console.log(e);
});
Is there a way to get the first and last values from the drag$ observable?
You can get the first and the last values from an Observable with take(1) and takeLast(1) operators.
import { range, merge, Subject } from 'rxjs';
import { take, takeLast, multicast } from 'rxjs/operators';
range(1, 10)
.pipe(
multicast(() => new Subject(), o => merge(
o.pipe(take(1)),
o.pipe(takeLast(1)),
)),
)
.subscribe(v => console.log('result', v));
In your case it looks like you could do something like this but I don't know what exactly is you goal:
down$
.pipe(
switchMap(() => move$.pipe(
takeUntil(up$),
multicast(() => new Subject(), o => merge(
o.pipe(take(1)),
o.pipe(takeLast(1)),
)),
)),
)
.subscribe(console.log);
See live demo: https://stackblitz.com/edit/rxjs6-demo-ymjoiy?file=index.ts
Sorry, I was making things too complicated. This is my final code:
const up$ = fromEvent(this.canvas, 'mouseup');
const down$ = fromEvent(this.canvas, 'mousedown');
const drag$ = down$.pipe(merge(up$));
drag$.subscribe((e: MouseEvent) => console.log(e); });
I'm listening to mousemove event until mouseup. I'm doing it with takeUntil.
My code:
const onMouseMove = fromEvent(window, "mousemove");
const onMouseUp = fromEvent(window, "mouseup");
const resizePanel = onMouseMove
.pipe(
takeUntil(onMouseUp),
map(
(event: MouseEvent) => {
this.isDragging = true;
this.resizePanel(event.clientX);
}
)
);
I have one variable isDragging: boolean, which I want to set to false, when mouseup occurs, e.g. after takeUntil in my code. It must be simple, but I can't figure it out.
You may want to try something like this
const onMouseMove = fromEvent(window, "mousemove");
const onMouseUp = fromEvent(window, "mouseup");
const resizePanel = onMouseMove
.pipe(
takeUntil(onMouseUp.pipe(tap(() => this.isDragging = false))),
map(
(event: MouseEvent) => {
this.isDragging = true;
this.resizePanel(event.clientX);
}
)
);
The idea is to add a pipe with tap operator to onMouseUp used as parameter for takeUntil
You could use combineLatest on onMouseMove$ and onMouseUp$ and use a finalise subject in takeUntil.
const { fromEvent, combineLatest, Subject } = rxjs;
const { takeUntil } = rxjs.operators;
const onMouseMove$ = fromEvent(window, "mousemove");
const onMouseUp$ = fromEvent(document, "mouseup");
const finalise = new Subject();
combineLatest(onMouseMove$, onMouseUp$).pipe(takeUntil(finalise)).subscribe(([onMouseMove, onMouseUp]) => {
if (onMouseUp) {
console.log('mouse up');
finalise.next(true);
finalise.complete();
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.3.3/rxjs.umd.min.js"></script>
You need to subscribe
const { fromEvent } = rxjs;
const onMouseUp$ = fromEvent(document, "mouseup");
onMouseUp$.subscribe(onMouseUp => {
console.log('mouse up');
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.3.3/rxjs.umd.min.js"></script>
takeUntil ends an observable, nothing happens after takeUntil.