Angular Autocomplete Input selected and blur event race condition

Viewed 9230

If you have an Angular Material MatAutoInput, and you listen to the option selection event and also listen to the blur event, you can see that if you select an item in the select list, the input blur event will get called as well.

This is quite problematic, as I need to listen to the blur event in the input field for validation, and also watch the select. If the user clicks on an element, the blur causes to call my valuation logic on the field, which can mess things up.

<form class="example-form">
  <mat-form-field class="example-full-width">
    <input type="text"
           placeholder="Pick one"
           aria-label="Number"
           matInput
           (blur)="onBlur()"
           [formControl]="myControl"
           [matAutocomplete]="auto">
    <mat-autocomplete #auto="matAutocomplete" (optionSelected)="optionSelected()">
      <mat-option *ngFor="let option of options" [value]="option">
        {{option}}
      </mat-option>
    </mat-autocomplete>
  </mat-form-field>
</form>

@Component({
  selector: 'autocomplete-simple-example',
  templateUrl: 'autocomplete-simple-example.html',
  styleUrls: ['autocomplete-simple-example.css'],
})
export class AutocompleteSimpleExample {
  myControl = new FormControl();
  options: string[] = ['One', 'Two', 'Three'];

  optionSelected(){
    console.log("option selected event, value: ", this.myControl.value);

  }

  onBlur(){
    console.log("Blur event, value:",  this.myControl.value);
  }
}

Live demo: https://stackblitz.com/edit/angular-pp3ztn?file=src%2Fapp%2Fautocomplete-simple-example.ts

Is there any way to go around this issue? Or can I be absolute sure about this order of events happening? If for any reason the event would happen in a different order (first the select, then the blur) it can mess things up in my deeper input logic.

My closes solution was to try to disable the blur event checking upon the click on the field, and only enabling the blur detection upon selecting an option OR blur event on the option list itself... which seemed quite hacky. Also, found a same-ish thread: https://github.com/angular/material/issues/3976 which sadly did not give any real solutions.

4 Answers

I have found a not-so-hacky soltuion:

Simply put this check in the (blur) event handling:

  @ViewChild('autocomplete') autocomplete: MatAutocomplete;

  ...
  onBlur(){  if(autocomplete.isOpen){....}   }

This way we can discriminate the blur event handling while the user is clicking on the option list, and ensure we run/do not run validation logic on blur.

In my case, I needed to clear the input on blur and set a separate value when an option was selected. Since the blur always seems to happen before the optionSelected event, any actions placed inside the (blur) listener will stop the optionSelected event from happening at all with no way to discern a click outside of the input box vs selecting an option. The only way I've seen to get around it is to place your (blur) actions inside of a setTimeout with a time > 0.

<input matInput (blur)="onBlur()"...
<mat-autocomplete (optionSelected)="onSelect($event)"...

and in your component:

onBlur() {
    setTimeout(() => {
        //doStuffOnBlur()
    }, 200);
}

onSelect(event: MatAutocompleteSelectedEvent) {
    //doStuffWithSelectedOption()
}

You can use mousedown and event.preventDefault() on the item that should not trigger a blur-event when clicked.

<mat-autocomplete #auto="matAutocomplete" (optionSelected)="optionSelected()">
   <mat-option *ngFor="let option of options" (mousedown)=$event.preventDefault()   
         [value]="option"> 
     {{option}}
   </mat-option>
</mat-autocomplete>

You could also leverage the MatAutocompleteTrigger and its panelClosingActions (docs)

@ViewChild('autocompleteInput', { read: MatAutocompleteTrigger }) triggerAutocompleteInput: MatAutocompleteTrigger;

ngAfterViewInit() {
    this.triggerAutocompleteInput.panelClosingActions
      .pipe(tap(console.log))
      .subscribe()
  }

<mat-form-field>
  <input
    #autocompleteInput
    //...
  >
</mat-form-field>
Related