This question is an extension of my previous question that you can find here:
How to use RxJS to display a "user is typing" indicator?
After having successfully been able to track whether or not the user was typing, I needed to be able to use that particular state as a trigger for a clock.
The logic is simple, essentially I want a clock to be run when the user is typing. But when the user stops typing, I need the clock to pause. When the user starts typing again, the clock should continue to accumulate.
I have already been able to get it to work, but it looks like a mess and I need help refactoring it so it isn't a ball of spaghetti. Here is what the code looks like:
/*** Render Functions ***/
const showTyping = () =>
$('.typing').text('User is typing...');
const showIdle = () =>
$('.typing').text('');
const updateTimer = (x) =>
$('.timer').text(x);
/*** Program Logic ***/
const typing$ = Rx.Observable
.fromEvent($('#input'), 'input')
.switchMapTo(Rx.Observable
.never()
.startWith('TYPING')
.merge(Rx.Observable.timer(1000).mapTo('IDLE')))
.do(e => e === 'TYPING' ? showTyping() : showIdle());
const timer$ = Rx.Observable
.interval(1000)
.withLatestFrom(typing$)
.map(x => x[1] === 'TYPING' ? 1 : 0)
.scan((a, b) => a + b)
.do(console.log)
.subscribe(updateTimer)
And here is the link to the live JSBin: http://jsbin.com/lazeyov/edit?js,console,output
Perhaps I will walk you through the logic of the code:
- I first build a stream to capture each typing event.
- For each of these events, I will use
switchMap
to: (a) fire off the original "TYPING" event so we don't lose it, and (b) fire off an "IDLE" event, 1 second later. You can see that I create these as separate streams and then merge them together. This way, I get a stream that will indicate the "typing state" of the input box. - I create a second stream that sends an event every second. Using
withLatestFrom
, I combine this stream with the previous "typing state" stream. Now that they are combined, I can check whether or not the typing state is "IDLE" or "TYPING". If they are typing, I give the event a value of1
, otherwise a0
. - Now I have a stream of
1
s and0
s, all I have to do is add them back up with.scan()
and render it to the DOM.
What is the RxJS way to write this functionality?
EDIT: Method 1 — Build a stream of change-events
Based on @osln's answer.
/*** Helper Functions ***/
const showTyping = () => $('.typing').text('User is typing...');
const showIdle = () => $('.typing').text('');
const updateTimer = (x) => $('.timer').text(x);
const handleTypingStateChange = state =>
state === 1 ? showTyping() : showIdle();
/*** Program Logic ***/
const inputEvents$ = Rx.Observable.fromEvent($('#input'), 'input').share();
// streams to indicate when user is typing or has become idle
const typing$ = inputEvents$.mapTo(1);
const isIdle$ = inputEvents$.debounceTime(1000).mapTo(0);
// stream to emit "typing state" change-events
const typingState$ = Rx.Observable.merge(typing$, isIdle$)
.distinctUntilChanged()
.share();
// every second, sample from typingState$
// if user is typing, add 1, otherwise 0
const timer$ = Rx.Observable
.interval(1000)
.withLatestFrom(typingState$, (tick, typingState) => typingState)
.scan((a, b) => a + b, 0)
// subscribe to streams
timer$.subscribe(updateTimer);
typingState$.subscribe(handleTypingStateChange);
EDIT: Method 2 — Using exhaustMap to start counter when user starts typing
Based on Dorus' answer.
/*** Helper Functions ***/
const showTyping = () => $('.typing').text('User is typing...');
const showIdle = () => $('.typing').text('');
const updateTimer = (x) => $('.timer').text(x);
/*** Program Logic ***/
// declare shared streams
const inputEvents$ = Rx.Observable.fromEvent($('#input'), 'input').share();
const idle$ = inputEvents$.debounceTime(1000).share();
// intermediate stream for counting until idle
const countUntilIdle$ = Rx.Observable
.interval(1000)
.startWith('start counter') // first tick required so we start watching for idle events right away
.takeUntil(idle$);
// build clock stream
const clock$ = inputEvents$
.exhaustMap(() => countUntilIdle$)
.scan((acc) => acc + 1, 0)
/*** Subscribe to Streams ***/
idle$.subscribe(showIdle);
inputEvents$.subscribe(showTyping);
clock$.subscribe(updateTimer);