Search code examples
javascriptangulartypescriptangular-materialautocomplete

Trigger angular material autocomplete after three letters


I am trying to create an Angular Material Autocomplete that triggers after the user has typed in at least three letters. I get the data from an API call...

Currently I have it working like so that upon click in the input field it triggers the API call and it calls all the data which is really bad...

My ideal scenario and what I want to achieve would be that the API call triggers after the user has typed in at least three characters and only matching results are displayed in the mat-option element...

I hope I was clear enough with what I am trying to achieve, if not, let me know in a comment bellow...

Here is my code: dashboard.component.html:

<form [formGroup]="dasboardSearchForm" class="dashboardSearch">
  <div class="form-fields">
    <mat-form-field class="dashboard-search">
      <mat-label>Search users...</mat-label>
      <input type="text" matInput [formControl]="myControl" placeholder="search users..." [matAutocomplete]="auto" (change)="onSelected($event)">
      <mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn" >
        <mat-option *ngFor="let option of filteredOptions | async" [value]="option" routerLink="/customers/customers/{{option.id}}">
          {{option.name}}
        </mat-option>
      </mat-autocomplete>
    </mat-form-field>
  </div>
</form>

dashboard.component.ts:

  onSelected(event: any) {
    this.filteredOptions = this.myControl.valueChanges.pipe(
      // debounceTime(500),
      startWith(''),
      switchMap(value => this._filter(value))
    );
    console.log('onSelected: ', this.filteredOptions);
  }

  displayFn(user: AutocompleteCustomers): string {
    return user && user.name ? user.name : '';
  }

  private _filter(value: string) {
    const filterValue = value.toLowerCase();
    return this.dashboardService.getCustomers().pipe(
      filter(data => !!data),
      map((data) => {
        debounceTime(3000);
        console.log(data);
        return data.filter(option => option.name.toLowerCase().includes(filterValue));
      })
    )
  }

dashboard.service.ts:

  getCustomers(): Observable<AutocompleteCustomers[]> {
    return this.httpClient.get<AutocompleteCustomers[]>(`${environment.apiUrl}data/customers`);
  }

here is also the Observable model:

export interface AutocompleteCustomers {
  id: string;
  name: string;
}

Solution

  • The function you're looking for is the RxJS filter operator.

    const filteredChanges = this.myControl.valueChanges.pipe(
      filter((value) => value.length >= 3)
    );
    

    There are, however, several other issues with your code.

    1. The autocomplete will never be triggered because the changes observer is only set up after the user selects an option, which they can't do without autocomplete providing options to select. To fix this, move the valueChanges observer to class constructor or assign it directly to a class property.

    2. debounceTime needs to be used in a pipe. With your current use, you are just creating an observable and not doing anything with it. It's also a bit strange to debounce the HTTP responses. Using debounce can be a good idea here, but you should use it on the input changes observable to reduce the number of requests to the backend, rather than ignoring responses.

    Here's a rough idea of how it should work:

    class DashboardComponent {
    
      // ...
    
      public filteredOptions = this.myControl.valueChanges.pipe(
        filter((value) => value?.length > 3),
        debounceTime(500),
        switchMap(value => this._filter(value))
      );
    
      private _filter(value: string) {
        const filterValue = value.toLowerCase();
        return this.dashboardService.getCustomers().pipe(
          map((data) => {
            if (!data) {
              return [];
            }
            return data.filter(option => option.name.toLowerCase().includes(filterValue));
          })
        )
      }
    }
    

    It would probably be best to pass the query to the server and only return the customers that match the query. If you are going to search through the customers on the client side, it would be more efficient to just load the customers once and reuse the same list when searching.

    class DashboardComponent {
    
      // ...
    
      private customers = [];
    
      public filteredOptions = this.myControl.valueChanges.pipe(
        filter((value) => value?.length > 3),
        map((value) => {
          const query = value.toLowerCase(),
          return this.customers.filter(customer => customer.name.toLowerCase().includes(query))
        }),
      );
    
      constructor(private dashboardService: DashboardService) {
        this.dashboardService.getCustomers().subscribe((data) => {
          this.customers = data || [];
        });
      }
    }