0
votes

Say I have an angular 2 component like so:

import { Component, AfterViewInit, ViewChildren, QueryList, ElementRef } from '@angular/core';
import { Observable } from 'rxjs/Rx';
import {ApiService} from './my.service'
@Component({
  selector: 'my-component',
  template: `
    <div class='button0' [ngClass]="{selected: selection === 0}" #button>Hello World</div>
    <div class='button1'  [ngClass]="{selected: selection === 1}" #button>Hello World</div>
  `,
  styles: [`
  .selected {
    color: red;
  }
  `],
  providers: [ApiService]
})
export class MyComponent implements AfterViewInit { 
  selection = 0;
  @ViewChildren('button') buttons: QueryList<ElementRef>;
  buttonObservables:any[] = null;

  constructor(private api: ApiService) {}

  updateServer(index) {
    api.requestsExample(index)
      .then((result) => {
        //update other divs and stuff
      }
  }


  updateColor(index) {
    this.selection = index;
  }

  ngAfterViewInit () {
    this.buttonObservables = this.buttons.map((button) => Observable
      .fromEvent<MouseEvent>(button.nativeElement, 'click'));

    this.buttonObservables.map((observable) => {
      observable.throttleTime(2000).subscribe((event:MouseEvent) => {
          const element:Element = event.target as Element;
        this.updateServer(element.classList[1].match(/\d+/g));
      })
    });

    this.buttonObservables.map((observable) => {
      observable.subscribe((event:MouseEvent) => {
        const element:Element = event.target as Element;
        this.updateColor(element.classList[1].match(/\d+/g));
      })
    });
  }
}

where ApiService.requestsExample

is an async annotated function which makes a request and returns a response.

The code just about works (e.g. requests are throttled, button mashing does not result in too many requests, colors still change)

I am struggling to figure out to handle the following edgecase: 1)I would like to guarantee that the result that was fired last is the one the response from which is accepted (assuming a response comes back), and then work back in chronological order. I am not sure how to achieve this due to the fact that requests are asynchronous? 2) (corollary)In order to prevent flicker on updating, I would also like to discard any results that come back from the server once a later result comes back (based on issue order rather than response order). 3) Once the last currently live request returns, I would like to discard all ongoing observables, as I no longer care about them.

So basically, if a user mashes the two buttons for 20 seconds, I would expect to make 10ish requests, but with the exception of toggling the button colors, update the UI once, and to the correct value.

Additionally I would just love any feedback on whether there is a better way to achieve this result with Observables (or even if observables are the right tool for this job!)

1

1 Answers

2
votes

Let's explain the RxJS 5 example below:

  • You want to make updateServer part of the reactive computation, so you make it return an observable.
  • Since you're treating all the clicks in the same way, it makes sense to mergeAll all the clicks from different buttons.
  • Since you're only using the button's index for the computation, it makes sense to map clicks to just that.
  • You can do the immediate updateColor as a side effect with do.
  • debounceTime(1000) emits a click only after one second is passed without other clicks. I think this is better than Throttle as you don't want to make unnecessary network calls if the user does multiple quick clicks. Only the last one.
  • Since you want to cancel the previous updateServer when a new click comes, it makes sense to use switchMap. It maps a click to a new updateServer observable then aborts it and switches to a new one if a new click arrives.
  • Since you want to ignore all further clicks after the first updateServer, or this is how I understand 3), take(1) will take one result and then complete the whole chain.
updateServer(index) {
  return Observable.fromPromise(api.requestsExample(index))
}

this
  .buttonObservables
  .mergeAll()
  .map(ev => ev.target.classList[1].match(/\d+/g))
  .do(idx => this.updateColor(idx))
  .debounceTime(1000)
  .switchMap(idx => this.updateServer(idx))
  .take(1)
  .subscribe(result => {
    // update other divs
  })