I am calling an API-service which returns an Observable - containing an array of elements.
apiMethod(input: Input): Observable<ResultElement[]>
From this I have been choosing the first element of the array, subscribing to that. Then used that element to route to another page like this:
this.apiService
.apiMethod(input)
.pipe(map((results) => results[0])
.subscribe(
(result) => {
return this.router.navigate('elements/', result.id)
}
)
This works just fine.
Problem is, I do not want to just use the first element, I want a MatDialog, or other similar to pop up, and give the user option of which element to choose, and THEN route to the correct one.
If the list only contain one element though, the dialog should not show, and the user should be routed immediately.
I have tried to open a dialog in the .pipe(map()) function, but the subscribe() happens before I get answer from the user, causing it to fail. And I am not sure if that even is the correct approach. How would any of you solve this problem?
Edit Ended up doing partly what @BizzyBob suggested:
Changing map to switchmap in the API-call, making it this way:
this.apiService
.apiMethod(input)
.pipe(switchMap((results) => this.mapToSingle(results)
.subscribe(
(result) => {
return this.router.navigate('elements/', result.id)
}
)
With the mapToSingle(ResultElement[]) being like this:
private mapToSingle(results: ResultElement[]): Observable<ResultElement> {
if (result.length === 1){
return of(results[0]);
}
const dialogConfig = new MatDialogConfig<ResultElement[]>();
dialogConfig.data = results;
const dialogRef = this.dialog.open(ResultDialogComponent, dialogConfig);
return dialogRef.afterClosed();
}
Two possible options:
The example below is a sketch of using an Observable to keep track of the component's state.
readonly getResultsSubject = new Subject<MyInput>();
readonly resultSelectedSubject = new Subject<ResultType>();
private readonly apiResults$ = this.getResultsSubjects.pipe(
switchMap((input) => this.apiMethod(input))
);
readonly state = combineLatest([
this.apiResults$.pipe(map(results => (s) => results.length === 1
? { ...s, results, selectedResult: x[0], showModal: false }
: { ...s, results, showModal: results.length > 1 })),
this.resultSelectedSubject.pipe(map(selectedResult => (s) => ({ ...s, selectedResult })))
]).pipe(
scan((s, reducer) => reducer(s), { }),
shareReplay(1)
);
ngOnInit() {
this.state.pipe(
filter(x => !!x.selectedResult)
).subscribe(x => this.router.navigate('elements/', x.selectedResult.id));
}
I've been using this pattern a lot lately. It makes it pretty easy the number of actions and properties of the state grow.
I would create a DialogComponent that takes in the list of choices as an input, and emits the chosen item when it's closed.
Then, create a helper method (maybe call it promptUser) that simply returns an observable that emits the selected value:
this.apiService.apiMethod(input)
.pipe(
switchMap(results => results.length > 1
? this.promptUser(results)
: of(results[0])
)
)
.subscribe(
result => this.router.navigate('elements/', result.id)
);
Here we simply use switchMap to return an observable that emits the proper item. If the length is greater than 1, we return the helper method that displays the dialog and emits the chosen item, else just emit the first (only) item. Notice that we wrapped plain value with of since within switchMap, we need to return observable.
In either case, the desired item is emitted and received by your subscribe callback.
I would solve it using the following method:
options: any;
this.apiService
.apiMethod(input)
.subscribe(
(result) => {
if (result.length === 1) {
this.router.navigate([result[0]]);
return;
}
options = result;
}
)
<modal-component *ngIf="options.length > 0"></modal-component>
html
<div (click)="redirect(value)">option 1</div>
ts
redirect(value) {
this.router.navigate([value]);
}
That would be the most straight forward