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
})
Related
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
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.
a: 1---2-3-4--5---6
b: ------T---------
o: ------1234-5---6
Using RxJS, is there some operator that can accomplish the diagram above? I have stream A which is a random stream of events, given a stream B which has a single true event, can I have an output stream that doesn't emit anything until that true event, and then sends everything is had saved up until then and afterwards emits normally?
I thought maybe I could use buffer(), but it seems like there is no way to do a one time buffer like this with that operator.
const { concat, interval, of, from } = rxjs;
const { share, delay, toArray, takeUntil, mergeMap } = rxjs.operators;
const waitUntil = signal$ => source$ => {
const sharedSource$ = source$.pipe(share());
return concat(
sharedSource$.pipe(
takeUntil(signal$),
toArray(),
mergeMap(from)
),
sharedSource$
);
}
const stopWaiting$ = of('signal').pipe(delay(2000));
const source$ = interval(500).pipe(
waitUntil(stopWaiting$)
).subscribe(console.log);
<script src="https://cdnjs.cloudflare.com/ajax/libs/rxjs/6.3.3/rxjs.umd.js"></script>
I think #ZahiC's solution is correct but personally I'd do it in a single chain using the multicast operator.
a$.pipe(
multicast(new Subject(), s => concat(
s.pipe(
buffer(b$),
take(1),
),
s
)),
)
multicast will basically spit the stream into two where concat will first subscribe to the first one that is buffered until b$ emits. Then it completes immediately because of take(1) and concat subscribe to the same steam again but this time unbuffered.
Here's my solution, using TypeScript:
export const queueUntil = <T>(signal$: Observable<any>) => (source$: Observable<T>) => {
let shouldBuffer = true;
return source$.pipe(
bufferWhen(() => shouldBuffer ? signal$.pipe(
tap(() => shouldBuffer = false),
) : source$),
concatMap(v => v),
);
};
and can be used like this:
a$.pipe(
queueUntil(b$)
)
All existing answers here (as of 2022/1/4) have the potential to skip/eat source notifications emitted on the same frame as the notifier emits (particularly if the source stream feeds new values back into itself). This solution supports this use case:
function waitUntil<T>(notifier$: Observable<any>): MonoTypeOperatorFunction<T> {
return (source$: Observable<T>) => {
const buffer$ = new ReplaySubject<T>();
let doBuffer = true;
source$.pipe(takeWhile(() => doBuffer, true)).subscribe(buffer$);
return notifier$.pipe(
take(1),
switchMap(() => {
doBuffer = false;
return concat(buffer$, source$);
}),
);
};
}
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); });