angularangular-materialautocompletemat-form-field

Angular Autocomplete Shows Different Value When Option is Selected Twice


Background
I have an Angular component that renders an Angular Material Autocomplete field. The field has three options, and each option contains a name and an id. When an option is selected, it's name field should be displayed in the field.

Problem
When an option is selected initially, the name field is displayed as expected. However, when the same option is selected again, while it is already selected, the id of the option is displayed in the field. The name of the selected option should be displayed, and not the id.

Question
How can this be fixed so that, regardless of how many times an option is selected, the name field is always displayed, and not the id field?

StackBlitz

TS

import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
  MatAutocompleteModule,
  MatAutocompleteSelectedEvent,
} from '@angular/material/autocomplete';
import { MatInputModule } from '@angular/material/input';

interface Item {
  id: string;
  name: string;
}

@Component({
  selector: 'app-autocomplete',
  standalone: true,
  templateUrl: './autocomplete.component.html',
  styleUrls: ['./autocomplete.component.css'],
  changeDetection: ChangeDetectionStrategy.OnPush,
  imports: [CommonModule, FormsModule, MatAutocompleteModule, MatInputModule],
})
export class AutocompleteComponent {
  options: Item[] = [
    { id: '1', name: 'Item One' },
    { id: '2', name: 'Item Two' },
    { id: '3', name: 'Item Three' },
  ];
  name: string = '';
  id: string = '';
  placeholder: string = 'Select an option';

  resetField() {
    this.name = '';
  }

  selectionChange(e: MatAutocompleteSelectedEvent) {
    this.name = e.option.viewValue;
    console.log(e.option.viewValue); // name
    console.log(e.option.value); // id
  }
}

HTML

<mat-form-field>
  <mat-label>{{ placeholder }}</mat-label>
  <input
    #assetInput
    type="text"
    [placeholder]="placeholder"
    matInput
    [(ngModel)]="name"
    [matAutocomplete]="auto"
  />
  <mat-autocomplete
    #auto="matAutocomplete"
    (optionSelected)="selectionChange($event)"
  >
    <mat-option *ngFor="let option of options" [value]="option.id">
      {{ option.name }}
    </mat-option>
  </mat-autocomplete>
</mat-form-field>

Solution

  • We can use displayWith property binding, where we supply a function that displays the correct value.

    We require .bind(this) so that the function can access the component scope, even if it executed inside the mat autocomplete component.

    displayFn(id: string) {
      if (!id) return '';
      let index = this.options.findIndex((item) => item.id === id);
      return this.options?.[index]?.name || id;
    }
    

    displayWith:

    @Input()
    displayWith: ((value: any) => string) | null
    Function that maps an option's control value to its display value in the trigger.


    HTML:

    <mat-form-field>
      <mat-label>{{ placeholder }}</mat-label>
      <input
        #assetInput
        type="text"
        [placeholder]="placeholder"
        matInput
        [(ngModel)]="name"
        [matAutocomplete]="auto"
      />
      <mat-autocomplete
        #auto="matAutocomplete"
        [displayWith]="displayFn.bind(this)"
        (optionSelected)="selectionChange($event)"
      >
        <mat-option *ngFor="let option of options" [value]="option.id">
          {{ option.name }}
        </mat-option>
      </mat-autocomplete>
    </mat-form-field>
    

    TS:

    import { CommonModule } from '@angular/common';
    import { ChangeDetectionStrategy, Component } from '@angular/core';
    import { FormsModule } from '@angular/forms';
    import {
      MatAutocompleteModule,
      MatAutocompleteSelectedEvent,
    } from '@angular/material/autocomplete';
    import { MatInputModule } from '@angular/material/input';
    
    interface Item {
      id: string;
      name: string;
    }
    
    @Component({
      selector: 'app-autocomplete',
      standalone: true,
      templateUrl: './autocomplete.component.html',
      styleUrls: ['./autocomplete.component.css'],
      changeDetection: ChangeDetectionStrategy.OnPush,
      imports: [CommonModule, FormsModule, MatAutocompleteModule, MatInputModule],
    })
    export class AutocompleteComponent {
      options: Item[] = [
        { id: '1', name: 'Item One' },
        { id: '2', name: 'Item Two' },
        { id: '3', name: 'Item Three' },
      ];
      name: string = '';
      id: string = '';
      placeholder: string = 'Select an option';
    
      resetField() {
        this.name = '';
      }
    
      selectionChange(e: MatAutocompleteSelectedEvent) {
        this.name = e.option.viewValue;
        console.log(e.option.viewValue); // name
        console.log(e.option.value); // id
      }
    
      displayFn(id: string) {
        if (!id) return '';
        let index = this.options.findIndex((item) => item.id === id);
        return this.options?.[index]?.name || id;
      }
    }
    

    Stackblitz Demo