How to use RxJS to display a "user is typing" indicator?

Question

I know a little bit of BaconJS, but now I'm trying to learn RxJS by creating a "User is typing..." indicator. It's pretty simple, it can be explained in two simple rules:

  1. When the user is typing, the indicator should be immediately visible.
  2. When the user stops typing, the indicator should still be visible until 1 second after the user's last typing action.

I'm not sure if this is correct, but I have so far created two streams:

  1. One heartbeat stream that emits a 0 every second.
  2. One stream to capture the user typing events and emit a 1 for every event.

Then I merge them together, and simply tap into the result. If it's a 1, then I show the indicator. If it's a 0, then I hide the indicator.

This is what that looks like:

const showTyping = () =>
  $('.typing').text('User is typing...');

const showIdle = () =>
  $('.typing').text('');

// 1 second heartbeats are mapped to 0
const heartbeat$ = Rx.Observable
  .interval(1000)
  .mapTo(0);

// user typing events are mapped to 1
const input$ = Rx.Observable
  .fromEvent($('#input'), 'input')
  .mapTo(1);

// we merge the streams together
const state$ = heartbeat$
  .merge(input$)
  .do(val => val === 0 ? showIdle() : showTyping())
  .subscribe(console.log);

Here is a link to the JSBin:

http://jsbin.com/vekixuv/edit?js,console,output

There are several problems and questions I have with this implementation:

  1. Sometimes when the user is typing, a 0 sneaks through, so the indicator flashes away for a split second before coming back on the next user keystroke.
  2. It's not guaranteed that the indicator will disappear 1 second after the user stops typing. It's only guaranteed that the indicator will disappear within 1 second (which is kind of the opposite of what we want).
  3. Is using a heartbeat stream the correct RxJS way to do this? I have a feeling it might not be.

I have a feeling that I am completely off-base with my implementation, I appreciate any help that you may be able to provide. Thanks.


Show source
| javascript   | rxjs   | rxjs5   | rxjs-dom   2017-01-07 12:01 2 Answers

Answers ( 2 )

  1. 2017-01-07 12:01

    You don't need a heartbeat for this, just emit change-events whenever something happens/changes:

    const showTyping = () =>
      $('.typing').text('User is typing...');
    
    const showIdle = () =>
      $('.typing').text('');
    
    
    // user typing events
    const input$ = Rx.Observable
      .fromEvent($('#input'), 'input');
    
    // user stopped typing
    const stoppedTypingAfter1s$ = input$
      .switchMapTo(Rx.Observable.timer(1000));
    
    // we merge the streams together
    const state$ = Rx.Observable.merge(
        input$.mapTo(1),
        stoppedTypingAfter1s$.mapTo(0)
    )
      .startWith(0)
      .do(val => val === 0 ? showIdle() : showTyping())
      .subscribe(console.log);
    

    See live here.

    The switchMap, will discard any previouse 1s-timer whenever a new typing-event is emitted.

  2. 2017-01-07 12:01

    You don't even need to use two Observables and use just one with debounceTime(). All the logic you tried to make is already present in debounceTime() operator:

    const showTyping = () =>
      $('.typing').text('User is typing...');
    
    const showIdle = () =>
      $('.typing').text('');
    
    const input$ = Rx.Observable
      .fromEvent($('#input'), 'input')
      .do(() => showTyping())
      .debounceTime(1000)
      .subscribe(() => showIdle());
    

    See live demo: http://jsbin.com/cixipa/6/edit?js,console,output

◀ Go back