Sergey Sokolov
Sergey Sokolov

Reputation: 2839

Tracking asynchronous operation in RxJs

I have a subscription which has asynchronous step - api call. While api call is in progress I want to show loading animation. It can be easily done when stream created with access to the UI:

Rx.Observable
    .combineLatest(page$, sorting$)
    .switchMap(function(args) {
        var page = args[0];
        var sorting = args[1];

        $('.overlay').show();

        return loadData(page, sorting);
    })
    .subscribe(function(data) {
        renderData(data);
        $('.overlay').hide();
    });

But when I move stream management code to the service (e.g. for code reuse) the ability to track asynchronous operation is lost and I can't show loading animation.

Any ideas how it can be done? Return two stream from the service?

Thanks in advance.

Upvotes: 2

Views: 421

Answers (3)

Sergey Sokolov
Sergey Sokolov

Reputation: 2839

Eventually I ended up using custom operator:

class RequestState {
  loading: boolean;
}


Observable.prototype.captureState = function captureState<T>(this: Observable<T>, state: RequestState): Observable<T> {
  state.loading = true;

  let onLoadingEnd = () => {
    state.loading = false;
  };

  obs = obs.do(onLoadingEnd, onLoadingEnd, onLoadingEnd);

  return obs;
};


class Component {
  requestState = new RequestState();

  ngOnInit() {
    this.api.endpoint(query)
      .captureState(this.requestState)
      .subscribe(...);
  }
}

Upvotes: 0

Mark van Straten
Mark van Straten

Reputation: 9425

Why not create a stream which contains multiple 'events' - started and finished, something like this:

function loadDataWithStartEvent($page, sorting$) {
  return Rx.Observable
    .combineLatest(page$, sorting$)
    .flatMap(args => {
      var page = args[0];
      var sorting = args[1];

      return Rx.Observable.just({ event : 'started' })
        .concat(
          // given that loadData returns an Observable<data> which only emits data one time
          loadData(pageSortingTuple.page, pageSortingTuple.sorting))
            .map(data => ({ event: 'finished', data: data}))
        );
    });    
}

Now you subscribe as follows:

loadDataWithStartEvent(page$, sorting$)
  .doOnNext(evt => {
    if(evt.type == 'started') {
      $('overlay').show();
    } else {
      $('overlay').hide();
    }
  })
  .filter(evt => evt.type === 'finished')
  .map(evt => evt.data);
  .subscribe(data => renderData(data));

Upvotes: 2

Meir
Meir

Reputation: 14385

You can pass an extra notification object to the setupLoading:

setupLoading: function(page$, sorting$, notify$) {
  return Rx.Observable
    .combineLatest(page$, sorting$)
    .switchMap(function(args) {
      notify$.next(true); // here we go...
      var page = args[0];
      var sorting = args[1];

      return loadData(page, sorting);
    });
}

and then create, in addition to the page$ and sorting$ and loadStarted$ observable.

loadStarted$.subscribe(() => $('.overlay').hide();

and your loader creation will be:

service.setupLoading(page$, sorting$, loadStarted$)
  .subscribe(function(data) {
     renderData(data);
  });
});

Btw, since you use rxjs so nicely, why not use it for the show/hide - the angular2 way - and give up jquery:

<div class="overlay" [style.display]="(showOverlay ? 'block' : 'none')>your overlay content here</div>

Upvotes: 1

Related Questions