Chemdream
Chemdream

Reputation: 627

Angular Material Autocomplete observable fires additional time after selecting dropdown option

Anytime I select an option from Material Autocomplete, it makes an additional HTTP GET call to the server. The desired result of selecting a dropdown option should just be to populate the input.

The dropdown options are retrieved dynamically from the server.

HTML:

<form class="example-form">
    <mat-form-field class="example-full-width">
      <input type="text" placeholder="search item" aria-label="Item1" matInput [formControl]="searchTerm" [matAutocomplete]="auto1">
      <mat-autocomplete #auto1="matAutocomplete" [displayWith]="displayFn">
        <mat-option *ngFor="let item of searchResult" [value]="item.Id">
          {{ item.Name }}
        </mat-option>
      </mat-autocomplete>
    </mat-form-field>
  </form>

TypeScript:

    searchTerm : FormControl = new FormControl();

    searchResult = [];
    constructor(private service: AppService){
      this.searchTerm.valueChanges
          .debounceTime(400) 
          .subscribe(data => {
              this.service.search_word(data).subscribe(response =>{
                  this.searchResult = response
              })

Everything else works fine. The only issue is the unexpected extra call to the server when the autocomplete option is selected.

QUICK UPDATE: No answer has totally solved this yet. However, I have narrowed this down to possibly be a problem with displayWith.

Upvotes: 8

Views: 5385

Answers (6)

phanf
phanf

Reputation: 821

You can use keyup instead of valueChanges. Below is how I solved the same issue. Please keep in mind, this example is here to display my use case of keyup with mat-autocomplete.

import { ViewChild, ElementRef } from '@angular/core';
import { fromEvent } from 'rxjs';
import { debounceTime, tap, switchMap, map, filter, distinctUntilChanged } from 'rxjs/operators';

Add mat-autocomplete html, note this also includes a loading spinner during http calls on remote but can be removed.

 <mat-form-field>
   <input matInput #searchInput placeholder="" [matAutocomplete]="auto">             
   <mat-autocomplete #auto="matAutocomplete">
    <mat-option *ngIf="isLoading">
     <mat-spinner diameter="50"></mat-spinner>
    </mat-option>
    <ng-container *ngIf="!isLoading">
     <mat-option *ngFor="let data of filteredData | async" [value]="data.modelValue">
      <span>{{ data.modelValue }}</span>
     </mat-option>
    </ng-container>
   </mat-autocomplete>
  </mat-form-field>

Add the following to your controller.

isLoading: boolean;
@ViewChild('searchInput') searchInput: ElementRef;
filteredData: searchModel[] = [];

Setup the keyup event listener.

fromEvent(this.searchInput.nativeElement, 'keyup').pipe(
  map((event: any) => {
    return event.target.value; // Map search input value
  }),
  filter(res => res.length > 3), // If character length greater then 3
  debounceTime(500),
  distinctUntilChanged(), // If previous search is different from current
  tap((val) => {
    this.isLoading = true;
    this.filteredData = [];
  }),
  switchMap(value =>
    this.service.search(value).pipe(
      finalize(() => this.isLoading = false)
    )

  )
).subscribe(data => {
  this.filteredData = data;
});

Upvotes: 0

Eyad Farra
Eyad Farra

Reputation: 4423

Here is tricky way, Set option value as object instead of string, then when the input changed you can determine if that from typing or selecting.

<!-- Template -->
<mat-option *ngFor="let item of searchResult" [value]="item">

// Component
this.searchTerm.valueChanges.subscribe(modelValue => {
  if (typeof modelValue === 'string') {
    this.searchUser.next(modelValue.toString());
  } else {
    this.searchTerm.setValue(modelValue.Id, {
      emitEvent: false
    });
  }
});

Upvotes: 0

carkod
carkod

Reputation: 2230

As of Angular Material version 7.2.0 you have a new EventEmitter called 'optionSelected', for MatAutocomplete

So your HTML would be something like this:

<mat-autocomplete (optionSelected)="optionSelected($event)" #auto1="matAutocomplete" [displayWith]="displayFn">
    <mat-option *ngFor="let item of searchResult" [value]="item.Id">
      {{ item.Name }}
    </mat-option>
  </mat-autocomplete>

And your .ts:

private optionSelected(event) {
  console.log(event)
}

And you would handle it inside of optionSelected() method.

Give it a try as I don't have all your code. And let me know if it doesn't work.

Upvotes: 1

anode7
anode7

Reputation: 396

you can use (keyup)="onKeyUp($event)" in your input text and you can use it like this

onKeyUp(event: any) {
   this.service.search_word(event.target.value).subscribe(response =>{
                  this.searchResult = response
              })
}

Upvotes: 1

Swoox
Swoox

Reputation: 3750

The problem lies in the amount of event that are tiggered there are two... I already made a bug for it's but it's how it supposed to work. Hope this will help.

This is my solution:

Html:

<mat-option *ngFor="let item of searchResult" [value]="item.Id" (onSelectionChange)="onSelectionChange($event, item.Id)"> // or item

In your component:

private onSelectionChange(_event: any, _id: any) {
  if (_event.isUserInput === true) { // there are two events one with true and one with false;
    this.service.search_word(_id).subscribe(response =>{
         this.searchResult = response
    })
}

Upvotes: 1

Trent
Trent

Reputation: 4316

Your issue is occurring because the dropdown selection changes the value of the searchTerm form control; thus emitting an event for the valueChanges observable on that form control.

There are a few ways to build some logic into your valueChanges observable on the form control in order to ignore this unwanted Http GET request.

Naive solution

The naive solution is to store the dropdown option selection value and skip the observable logic if the selectionValue === searchTerm.value.

var optionSelection; // set this value on selection of a dropdown option
this.searchTerm.valueChanges
  .debounceTime(400) 
  .subscribe(data => {
    if (optionSelection !== searchTerm.value) {
      this.service.search_word(data).subscribe(response =>{
        this.searchResult = response
      })
    }
  })

Better Solution

The FormControl class has a setValue function, which has an optional parameter to specify whether or not to emit an event to the valueChanges observable.

setValue(value: any, options: {
    onlySelf?: boolean;
    emitEvent?: boolean;
    emitModelToViewChange?: boolean;
    emitViewToModelChange?: boolean;
} = {}): void

By changing your dropdown option selection to set the searchTerm form control value with setValue, you can pass the optional emitEvent: false parameter and stop this additional observable event from emitting.

searchTerm.setValue(dropdownValue, { emitEvent: false });

If emitEvent is false, this change will cause a valueChanges event on the FormControl to not be emitted. This defaults to true (as it falls through to updateValueAndValidity).

Upvotes: 4

Related Questions