angulartypescriptrxjsangular-reactive-forms

When API fails, the "valueChanges" won't be executed in the next event [reactive forms + rxjs switchmap]


I have a dropdown that contains a list of countries, the moment I choose one of the those countries from the dropdown, all the provinces related to this country should be displayed.

I have two APIs - one that returns the list of countries, and the second is executed when choosing a country from the drop down, it will take the country Id as param and will return the provinces according to this Id.

Everything looks ok, until I choose a country from the dropdown that doesn't have any provinces in the second API.

In that case the API (that returns provinces according to country ID) will fail and in next time when I choose a new country from the dropdown (even if it does contain provinces), the API won't work (I don't see any call made to the province API).

Any idea how to resolve this issue?

This is the service:

export class CountriesService {

  private readonly countries$: Observable<Country[]>;

  constructor(private http: HttpClient) {
    this.countries$ = http.get<Country[]>(' http://localhost:3000/countries');
  }

  getCountries = (): Observable<Country[]> => this.countries$;

  getProvinces(countryId: string): Observable<Province[]> {
    return this.http.get<Province[]>('http://localhost:3001/countriesProvinces/' + countryId)
      .pipe(map(result => result['provinces']));
  }
}

This is the template:

<ng-container *ngIf="countries$ | async as countries">
  <select [formControl]="countryDropDown">
    <option *ngFor="let country of countries" [value]="country.id">{{country.name}}</option>
  </select>
</ng-container>

<div *ngIf="provinces$ | async as provinces">
  <ng-container *ngIf="provinces$">
        <p *ngFor="let province of provinces">{{province | json}}</p>
  </ng-container>
</div>

And this is the controller:

export class FormsComponent {
  countryDropDown: FormControl = new FormControl<Country['id']>(null);
  countries$: Observable<Country[]> = this.countriesService.getCountries();
  provinces$: Observable<Province[]>;

  constructor(private readonly countriesService: CountriesService) {
    this.provinces$ = this.countryDropDown.valueChanges.pipe(
      switchMap(countryId => this.countriesService.getProvinces(countryId))
    );
  }
}

This is the scenario when country 3 and 4 do not have any provinces:

enter image description here

This is the country API:

{
  "countries": [
    {
      "id": "1",
      "name": "Country_1"
    },
    {
      "id": "2",
      "name": "Country_2"
    },
    {
      "id": "3",
      "name": "Country_3"
    },
    {
      "id": "4",
      "name": "Country_4"
    }
  ]
}

This is the provinces API:

when called with a country id, it will return only the provinces of this ID, else it will fail

{
  "countriesProvinces": [
    {
      "id": "1",
      "provinces": [
        {
          "id": "1",
          "name": "provinces_11"
        },
        {
          "id": "2",
          "name": "provinces_12"
        },
        {
          "id": "3",
          "name": "provinces_13"
        }
      ]
    },
    {
      "id": "2",
      "provinces": [
        {
          "id": "1",
          "name": "provinces_21"
        },
        {
          "id": "2",
          "name": "provinces_22"
        },
        {
          "id": "3",
          "name": "provinces_33"
        }
      ]
    }
  ]
}

Solution

  • One change is needed which is handling the error in the service (not when the event happens (valueChanges)):

    This code, needs to be updated:

    getProvinces(countryId: string): Observable<Province[]> {
        return this.http.get<Province[]>('http://localhost:3001/countriesProvinces/' + countryId)
          .pipe(map(result => result['provinces']));
      }
    

    To look like this:

      getProvinces(countryId: string): Observable<Province[]> {
        return this.http.get<Province[]>('http://localhost:3001/countriesProvinces/' + countryId)
          .pipe(map(result => result['provinces'])).pipe(
            catchError(async (error) => console.log(error)));
      }