Home > Blockchain >  Rxjs operation to click multiple buttons and make api calls
Rxjs operation to click multiple buttons and make api calls

Time:01-22

The UI has an array of buttons that can be clicked. Clicking the button will disable that button and trigger an API call. If the person decides to click another button the current API call should be canceled and the next API call should be initialized.

following is the code that i wrote.

@ViewChildren('shape') shapes!: QueryList<ElementRef>;

const shapes$: Array<Observable<PointerEvent>> = this.shapes.map((ele) => fromEvent(ele.nativeElement, 'click'));
            merge(...shapes$)
                .pipe(
                    map((e: PointerEvent) => (e.target as HTMLButtonElement)),
                    tap((e) => e.disabled = true),
                    map((e: HTMLButtonElement) => e.getAttribute('shape') as ShapeNames),
                    switchMap((shape) => this.makeAPICall(shape)),
                    // How do i enable the previously clicked button. 
                )
                .subscribe((...res) => {
                    console.log(res);
                });

How do i renable the button when the second button is clicked? How do i access the previous clicked element after switchMap

CodePudding user response:

This scenario can be simplified if you completely separate the data from the DOM. @ViewChildren is not needed and it's not necessary to "manually" manipulate the html button's disabled property.

How do i renable the button when the second button is clicked?

To accomplish this, we can simply keep track of the "active shape", then in the template, bind the disabled property of the button to an expression that compares the shape to the activeShape:

    <li *ngFor="let shape of vm.shapes"> 
      {{ shape }} 
      <button (click)="setActiveShape(shape)" [disabled]="shape === vm.activeShape">
        get data
      </button>
    </li>

Notice when the button is clicked, we set the active shape, then to determine if the button should be disabled, we simply compare against the activeShape property. This means that when activeShape changes, both the previously disabled button and the newly clicked button will automatically show the correct enabled/disabled state.


You may have noticed the vm in the code sample above. This is simply a "view model" object that contains all data that our view needs. RxJS makes it easy to create a single observable that emits an object when any of our data sources emit an updated value.

If you think about your "state", you really have 3 pieces of data:

  • shapes - the array of shapes
  • activeShape - the shape for which there is an active api call
  • shapeData - the result of the api call

We can use a BehaviorSubject to emit the new activeShape whenever a button is clicked:

  private activeShape$ = new BehaviorSubject<string>(undefined);

  setActiveShape(value: string) {
    this.activeShape$.next(value);
  }

This subject will be used as an observable source for our view model.

We can declare the shapeData from the emission of the activeShape using switchMap like this:

  private shapeData$ = this.activeShape$.pipe(
    switchMap(shape => this.makeApiCall(shape)),
  );

We can use combineLatest to put all our independent observable sources into a single object for use in the template. This observable will emit whenever any of its sources emit a new value:

  vm$ = combineLatest({
    activeShape : this.activeShape$, 
    shapeData   : this.shapeData$,
    shapes      : this.getShapes()
  });

It's important to note that combineLatest will not emit until all of its sources have emitted at least one value. So, we will modify shapeData$ to emit undefined initially by using startWith. Also, it probably doesn't make sense to call the api when there is no active shape, so we can use filter to stop that:

  private shapeData$ = this.activeShape$.pipe(
    filter(shape => !!shape),
    switchMap(shape => this.makeApiCall(shape)),
    startWith(undefined)
  );

Notice we haven't talked about subscribing yet. If we subscribe in our component's controller, then we have to be responsible for unsubscribing. I find it much simpler to use Angular's async pipe. This will automatically subscribe for us when the component is created and unsubscribe when the component is destroyed:

<ng-container *ngIf="vm$ | async as vm">

  <ul>
    <li *ngFor="let shape of vm.shapes"> 
      {{ shape }} 
      <button (click)="setActiveShape(shape)" [disabled]="shape === vm.activeShape"> get data </button>
    </li>
  </ul>

  <div>
    {{ vm.shapeData ?? 'No Data' }}
  </div>

</ng-container>

Here's a working StackBlitz demo that you can play around with.

CodePudding user response:

I took the stackblitz-example of harikrish and refactored it. I believe there is absolutely no pairwise needed. Also, in my example, the selected button is re-enabled after each API call, regardless of whether another button is clicked later.

Check my updated stackblitz example here or see the following code:

shapes = ['circle', 'triangle', 'square', 'trapeziod'];

@ViewChildren('tools') tools!: QueryList<ElementRef>;

ngAfterViewInit() {
  const tools: Array<Observable<PointerEvent>> = this.tools.map((e) =>
    fromEvent(e.nativeElement, 'click')
  );

  merge(...tools)
    .pipe(
      switchMap((e: PointerEvent) => {
        const button = e.target as HTMLButtonElement;
        button.disabled = true;

        const shape = button.getAttribute('shape');
        return this.makeAPICall(shape).pipe(
          first(), // first() might not be needed for a plain http-request
          finalize(() => {
            button.disabled = false;
          })
        );
      })
    )
    .subscribe((e) => console.log(e));
}

makeAPICall(shape: string) {
  return of('').pipe(delay(1000));
}
  • Related