angularobservablemat-autocomplete

mat-autocomplete not filtering with observable


I have two mat-autocomplete drop downs in a reactive Angular form (Angular and Angular Material v12).

One works. It is pulling an array of objects from a service and does not include an Observable for the options. It is developed similarly to the example. The other one is subscribing to an observable for the drop down options, and they show up, but cannot filter. I see no errors.

My hypothesis is that the either the filter is firing before the data is there, or there is some other issue with how the filter is applied. The data shows up just fine - I just can't type and filter.

I set up the observable code slightly differently to allow the subscription on the API to fire before the subscription on the filter and to compare the unique ID. It doesn't seem to have worked. I cannot determine which part of the code is failing. I have tried repeating the working code with the observable, but it can't filter the type in the same way (takes two arguments), and I'm stuck there.

Perhaps there is a way to simplify my subscription? I understand what is going on, but my observable experience is light. Relevant code below:

Working template:

<mat-grid-tile-header>
    Waste Type
</mat-grid-tile-header>
   <mat-form-field appearance="standard">
      <mat-label>Waste Type</mat-label>
         <input
            tabindex="2"
            #waste_type
            matInput
            [matAutocomplete]="waste_type_auto"
            placeholder="Choose a Waste Type"
            class="input"
            type="text"
            formControlName="waste_type"
            [ngClass]="{'is-success' : collectForm.get('waste_type').valid && collectForm.get('waste_type').dirty, 'is-danger' : !collectForm.get('waste_type').valid }"
             aria-label="Waste Type">
             <mat-autocomplete #waste_type_auto="matAutocomplete">
                <mat-option *ngFor="let waste_type of filteredWaste_Types$ | async" [value]="waste_type.name">
                <img class="example-option-img" aria-hidden [src]="waste_type.icon" height="25">
                <span>{{ waste_type.name }}</span>
                </mat-option>
             </mat-autocomplete>
             <mat-hint [hidden]="collectForm.get('waste_type').valid" align="end">Waste type (e.g., Recycling) is required</mat-hint>
              </mat-form-field>

Non-working template (virtually the same):

<mat-grid-tile-header>
          Tare
        </mat-grid-tile-header>
            <mat-form-field appearance="standard">
              <mat-label>Waste Container</mat-label>
              <input
              tabindex="4"
              #tare_container
              placeholder="Enter Container"
              matInput
              [matAutocomplete]="tare_auto"
              class="input"
              type="text"
              formControlName="tare_container"
              [ngClass]="{'is-success' : collectForm.get('tare_container').valid && collectForm.get('tare_container').dirty, 'is-danger' : !collectForm.get('tare_container').valid }"
               aria-label="Tare Container">
                <mat-autocomplete #tare_auto="matAutocomplete">
                  <mat-option *ngFor="let tare of filteredTares$ | async" [value]="tare.container_name">
                  <img class="example-option-img" aria-hidden [src]="tare.container_icon" height="25">
                  <span>{{ tare.container_name }}</span>
                  </mat-option>
                </mat-autocomplete>
                <mat-hint [hidden]="collectForm.get('tare_container').valid" align="end">Tare (e.g., Lg Compost Bin) is required</mat-hint>
                </mat-form-field>

.ts for both filters

filteredWaste_Types$: Observable<Waste_Type[]>;
filteredTares$: Observable<Tare[]>;

// Working Value Changes Subscription

this.filteredWaste_Types$ = this.collectForm.get('waste_type').valueChanges
        .pipe(
          startWith(''),
          map(value => this._filteredWaste_Types(value))
        );

// Non-Working Value Changes Subscription

this.filteredTares$ = zip(
       this.collectForm.get('tare_container').valueChanges
       .pipe(
         startWith('')),
         this.ts.tares,
        )
        .pipe(
          map(([value, tares]) => {
          const filtered = value ? this._filteredTares(value, tares) : tares;
          return filtered;
        })
      );

// Working Filter

private _filteredWaste_Types(value: string): Waste_Type[] {
    const filterValue = value.toLowerCase();
    return this.wts.waste_types.filter(waste_type => waste_type.name.toLowerCase().includes(filterValue));
}

// Non-Working Filter
    
private _filteredTares(value: string, tares: Tare[]): Tare[] {
     const filterValue = value.toLowerCase();
     return tares.filter(tare => tare.container_name.toLowerCase().includes(filterValue));
}

Working waste_type service:

waste_types: Waste_Type[] = [
  {
    name: 'Batteries',
    icon: './assets/img/battery_icon.png'
  },
  {
    name: 'Cardboard',
    icon: './assets/img/cardboard_icon.png'
  },
  {
    name: 'Compost',
    icon: './assets/img/compost_icon.png'
  }];

Non-working tare service:

public cachedTares: Tare[] = [];
    
public get tares() {
   return this.api.getTares()
   .pipe(
    map((tares) => {
    this.cachedTares = tares;
    return tares;
    })
    );
    };
    

API that serves non-working service:

public getTares() {
  const uri = '/containers';
  if (this.isOnline) {
    return this.http.get<Tare[]>(uri, this.httpOptions).pipe(
    map(this.cacheGetRequest(uri)),
    );
    } else {
    const result = this.getCachedRequest<Tare[]>(uri);
    return new BehaviorSubject(result).asObservable();
    }
   }

The data structure of each is virtually the same:

export interface Waste_Type {
  name: string;
  icon: string;
}

export interface Tare {
  id: number;
  container_name: string;
  container_icon: string;
  container_weight: number;
}

Solution

  • I guess you just need to replace zip with combineLatest rxjs operator.

    The difference is:

    The combineLatest operator behaves similarly to zip, but while zip emits only when each Observable source has previously emitted an item, combineLatest emits an item whenever any of the source Observables emits an item

    It means that your filteredTares$ observable emits value only once when you received response from TareService. After that it keeps silence even if valueChanges is emitting new values(even when you type in input)

    this.filteredTares$ = combineLatest([
      this.collectForm2.get('tare_container').valueChanges.pipe(startWith('')),
      this.ts.tares,
    ])
    ...
    

    Stackblitz Example