Search code examples
angularrxjsangular-materialobservable

how to get the exact latest response to HTML using observable in angular12


I am using Angular12, and here i am using autocomplete in angular material, here the issue is that i am able to get result based on api hit, but in html, i always get latest-1 response shown.

Ts:

  private getAutoPopulateData(serchVal: any): Observable<string[]> {
    let data: any = [];
    let results: any = [];
    if (serchVal && serchVal.length > 0) {
      this.searchService.searchOpenSearchData(serchVal).subscribe((searchData: AutoPopulate) => {
        results = searchData.hits;
        if (results.hits.length > 0) {
          results.hits.forEach(h=>{
            let item=h._source;
             let addressProps=Object.keys(item).filter(p=> p.startsWith('AddressLine') && item[p].length >0);
             addressProps.forEach(ap=>{
                let addresses=item[ap].map((ai,idx)=>{
                 let address= `${ai} ${item.City[idx]} ${item.State} ${item.Zip[idx]}`;
                 return {name:item.ProfileName,address}
                });
               data= data.concat(addresses);
               this.seeResultsData = data.length;
             })
            });
        }
        this.resultData = of(data);
        this.isLoading = false;
      });
    }
    else {
      this.isLoading = false
    }
    if(this.resultData) {
      return this.resultData;
    } else 
    return null
  }

  private autoCompleteFormInit() {
    this.stateCtrl.valueChanges.pipe(
      debounceTime(500),
      tap((() => this.isLoading = true)),
      map(value => (this.getAutoPopulateData(value))
        .pipe(finalize(() => this.isLoading = false)))
    ).subscribe(value => {
    this.filteredOptions = value
    });
  }

public resultData: Observable<any> = of([]);
filteredOptions: Observable<any> = of([]);
searchOpenSearchData(searchValue:any): Observable<AutoPopulate> {
     let url =  URLConstants.openSearchUrl + searchValue;
     return this.http.get<AutoPopulate>(url);
   }

HTML:

 <input matInput aria-label="State" placeholder="Enter a Name" [matAutocomplete]="auto" class="paddingleft-5 text-medium" (ngModelChange)="updatedVal($event)" type="text" [formControl]="stateCtrl" *ngIf="form.value.tradingPartnerValue && dropdownSelection == 'tradingPartnerName'"/>
        <mat-autocomplete #auto="matAutocomplete" (optionSelected)="onOptionSelected($event.option)">
          <div *ngIf="showAutocomplete" class="{{seeResultsData >3 ? 'set-height' : ''}}">
            <mat-option *ngIf="isLoading" class="is-loading content-center">
              <mat-spinner diameter="50" class="show-center"></mat-spinner>
            </mat-option>
            <ng-container *ngIf="!isLoading">
            <mat-option class="option-custom text-medium" *ngFor="let state of filteredOptions | async"
              [value]="state" [ngClass]="{'hide-autocomplete': !showAutocomplete}">
                <span [innerHTML]="state.name | highlight : stateCtrl.value"></span>
                <span [innerHTML]="state.address | highlight : stateCtrl.value"></span>
            </mat-option>
            </ng-container>
          </div>
</mat-autocomplete>

Json response:

{
  "took": 178,
  "timed_out": false,
  "_shards": {
    "total": 0,
    "successful": 0,
    "skipped": 0,
    "failed": 0
  },
  "hits": {
    "total": {
      "value": 10000,
      "relation": "gte"
    },
    "max_score": 12.9226,
    "hits": [
      {
        "_index": "pulse",
        "_id": "17320",
        "_score": 12.9226,
        "_source": {
          "ProfileName": "Pharmarx Pharmaceutical Inc",
          "State": "CA",
          "BusinessModel": "Dispenser",
          "LicenseInfo": [
            {
              "LicenseType": "Resident Licensed Sterile Compounding",
              "LicenseNumber": "99658",
              "AddressLine1": "21441 Osborne St Ste C & D",
              "AddressLine2": "",
              "AddressLine3": "",
              "AddressLine4": "",
              "Zip": "91304",
              "City": "Canoga Park"
            },
            {
              "LicenseType": "Retail Pharmacy",
              "LicenseNumber": "50511",
              "AddressLine1": "21441 Osborne St Ste C & D",
              "AddressLine2": "",
              "AddressLine3": "",
              "AddressLine4": "",
              "Zip": "91304",
              "City": "Canoga Park"
            }
          ]
        }
      },
      {
        "_index": "pulse",
        "_id": "7723",
        "_score": 10.891928,
        "_source": {
          "ProfileName": "ENCINO PHARMACY_AKA MDR PHARMACEUTICAL CARE",
          "State": "MI",
          "BusinessModel": "Dispenser",
          "LicenseInfo": [
            {
              "LicenseType": "PHARMACY",
              "LicenseNumber": "5301010993",
              "AddressLine1": "17071 VENTURA BLVD STE 100",
              "AddressLine2": "",
              "AddressLine3": "",
              "AddressLine4": "",
              "Zip": "91316",
              "City": "ENCINO"
            }
          ]
        }
      },
      {
        "_index": "pulse",
        "_id": "7724",
        "_score": 9.796367,
        "_source": {
          "ProfileName": "Encino Pharmacy_aka Mdr Pharmaceutical Care",
          "State": "CA",
          "BusinessModel": "Dispenser",
          "LicenseInfo": [
            {
              "LicenseType": "Retail Pharmacy",
              "LicenseNumber": "34818",
              "AddressLine1": "17059-17071 VENTURA BLVD.",
              "AddressLine2": "STE. 100",
              "AddressLine3": "",
              "AddressLine4": "",
              "Zip": "91316",
              "City": "ENCINO"
            }
          ]
        }
      },
      {
        "_index": "pulse",
        "_id": "192",
        "_score": 9.656487,
        "_source": {
          "ProfileName": "Abdin Pharmacies Pharma LLC",
          "State": "FL",
          "BusinessModel": "Dispenser",
          "LicenseInfo": [
            {
              "LicenseType": "Pharmacy",
              "LicenseNumber": "036936",
              "AddressLine1": "1873 SECOND AVE. NEW",
              "AddressLine2": "",
              "AddressLine3": "",
              "AddressLine4": "",
              "Zip": "10029",
              "City": "YORK"
            },
            {
              "LicenseType": "Pharmacy",
              "LicenseNumber": "033404",
              "AddressLine1": "1401 BRONX RIVER AVE.",
              "AddressLine2": "",
              "AddressLine3": "",
              "AddressLine4": "",
              "Zip": "10472",
              "City": "BRONX"
            },
            {
              "LicenseType": "Pharmacy",
              "LicenseNumber": "PH33043",
              "AddressLine1": "32866 US Hwy 19N",
              "AddressLine2": "",
              "AddressLine3": "",
              "AddressLine4": "",
              "Zip": "34684",
              "City": "PALM HARBOR"
            }
          ]
        }
      },
      {
        "_index": "pulse",
        "_id": "17304",
        "_score": 9.186218,
        "_source": {
          "ProfileName": "PHARMALABS, LLC PharmaLabs",
          "State": "FL",
          "BusinessModel": "Dispenser",
          "LicenseInfo": [
            {
              "LicenseType": "Pharmacy",
              "LicenseNumber": "PH26495",
              "AddressLine1": "10901 ROOSEVELT BOULEVARD NORTH SUITE 1200",
              "AddressLine2": "",
              "AddressLine3": "",
              "AddressLine4": "",
              "Zip": "33716",
              "City": "Saint Petersburg"
            }
          ]
        }
      }
    ]
  }
}

