1

Hi i'm trying to get to grips with rxjs in angular 9 and i'm trying to make a call conditionally based on a response of an observable but can't seem to get it to work without nesting subscriptions. I have read several articles including this and this but I can't seem to resolve my issue.

In my reading I also read this SO post

The confirmed() method in the DialogConfirmService is triggered when a confirm button is clicked

Please note I have tried more than two refactors but the posted ones are examples of what I've been trying.

My current working solution (I want to get away from the nested subscriptions)

this.dialogConfirmService
    .confirmed()
    .pipe(take(1))
    .pipe(map(x => {
             this.isSaving = true; 
             return x; 
          })
     )
    .subscribe(confirmed => {
      if (confirmed) {
        this.myService.save('', '')
                      .pipe(take(1))
                      .subscribe(
                            (data) => this.onSuccess(data),
                            (error) => this.handleError(error)
                      );
      }
    });

My refactor failure 1

this.dialogConfirmService
        .confirmed()
        .pipe(take(1))
        .pipe(
          map(x => {
              this.isSaving = true;
              console.log(x);
              return x;
            }
          ),
          mergeMap(cv =>
            this.myService.save(
              this.rulesetForm.controls.name.value,
              JSON.parse(this.rulesetForm.controls.definiation.value)
            )
        )).subscribe(j => this.isSaving = false);

My refactor failure 2

onSave(): void {
    this.dialogConfirmService
            .confirmed()
            .pipe(take(1))
            .pipe(
              flatMap(confirmed => this.onConfirm(confirmed)),
            );
}

  private onConfirm(confirmed: any) {
    if (confirmed) {
      this.isSaving = true;
      this._ruleSetMapperService.save(
        this.rulesetForm.controls.name.value,
        JSON.parse(this.rulesetForm.controls.definiation.value)
      );
    } else {
      return Observable.create(function(observer) {
        observer.next(confirmed);
        observer.complete();
      });
    }
  }

Below is the other code that is called (for the sake of completness)

the dialog service

..other stuff...
export class DialogConfirmService {

  ...other stuff...

  public confirmed(): Observable<any> {

    return this.dialogRef
               .afterClosed()
               .pipe(take(1),
                     map(res => {
                        return res;
                      })
                );
  }
}

My saving service

export class MyService {
  save(rsn: string, rsd: string): Observable<any> {
    ...other stuff... (generates the body)

    return this._http.post('myUrl', JSON.stringify(body), { headers: this.headers });
 }
}
tony09uk
  • 2,841
  • 9
  • 45
  • 71

1 Answers1

7

Here are a few alternatives through which you can avoid nesting subscriptions:

1)

this.dialogConfirmService
            .confirmed()
            .pipe(
                tap(() => (this.isSaving = true)),
                switchMap((confirmed) =>
                    confirmed ? this.myService.save('', '') : of(null)
                ),
                catchError((error) => throwError(error))
            )
            .subscribe((data) => this.onSuccess(data));

RxJS operators:

tap - used for side effects;

switchMap - completes the previous observable and subscribes to the new one (combination of switch and map operators) based on the condition. Remember, even if the condition is false, you always need to cover both scenarios.

catchError, throwError - well, used for handling errors

2.

this.dialogConfirmService
            .confirmed()
            .pipe(
                filter((confirmed) => !!confirmed),
                tap(() => (this.isSaving = true)),
                switchMap(() => this.myService.save('', '')),
                catchError((error) => throwError(error))
            )
            .subscribe((data) => this.onSuccess(data));

filter - in this case, it will subscribe to next observable only if the condition is true.

3.

this.dialogConfirmService
            .confirmed()
            .pipe(
                switchMap((confirmed) =>
                    iif(() => confirmed, this.myService.save('', ''))
                ),
                catchError((error) => throwError(error))
            )
            .subscribe((data) => this.onSuccess(data));

iif - this is similar to ternary operator. It accepts a condition function and two observables, however the second observable is not required.

About the http call you make, you should not stringify the payload.

ionut-t
  • 1,121
  • 1
  • 7
  • 14
  • This answer has help me in many way, thank you so much. Looking at your provided examples (and with my very limited knowledge) I believe I will still have to unsubscribe from the observable? PLEASE NOTE: I tested this prior asking and unsubscribe did not throw any error so I added a pipe(take(1)) to your example and still didn't throw, so I really don't know how to tell what the expected behaviour is – tony09uk Jul 14 '20 at 07:21
  • Also thanks for the extra suggestion regarding stringifying the payload, I've removed that now – tony09uk Jul 14 '20 at 07:36
  • 1
    @tony09uk I'm glad it helped you. You don't have to manually unsubscribe from HTTP Client observables. Angular does that for you. In the examples above I've ended the streams with an http call and switchMap has already completed the previous observable so is no need to manually unsubscribe. – ionut-t Jul 14 '20 at 09:18