Why is onSelectionChange called twice?

Viewed 20274

I am using Angular Material Autocomplete as follows:

<mat-autocomplete #autocomplete="matAutocomplete" [displayWith]="displayFn" autoActiveFirstOption>
  <mat-option *ngFor="let option of filteredOptions$ | async" [value]="option" (onSelectionChange)="onSelectionChanged(option)" >
    {{displayFn(option)}}
  </mat-option>
</mat-autocomplete>    

This is the handler:

onSelectionChanged(option) {
  console.log('Selected ' + option.name);
}

For some reason onSelectionChanged() gets called twice. The second time with the old value! I don't get why. What is happenning here?

Selecting item 1 and then item 2 and then item 3 will print:

> Selected item 1
> Selected item 2
> Selected item 1  // The unwanted call with the old value
> Selected item 3
> Selected item 2  // The unwanted call with the old value
10 Answers

I was facing the same problem for a mat-option inside a mat-select and fixed this way:

Template

<mat-select>
 <mat-option (onSelectionChange)="handleMetaSignalChange(metaSignal.name,$event);" *ngFor="let metaSignal of metaSignals" [value]="metaSignal">
  {{ metaSignal.name }}
 </mat-option>
</mat-select>

Code

 handleMetaSignalChange(metaSignal: string, event: any) {
    if (event.isUserInput) {    // ignore on deselection of the previous option
      console.log('Meta Signal Changed to ' + metaSignal + event.isUserInput);
    }
 }

material has optionSelected event you can use it

<mat-autocomplete #autocomplete="matAutocomplete" (optionSelected)="onSelectionChanged($event)" [displayWith]="displayFn" autoActiveFirstOption>
 <mat-option *ngFor="let option of filteredOptions$ | async" [value]="option" >
    {{displayFn(option)}}
 </mat-option>
</mat-autocomplete>  

and get your value that way

onSelectionChanged(event) {
   console.log(event.option.value);
}

If you need to get the whole OBJECT and use its children values in the component:

1- Send both object and $event from DOM to component.ts.

<!-- Printing out the country name and flag only -->

 <mat-autocomplete #auto="matAutocomplete">
   <mat-option 
      *ngFor="let country of filteredCountries$ | async" 
      [value]="country.name"
      (onSelectionChange)="getSelectedCountry(country, $event)">
      <img class="example-option-img" aria-hidden [src]="country.flag" height="25">
      <span>{{country.name}}</span>
   </mat-option>
 </mat-autocomplete>

2- Now update your values in the component.ts

getSelectedCountry(country: ICountry, event: any): void {
   if (event.isUserInput) {    // ignore on deselection of the previous option
     console.log("Selected country name: ", country.name);
     console.log("Selected country code: ", country.code);
     console.log("Selected country flag link: ", country.flag);
}

Note: ICountry is my country Interface which is not required to have.

As noted in this issue, this is the expected behavior of onSelectionChanged. A selection change event is fired not only when an option is selected but also when it is deselected. So when an option is selected, the event fires on it, and also on any option that is deselected.

Try using optionSelected instead.

You can check if event was triggered by user as follows:

//.html 
 <mat-option *ngFor="let option of filteredOptions$ | async" [value]="option" (onSelectionChange)="onSelectionChanged($event)" >


//.ts
onSelectionChanged(option) {
   if(option.isUserInput==true){
      console.log('Selected ' + option.name);
   }
}

Just used (click) instead. Take a look:

  <mat-autocomplete #auto="matAutocomplete">
    <mat-option
      (click)="selected(entity)"
      *ngFor="let entity of filteredOptions"
      [value]="entity.short"
    >
      {{ entity.short_header}}
    </mat-option>
  </mat-autocomplete>

Edit: This won't be triggered if you use the keyboard to select the option. So, alternatively as you already use [displayWith], the value will be the object. and you can use the (optionSelected) output on the mat-autocomplete element:

<mat-autocomplete (optionSelected)="optionSelected($event)" 
#auto="matAutocomplete" [displayWith]="displayEntity">
    <mat-option *ngFor="let e of filteredEntities$ | async" [value]="e">
        {{ e.short_header }}
    </mat-option>
</mat-autocomplete>

You can fix it using as follows:

  1. Material way:

<mat-select [(ngModel)]="selectedLanguage" (ngModelChange)="onLangChange($event)">
                                            <mat-option *ngFor="let language of languages" [value]="language">
                                                {{language}}
                                            </mat-option>
                                        </mat-select>    
  1. Traditional way:

   <select [(ngModel)]="status" (ngModelChange)="onChangeStatus($event)"
                                                [ngModelOptions]="{standalone: true}"
                                                class="form-control custom-select" id="status">
                                                <option i18n value="All" selected>Statut</option>
                                                <option i18n value="Yes">Visible</option>
                                                <option i18n value="No">Non Visible</option>
                                            </select>    

In your component ts file:

onChangeStatus(event) {
    if (event === "All") {
      this.status = ...
    }
    else
     ...
}

HTML WITH MATERIAL TEMPLATE: This is selection it uses onSelectionChange, but it is called twice in the select, so passing the $event object to the change() method, then allows the isUserInput attribute to determine if the event has fired already, and perform the statements within it only once. This select works well within a mat-table.

<ng-container matColumnDef="version">
  <th mat-header-cell *matHeaderCellDef>Version</th>
  <td mat-cell *matCellDef="let row; let i = index">
    <mat-form-field appearance="fill">
      <mat-select [(value)]="selected[i].name">
        <mat-option *ngFor="let item of mySelectArray;" 
        [value]="item.name" (onSelectionChange)="change(i, $event, item);">
          {{item.id}}&nbsp;{{item.name}}
        </mat-option>
      </mat-select>
    </mat-form-field>
  </td>
</ng-container>

IN THE TYPESCRIPT: This change method, has the event.isUserInput which determines whether the event has already fired, thus prohibiting it from executing that change again. I hope this helps, SNIFF.

change(i: number, event, item: {id: number, name: string, value: string, type: string}) {
    if (event.isUserInput) {
      this.myTableDtoArray[i] = item;
      this.dataSource = new MatTableDataSource<myTableDto>(this.myTableDtoArray);
      this.dataSource._updateChangeSubscription;
      console.log(this.myTableDtoArray[i], item);
    }
  }

You can also use (change) event

<mat-autocomplete #autocomplete="matAutocomplete" [displayWith]="displayFn" autoActiveFirstOption>
  <mat-option *ngFor="let option of filteredOptions$ | async" [value]="option" (change)="onSelectionChanged(option)" >
    {{displayFn(option)}}
  </mat-option>
</mat-autocomplete>  
changeDegree(event) {
    if (event.isUserInput) { 
      console.log(event.source.value);

      this._degreeArray.length = 0;
      this.changeDegreeDetails(event.source.value.id);
    }
  }
Related