Search code examples
angulartypescriptrxjsangular-reactive-formsangular-forms

How to simplify Rx.js functions and have only one subscribe


I have one angular component a I want to simplify this Rx.js functions to have one only one subscribe and not have subscrition in subsribe. Is it possible? Otherwise this Rx.js has task debounce form values whenever they change and send to server values to check if app password is valid, if it is, toastr and modal close is called.

import {
    ChangeDetectionStrategy,
    Component,
    OnDestroy,
    OnInit,
} from '@angular/core';
import {
    FormControl,
    FormGroup,
    FormsModule,
    ReactiveFormsModule,
} from '@angular/forms';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { FormWrapperComponent } from '@app/components/form-wrapper/form-wrapper.component';
import { EHttpResponseCode } from '@app/enums/http-response';
import { IPasswordForm } from '@app/interfaces/password-form.interface';
import { AppPasswordService } from '@app/services/app-password.service';
import { ModalService } from '@app/services/modal.service';
import { environment } from '@environments/environment';
import { I18NextModule, I18NextPipe } from 'angular-i18next';
import { ToastrService } from 'ngx-toastr';
import { debounceTime, distinctUntilChanged, Subscription } from 'rxjs';

@Component({
  selector: 'app-application-password-modal',
  imports: [
    MatInputModule,
    MatFormFieldModule,
    I18NextModule,
    FormsModule,
    ReactiveFormsModule,
    FormWrapperComponent,
  ],
  templateUrl: './application-password-modal.component.html',
  styleUrl: './application-password-modal.component.scss',
  changeDetection: ChangeDetectionStrategy.OnPush,
})
export class ApplicationPasswordModalComponent implements OnInit, OnDestroy {
  /**
   * @description
   * Property passwordForm is a FormGroup that contains the form to fill up password
   * and have access to change database config
   *
   * @type {FormGroup<IPasswordForm>}
   */
  passwordForm: FormGroup<IPasswordForm>;

  /**
   * Subscription to password values changes
   * @type {Subscription}
   */
  valuesChangesSubscription: Subscription = new Subscription();

  /**
   * Subscription to verify password
   * @type {Subscription | null}
   */
  verifyPasswordSubscription: Subscription | null = null;

  /**
   * @descriptio
   * Constructor
   *
   * @param {AppPasswordService} appPasswordService
   * @param {ModalService} modalService
   * @param {ToastrService} toastr
   * @param {I18NextPipe} i18nextPipe
   */
  constructor(
    private appPasswordService: AppPasswordService,
    private modalService: ModalService,
    private toastr: ToastrService,
    private i18nextPipe: I18NextPipe
  ) {
    this.passwordForm = new FormGroup<IPasswordForm>({
      password: new FormControl<string | null>('', []),
    });
  }

  /**
   * @description
   * Lifecycle method
   * Update form values from session storage is stored data are present
   * and subscribing to form value changes to log form validation from backend
   *
   * @returns {void}
   */
  ngOnInit(): void {
    if (!environment.production) {
      this.toastr.info(
        this.i18nextPipe.transform('In developer mode password is dbbrix.')
      );
    }

    /**
     * Subscribe to password form value changes
     * Debounce logic, 1 second delay
     * Distinct until changed form data, only emit if value has changed
     * If data are present, data are sended to backend and if data are correct, modal is closed
     */
    this.valuesChangesSubscription = this.passwordForm.valueChanges
      .pipe(
        debounceTime(1000),
        distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))
      )
      .subscribe(() => {
        if (this.passwordForm.valid) {
          const passwordControl = this.passwordForm.get('password');

          const data = {
            password: this.passwordForm.value.password ?? '',
          };
          this.verifyPasswordSubscription = this.appPasswordService
            .verifyAppPassword(data)
            .subscribe({
              next: res => {
                if (
                  res.status === EHttpResponseCode.OK ||
                  res.status === EHttpResponseCode.CREATED
                ) {
                  this.modalService.closeApplicationPasswordModal();
                  this.toastr.success(
                    this.i18nextPipe.transform(
                      'App password is correct. Now You can fill up database config.'
                    )
                  );
                }

                if (res.status === EHttpResponseCode.UNAUTHORIZED) {
                  passwordControl?.setErrors({ incorrect: true });
                  this.toastr.error(
                    this.i18nextPipe.transform(
                      'App password is incorrect. Please try again.'
                    )
                  );
                }
              },
              error: () => {
                if (!environment.production) {
                  if (this.passwordForm.value.password === 'password') {
                    passwordControl?.setErrors(null);
                    this.modalService.closeApplicationPasswordModal();
                    this.toastr.success(
                      this.i18nextPipe.transform(
                        'App password is correct. Now You can fill up database config.'
                      )
                    );
                  } else {
                    passwordControl?.setErrors({ incorrect: true });
                    this.toastr.error(
                      this.i18nextPipe.transform(
                        'App password is incorrect. Please try again.'
                      )
                    );
                  }
                }
              },
            });
        }
      });
  }

  ngOnDestroy(): void {
    this.verifyPasswordSubscription?.unsubscribe();
    this.valuesChangesSubscription?.unsubscribe();
  }
}

Solution

  • Use a switchMap to perform the inner API call.

    We use the rxjs operator filter to prevent the inner API from triggering, essentially replacing the if (this.passwordForm.valid) {.

    The end code will look like below.

    ngOnInit(): void {
      if (!environment.production) {
        this.toastr.info(
          this.i18nextPipe.transform('In developer mode password is dbbrix.')
        );
      }
      /**
       * Subscribe to password form value changes
       * Debounce logic, 1 second delay
       * Distinct until changed form data, only emit if value has changed
       * If data are present, data are sended to backend and if data are correct, modal is closed
       */
      this.valuesChangesSubscription = this.passwordForm.valueChanges
        .pipe(
          debounceTime(1000),
          distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
          filter(() => this.passwordForm.valid),
          switchMap(() => {
            const data = {
              password: this.passwordForm.value.password ?? '',
            };
            return this.appPasswordService.verifyAppPassword(data);
          })
        )
        .subscribe({
          next: (res) => {
            const passwordControl = this.passwordForm.get('password');
            if (
              res.status === EHttpResponseCode.OK ||
              res.status === EHttpResponseCode.CREATED
            ) {
              this.modalService.closeApplicationPasswordModal();
              this.toastr.success(
                this.i18nextPipe.transform(
                  'App password is correct. Now You can fill up database config.'
                )
              );
            }
    
            if (res.status === EHttpResponseCode.UNAUTHORIZED) {
              passwordControl?.setErrors({ incorrect: true });
              this.toastr.error(
                this.i18nextPipe.transform(
                  'App password is incorrect. Please try again.'
                )
              );
            }
          },
          error: () => {
            if (!environment.production) {
              if (this.passwordForm.value.password === 'password') {
                passwordControl?.setErrors(null);
                this.modalService.closeApplicationPasswordModal();
                this.toastr.success(
                  this.i18nextPipe.transform(
                    'App password is correct. Now You can fill up database config.'
                  )
                );
              } else {
                passwordControl?.setErrors({ incorrect: true });
                this.toastr.error(
                  this.i18nextPipe.transform(
                    'App password is incorrect. Please try again.'
                  )
                );
              }
            }
          },
        });
    }