1

I'm an Angular and rxjs beginner and in my app I have a form control with mat-autocomplete from https://material.angular.io/components/autocomplete/overview.

On each valueChanges event of that control I want to send an api call which should return an array of data to be displayed as mat-autocomplete options. The problem is that in order to do this I need to return api's response of type Observable<ApiResponseData> as an Array<MaterialData> inside of my filtering function.

So far I'm listening to valueChanges where Array<MaterialData> is expected.

this.filteredMaterialData = this.orderPositionFormGroup.controls['materialId'].valueChanges.pipe(
  map(
    value => {
      if (value.length < 3) {
        return []
      } else {
        return this.filterMaterial(value)
      }
    }
  )
);

filterMaterial function looks like this and is incorrect because backendFilteredData is returned before being assigned inside of subscription.

private filterMaterial(name: string): Array<MaterialData> {
  this.isLoading = true;
  let backendFilteredData: Array<MaterialData> = [];
  this.materialService.getMaterialsByName(name).subscribe(
    data => {
      backendFilteredData = data.content;
      this.isLoading = false;
    }
  )
  return backendFilteredData
}

And this is how materialService looks like.

public getMaterialsByName(name: string): Observable<ApiResponseData> {
  const headers: HttpHeaders = new HttpHeaders(
    {
      "Content-Type": "application/json; charser=utf-8"
    }
  );
  
  const params: HttpParams = new HttpParams()
    .set('page', 0)
    .set('size', 500)
    .set('sort', "name,asc");

  const body = {
    logic: "and",
    cond: [
      {
        field: "name",
        operator: "ilike",
        format: "",
        value: "%" + name + "%"
      }
    ]
  };

  return this.http.post<ApiResponseData>(`${environment.backendHost}/material/lite/filter`, body, {
    headers: headers,
    params: params
  });
}

Knowing that ApiResponseData.content contains desired Array<MaterialData> how can I modify my code to be able to return that api reponse as a filterMaterial function result?

Zets
  • 13
  • 3

1 Answers1

0

The golden rule is to never do nested subscribe. As you can see, this could lead to errors. To solve this, you always need to return an observable, and then concat them inside a pipe. In your case I would use switchMap.

here is the refactored code. I haven't tested it but should be correct

this.filteredMaterialData = this.orderPositionFormGroup.controls['materialId'].valueChanges.pipe(
  switchMap( // use switchMap, because we will return an observable and not a value
    value => {
      if (value.length < 3) {
        return of([]);  // use of, because we need to return an observable
      } else {
        return this.filterMaterial(value); // this will return an observable
      }
    }
  )
);

private filterMaterial(name: string): Observable<Array<MaterialData>> {
  this.isLoading = true;
  return this.materialService.getMaterialsByName(name).pipe( // you return an obervable here
    map(data => {
      this.isLoading = false;
      return data.content;
    });
  );
}

and just as an addition, you could write the whole switchMap as a one-liner: switchMap( value => value.length < 3 ? of([]) : this.filterMaterial(value) )

Zerotwelve
  • 2,087
  • 1
  • 9
  • 24
  • In this solution http request is not being sent as from what I understand I'm no longer subscribing to getMaterialsByName. Is there a way around it? – Zets Mar 29 '23 at 07:52
  • as soon as you subscribe to `this.filteredMaterialData` you will automatically subscribe to `getMaterialsByName`. the best way to do it, is to use the async pipe in your HTML i.e. ` SOME_CODE_HERE ` – Zerotwelve Mar 29 '23 at 08:05
  • My bad, I was piping it through async only when isLoading was false which was making subscription never happen. The answer is perfectly correct. – Zets Mar 29 '23 at 10:01