in UI, i need to display, Profile Name as name in UI, along with multiple address present under same ProfileName, for example one sample i have taken from json, so from this array of object must contain,

[{name:'Pharmarx Pharmaceutical Inc', address:'21441 Osborne St Ste C & D,Canoga Park CA 9104'},
{name:'Pharmarx Pharmaceutical Inc', address:'21442 Osborne St Ste C & D(Address2/Address3/Address4)what ever is present must show, Canoga Park CA 9104'}]

so it is like with one Profile Name, we have multiple location address

 "_source": {
      "ProfileName": "Pharmarx Pharmaceutical Inc",
      "State": "CA",
      "BusinessModel": "Dispenser",
      "LicenseInfo": [
        {
          "LicenseType": "Resident Licensed Sterile Compounding",
          "LicenseNumber": "99658",
          "AddressLine1": "21441 Osborne St Ste C & D",
          "AddressLine2": "",
          "AddressLine3": "",
          "AddressLine4": "",
          "Zip": "91304",
          "City": "Canoga Park"
        },
        {
          "LicenseType": "Retail Pharmacy",
          "LicenseNumber": "50511",
          "AddressLine1": "21441 Osborne St Ste C & D",
          "AddressLine2": "",
          "AddressLine3": "",
          "AddressLine4": "",
          "Zip": "91304",
          "City": "Canoga Park"
        }
      ]
    }

Solution

  • You have some issues in your code, but the main reason that you get the previous result is that you do:

    if (this.resultData) {
      return this.resultData;
    }
    

    If this.resultData is populated with the data from a previous request it will return immediately before the observable (this.searchService.searchOpenSearchData(serchVal)) emits and sets a new value for this.resultData.

    You should set this.resultData to the result of the observable from the api request and await the result rather then returning it like you did in your code.

    Something like:

    private getAutoPopulateData(serchVal: any): Observable<string[]> {
      let data: any = [];
      let results: any = [];
      if (serchVal && serchVal.length > 0) {
        this.isLoading = true;
        this.resultData = this.searchService.searchOpenSearchData(serchVal).pipe(
          map((searchData: AutoPopulate) => {
            results = searchData.hits;
            if (results.hits.length > 0) {
              results.hits.forEach(h => {
                let item=h._source;
                let addressProps=Object.keys(item).filter(
                  p => p.startsWith('AddressLine') && item[p].length > 0
                );
                addressProps.forEach(ap => {
                  let addresses=item[ap].map((ai,idx)=>{
                  let address= `${ai} ${item.City[idx]} ${item.State} ${item.Zip[idx]}`;
                  return {name:item.ProfileName, address}
                });
                data = data.concat(addresses);
                this.seeResultsData = data.length;
              });
              return data;
            }
            return null;
          }),
          finalize(() => this.isLoading = false);
        );
      } else {
        this.resultData = of(null);
      }
      return this.resultData;
    }
    

    I tried to return null where needed, but check if those places are according to your need+s (maybe you want to return an empty array instead). I used pipe, map and finalize from rxjs which need to be imported. I moved your isLoading = false into finalize so it also "stops loading" on error responses.

    If further questions, leave a comment.

    UPDATE

    After you added additional code to your question in an "edit" I have to update my answer:

    With the changes I mention above, your autoCompleteFormInit method should change as follows:

    private this.subscription: Subscription;
    
    private autoCompleteFormInit() {
      this.subscription = this.stateCtrl.valueChanges.pipe(
        debounceTime(500),
        switchMap(value => this.getAutoPopulateData(value))
      ).subscribe(value => this.filteredOptions = value);
    }
    
    // To prevent memory leaks, store subscription and unsubscribe on destroy
    public ngOnDestroy(): void {
      if (this.subscription) this.subscription.unsubscribe();
    }
    

    I also changed the logic regarding isLoading, it is better if it is only called in the method that actually sends the request.

    There is still a lot more that can be improved, but this should work with minimal code changes in the original posted code example.