How to I use angular material autocomplete with complex objects?

Advertisements

I am building an application that uses Angular Material in Angular 16. I have ngrx state management gathering an array of objects to pick from. I want the input to display the name property of this object when one is picked from the autocomplete options. I am currently using a matInput with mat-autocomplete.

import { Component, OnInit } from '@angular/core';
import {FormControl, FormsModule, ReactiveFormsModule} from '@angular/forms';
import {AsyncPipe} from '@angular/common';
import {MatAutocompleteModule} from '@angular/material/autocomplete';
import {MatInputModule} from '@angular/material/input';
import {MatFormFieldModule} from '@angular/material/form-field';
import { TokenNameWithId } from '../../../shared/models/card.model';
import { Observable, combineLatest, debounceTime, map, startWith } from 'rxjs';
import { Store } from '@ngrx/store';
import { selectAllTokenNames } from '../+state/token-names.selectors';

@Component({
  selector: 'tokenator-tokens-dropdown',
  standalone: true,
  imports: [FormsModule,
    MatFormFieldModule,
    MatInputModule,
    MatAutocompleteModule,
    ReactiveFormsModule,
    AsyncPipe
  ],
  providers: [
  ],
  templateUrl: './tokens-dropdown.component.html',
  styleUrl: './tokens-dropdown.component.scss',
})
export class TokensDropdownComponent implements OnInit {
  public tokenNamesControl = new FormControl<string>('', {
    nonNullable: true,
  });
  public filteredTokenNames!: Observable<TokenNameWithId[]>;
  private tokenNames$ = this.store.select(selectAllTokenNames);

  constructor(private store: Store) {
  }

  ngOnInit(): void {
    this.filteredTokenNames = combineLatest([
      this.tokenNamesControl.valueChanges.pipe(startWith('')),
      this.tokenNames$,
    ]).pipe(
      debounceTime(250),
      map(([value, tokenNamesWithId]) => value ? this._filter(value, tokenNamesWithId) : tokenNamesWithId
    ));
  }

  private _filter(value: string, tokenNamesWithId: TokenNameWithId[]): TokenNameWithId[] {
    const filterValue = value.toLowerCase();
    return tokenNamesWithId.filter((tokenNameWithId: TokenNameWithId) => tokenNameWithId.displayName.includes(filterValue));
  }
}
.example-form {
    min-width: 150px;
    max-width: 500px;
    width: 100%;
  }
  
  .example-full-width {
    padding-top: 10px;
    width: 100%;
  }
<form class="example-form">
    <mat-form-field class="example-full-width">
        <mat-label>Token</mat-label>
        <input 
            type="text"
            placeholder="Select a token"
            aria-label="Token name"
            matInput
            [formControl]="tokenNamesControl"
            [matAutocomplete]="auto">
        <mat-autocomplete #auto="matAutocomplete">
            @for (tokenName of filteredTokenNames | async ; track tokenName) {
                <mat-option [value]="tokenName">{{tokenName.displayName}}</mat-option>
            }
        </mat-autocomplete>
    </mat-form-field>
</form>

This results in odd behavior from the matInput where the search in autocomplete works, I can select a object, but the displayed value is [object Object]. This makes sense, since the underlying selected value from the autocomplete is an object, but I would like the input to have a displayed text of tokenName.displayName just like my <mat-options> do.

Is there a way to get the matInput to display a property of an object selected from autocomplete? Alternatively, is there a better pairing of angular material components I should be using when selecting from an array more complex objects that preserves having working autocomplete?

>Solution :

You need to use the [displayWith] property to customize the displayed text for the selected option.

displayFn(tokenName: any): string {
  return tokenName && tokenName.displayName ? tokenName.displayName : '';
}
<mat-autocomplete #auto="matAutocomplete" [displayWith]="displayFn">
    ...
</mat-autocomplete>

Reference: Setting separate control and display values

Leave a ReplyCancel reply