Home > Net >  Is there a way to neaten up this chain of rxjs operators
Is there a way to neaten up this chain of rxjs operators

Time:08-06

So I have what I consider to be quite a complicated stream of Rxjs piped observables, as shown below. Everything starts off okay, but at the part where I put the comment

//THIS IS WHERE IT STARTS GETTING A BIT CRAZY

It starts getting a bit weird for me. The issue is that at that point, if the addressUsage type is POSTAL, we need to make another network request and then eventually show a modal (which then needs to be subscribed to), whereas for any other type of address, nothing more is needed at that point.

So to cater for this I just start returning of(null) or of([]) from the switchmaps except for when its the postal address type. This works and achieves the functionality that I need, but it feels very wrong. Is there a nicer/more readable way in which I can achieve this functionality?

this.entitiesManagementService.searchAddressEventEmitter$.pipe(filter((addressUsage: AddressUsage) => {
  if (this.addressAttributesSearchForm.valid && addressUsage === this.addressUsage) {
    return true;
  } else {
    this.addressAttributesSearchForm.markAllAsTouched();
    return false;
  }
})).pipe(this.takeUntilDestroyed(), switchMap((_) => {
  return forkJoin([
    this.entitiesManagementService.getAddressClassification(<payload>),
    this.entitiesManagementService.searchAddress(<payload>),
  ]);
})).pipe(switchMap(([fields, searchResults]) => {
  const addressFieldDetails = fields[0].addressFieldDetailDetails;

  const dialogRef = this.modalService.open(SelectAddressModalComponent, {
    data: searchResults.genericAddressDetail.genericAddress,
  });
  return forkJoin([dialogRef.afterClosed(), of(addressFieldDetails)]);
}))
  .pipe(filter(([selectedAddress, _]) => selectedAddress !== ''))
  .pipe(switchMap(([selectedAddress, fields]: [GenericAddress, AddressFieldConfig[]]) => {
  //THIS IS WHERE IT STARTS GETTING A BIT CRAZY
    if (this.addressUsage === AddressUsage.POSTAL) {
      const formValue = this.addressAttributesSearchForm.getRawValue();
      const postalCode = selectedAddress.addressField.find(field => field.addressFieldName === 'POST_CD')!.addressFieldValue;
      const suburb = selectedAddress.addressField.find(field => field.addressFieldName === 'SUBURB')!.addressFieldValue;
      return forkJoin([this.entitiesManagementService.getSuburbInformation(this.componentName, new SearchAreaDetailsPayload(this.getKeyByValue(formValue.classification), this.addressUsage, postalCode, suburb)), of(selectedAddress), of(fields)]);
    } else if (selectedAddress.description === 'createExceptionAddress') {
      this.goToExceptionForm();
      return forkJoin([]);
    } else {
      this.selectedDocId = selectedAddress.docID;
      this.setupFormValidatorsAndPrepopFields(fields, selectedAddress.addressField);
      return forkJoin([]);
    }
  })).pipe(switchMap(([suburbInformation, selectedAddress, fields]) => {
  if (suburbInformation !== null) {
    const dialogRef = this.modalService.open(SelectSuburbModalComponent, {
      width: '900px',
      height: '600px',
      panelClass: 'mat-dialog-override',
      data: suburbInformation?.areaDetails?.postalAddress ?? [],
    });

    return forkJoin([dialogRef.afterClosed(), of(selectedAddress), of(fields)]);
  } else {
    return of([]);
  }
})).subscribe(([selectedSuburb, selectedAddress, fields]) => {
  this.selectedDocId = selectedAddress.docID;
  this.setupFormValidatorsAndPrepopFields(fields, selectedAddress.addressField, selectedSuburb);
});

CodePudding user response:

You may want to consider to use the EMPTY Observable, i.e. an Observable which does not emit anything and completes immediately.

The code would look like this

//THIS IS WHERE IT STARTS GETTING A BIT CRAZY
    if (this.addressUsage === AddressUsage.POSTAL) {
      ....
      return forkJoin([this.entitiesManagementService.getSuburbInformation(this.componentName, new SearchAreaDetailsPayload(this.getKeyByValue(formValue.classification), this.addressUsage, postalCode, suburb)), of(selectedAddress), of(fields)]);
    } else if (selectedAddress.description === 'createExceptionAddress') {
      this.goToExceptionForm();
    } else {
      this.selectedDocId = selectedAddress.docID;
      this.setupFormValidatorsAndPrepopFields(fields, selectedAddress.addressField);
    }
    // here you return EMPTY
    return EMPTY

