Andrey Tsarev
Andrey Tsarev

Reputation: 779

Stop Angular Material Autocomplete from updating input

I am creating a country autocomplete input. I would like to abstract the input into a component using the ControlValueAccessor interface. I want to use this component to take a FormControl<string | null> where string is a ISO_3166-2 string and null means no selection.

Interaction with parent

There are three main parts in the component:

Expected state

Currently, when a user selects an autocomplete item it sets the input's FormControl<string> to the object selected (e.g. {code: 'US', name: 'United States'}), which obviously is not FormControl<string> type. Is there a way to handle the autocomplete selection and the input value completely separately?

Here is an example: https://stackblitz.com/edit/angular-ivy-ju6qw9?file=src/app/country-input/country-input.component.ts

In this example, I have used a FormControl<Country> instead of a FormControl<string> for the input value, but I am still not getting the desired result because the FormControl is string on runtime after user inputs free text. The filtering functionality is also omitted for simplicity.

Upvotes: 3

Views: 2045

Answers (4)

diet coke
diet coke

Reputation: 1

I know this is old but this is how I got around it. The default behavior - in html, assign optionSelected method to a func:

<mat-autocomplete
   ...
   (optionSelected)="selected($event)">

and in the component's .ts you reset the input control for the form the matInput belongs to (assuming you're using one):

selected(event: MatAutocompleteSelectedEvent): void {
   //this is the important bit vvvv
   this.myFormControl.reset();**
        ...
   //custom logic to handle selected value using event.option.value
        ...
}

Resetting the form for me instantly got rid of the value. However, if you're using multiple fields, I assume reset gets rid of those fields too, unfortunately.

edit: my whole html code is

<form id="inputForm">
    <mat-form-field style="width: 100%">
        <mat-label>Add a site...</mat-label>
        <input
          matInput
          [formControl]="myFormControl"
          #input
          [(ngModel)]="siteInputValue"
          (keyup.enter)="add()"
          [matAutocomplete]="auto" />

        <mat-autocomplete
           (opened)="setFilterList()"
           #auto="matAutocomplete"
           (optionSelected)="selected($event)">

           <mat-option
             [value]="site.id"
             *ngFor="let site of siteInputFilterList | async"
           >{{ site.name }}</mat-option>
        </mat-autocomplete>
    </mat-form-field>
</form>

Upvotes: 0

Hossein Tabei
Hossein Tabei

Reputation: 21

When the input is focused you can change the value before being displayed.
This is not a very good solution, but I hope it helps.


Template:

<mat-form-field>
  <input matInput
  [matAutocomplete]="auto"
  [formControl]="fcInput"
  (focus)="onInputFocus()">

  <mat-autocomplete #auto>
     <mat-option *ngFor="let country of countries" [value]="country">
        {{country.name}}
     </mat-option>
 </mat-autocomplete>


Component:

countries: Country[] = ...;
fcInput = new FormControl<String|null>(null);

onInputFocus(): void {
   if (this.fcInput.value instanceof Object) {
      let country: Country = <Country>{};
      Object.assign(country, this.fcInput.value);
      this.fcInput.setValue(country.name);
   }
}

Upvotes: 0

mumenthalers
mumenthalers

Reputation: 61

You can implement your own AutocompleteInputDirective for the input field of the mat-autocomplete where you provide the NG_VALUE_ACCESSOR and the MatFormFieldControl. You can then listen to the input event on the HostElement (the HTMLInput element) but also subscribe to the optionSelected observable of the matAutocomplete (which you can require by using a selector like input[myAutocompleteInput][matAutocomplete]).

Now you are able to differ between user-entered search query values (which can be emitted to a dedicated event emitter) and actually selected option values, which can be emitted to the FormControl (_onChangeFn from ControlValueAccessor). It doesn't matter whether you are using objects as values or specific string values (enums, isoCodes) - only the "actual" values or null will be set to the FormControl. Additionally the MatFormField/MatLabel will correctly show the errorState of the source FormControl and also add the * to the label when required.

Usage like this:

<mat-form-field>
  <mat-label>My Autocomplete</mat-label>

  <input
    type="text"
    myAutocompleteInput
    [formControl]="myFormCtrl"
    (query)="filter($event)"
    [matAutocomplete]="autocomplete"
  />

  <mat-autocomplete #autocomplete="matAutocomplete">...</mat-autocomplete>

</mat-form-field>

the directive ...

  • provides the EventEmitter query for the user search query
  • sets the value on the myFormCtrl when an option was selected
  • sets null on the myFormCtrl when the user changes the input field value

see the stackblitz - might be helpful

The MatChipGrid is essentially doing the same to prevent emitting plain user queries. I find it surprising that Angular Material still doesn't offer a such a solution for the MatAutocomplete 🤷‍♂️.

Upvotes: 2

skouch2022
skouch2022

Reputation: 1161

Is there a way to handle the autocomplete selection and the input value completely separately?

You can use optionSelected of the mat-autocomplete. This is an event emitter that will emit only when the option is selected. This allows you to manage when to update the selected's value.

Example.component.html

<mat-form-field>
  <input matInput [formControl]="selected" [matAutocomplete]="auto" value=""/>

  <mat-autocomplete 
    #auto="matAutocomplete" 
    [displayWith]="displayFn" 
    (optionSelected)="onOptionSelected($event)">

      <mat-option *ngFor="let country of countries" [value]="country">
        {{country.name}}
      </mat-option>
  </mat-autocomplete>

</mat-form-field>

Example.component.ts

@Component({
 ...
})
export class ExampleComponent {
  
  ...

  public onOptionSelected(event: MatAutocompleteSelectedEvent) {

    console.log(event.option.value);
    // do something with the value here.
  }
}

Upvotes: 2

Related Questions