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 shapesactiveShape
- the shape for which there is an active api callshapeData
- 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));
}