CodePudding user response:

Most of your complexity looks like it's incidental. Not really fundamental to the domain at hand, but rather to how you've structured your program. That can't really be fixed from the small snippet of code you've shown here.

From what you've shown, you can remove some of the clutter by simply not doing redundant calls to pipe.

Semantically, forkJoin([]) and EMPTY are much the same. They emit nothing and complete right away. EMPTY is the better choice for clarity, and type-checking purposes.

forkJoin with a bunch of of(a), of(b), of(c) calls works as a way to combine values, but it's a lot of extra ceremony to be wrapping an unwrapping single-value observables everywhere. You can just map such value directly into a stream wherever they're still in scope.

Doing so is also a bit less restrictive. For example, when I filter selectedAddress before mapping (rather than destructuring the input to a filter later). It would also let you structure our data in a format that is neither a tuple nor an array.

For example:
map(selectedAddress => [selectedAddress, addressFieldDetails])
could be a JavaScript Object instead:
map(selectedAddress => ({address: selectedAddress, details: addressFieldDetails}))

this.entitiesManagementService.searchAddressEventEmitter$.pipe(

  filter((addressUsage: AddressUsage) => {
    if (this.addressAttributesSearchForm.valid && addressUsage === this.addressUsage) {
      return true;
    }

    this.addressAttributesSearchForm.markAllAsTouched();
    return false;
  }),

  this.takeUntilDestroyed(),

  switchMap(_ => forkJoin([
    this.entitiesManagementService.getAddressClassification<payload>(),
    this.entitiesManagementService.searchAddress<payload>()
  ])),

  switchMap(([[{addressFieldDetails}], searchResults]) => {
    const dialogRef = this.modalService.open(SelectAddressModalComponent, {
      data: searchResults.genericAddressDetail.genericAddress,
    });
    return dialogRef.afterClosed().pipe(
      filter(selectedAddress => selectedAddress !== ''),
      map(selectedAddress => [selectedAddress, addressFieldDetails])
    );
  }),

  switchMap(([selectedAddress, fields]: [GenericAddress, AddressFieldConfig[]]) => {
    if (this.addressUsage === AddressUsage.POSTAL) {
      const formValue = this.addressAttributesSearchForm.getRawValue();
      const postalCode = selectedAddress.addressField.find(field => field.addressFieldName === 'POST_CD')!.addressFieldValue;
      const suburb = selectedAddress.addressField.find(field => field.addressFieldName === 'SUBURB')!.addressFieldValue;
      return this.entitiesManagementService.getSuburbInformation(
        this.componentName, 
        new SearchAreaDetailsPayload(this.getKeyByValue(formValue.classification), 
        this.addressUsage, 
        postalCode, 
        suburb)
      ).pipe(
        map(suburbInformation => [suburbInformation, selectedAddress, fields])
      );
    } else if (selectedAddress.description === 'createExceptionAddress') {
      this.goToExceptionForm();
    } else {
      this.selectedDocId = selectedAddress.docID;
      this.setupFormValidatorsAndPrepopFields(fields, selectedAddress.addressField);
    }
    return EMPTY;
  }),

  filter(([suburbInformation]) => suburbInformation !== null),

  switchMap(([suburbInformation, selectedAddress, fields]) => {
    const dialogRef = this.modalService.open(SelectSuburbModalComponent, {
      width: '900px',
      height: '600px',
      panelClass: 'mat-dialog-override',
      data: suburbInformation?.areaDetails?.postalAddress ?? [],
    });
    return dialogRef.afterClosed().pipe(
      map(selectedSuburb => [selectedSuburb, selectedAddress, fields])
    );
  })

).subscribe(([selectedSuburb, selectedAddress, fields]) => {
  this.selectedDocId = selectedAddress.docID;
  this.setupFormValidatorsAndPrepopFields(fields, selectedAddress.addressField, selectedSuburb);
});
  • Related