Reputation: 18123
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.
Upvotes: 13
Views: 18596
Reputation: 31
Sorry for my english right away ;) I faced a similar problem and used the following solution to get the desired functionality
HTML template:
<mat-form-field class="mat-input-styles-white-theme-settings disabled-hint">
<mat-label>
<div class="label-content">
<span>TempLabel</span>
</div>
</mat-label>
<input matInput type="name" [formControlName]="'PersonSearch'" [autocomplete]="'off'" [matAutocomplete]="personSearchAutocomplete">
<mat-autocomplete #personSearchAutocomplete
[displayWith]="displayedSearchFn.bind(this)"
(optionSelected)="onSelectSearchValue($event)">
@for (elem of searchUserDataTrigger$ | async; track elem) {
<mat-option [value]="elem.Id">
<span>{{elem.FirstName +
' ' + elem.LastName}}
</span>
</mat-option>
}
</mat-autocomplete>
</mat-form-field>
Next, we create an observable that will interact with the search field:
private searchUserData$!: Observable<ISearchUserResponse | string>
public searchUserDataTrigger$: Subject<Array<ISearchUserItem>> = new Subject<Array<ISearchUserItem>>()
public allowedUsersValues: Array<ISearchUserItem> = []
public fg: FormGroup = new FormGroup({
PersonSearch: new FormControl("", [EqArrValidator(this.allowedUsersValues.map(el => el.Id)), Validators.required])})
ngOnInit(): void {
this.searchUserData$ = this.fg.get("PersonSearch")!.valueChanges
.pipe(
map(el => {
//We find logins by unique user ID
let userId = Number(el)
let value = this.allowedUsersValues.filter(el => el.Id == userId)
if (value.length > 0)
return value
return el
}),
distinctUntilChanged(),
debounceTime(200),
exhaustMap(el => {
let passContent$ = from(["isUserSelectedValue"])
let getterData$ = this._usersService.searchUserByStr(el).pipe(catchError((err) => of()))
return iif(() => typeof (el) == "string", getterData$, passContent$.pipe(map(el => {
let selectedEl = this.allowedUsersValues.filter(el2 => el2.Id == this.selectedSearchUserId)
if (selectedEl.length > 0) {
/**
* We sift out the event generated by mat-option and carry out further processing of the arrays.
*
* ...
* Your code
* ...
*/
return
}
return el
})))
}))
this.searchUserData$.pipe(takeUntil(this.unsubscribe$)).subscribe({
next: res => {
// We find out what type is emitted ("isUserSelectedValue" | object). If necessary, you can add a strict comparison, which you specify in the pipeline .pipe()
if (typeof (res) == "string") {
return
}
this.allowedUsersValues = [...res.data]
this.fg.get("PersonSearch")?.setValidators([
EqArrValidator(this.allowedUsersValues.map(el => el.Id)), Validators.required
])
this.searchUserDataTrigger$.next(this.allowedUsersValues)
this.fg.updateValueAndValidity()
},
complete: () => { }
})
}
In this case, you listen to all values from the input field using valueChanges.
Next, select the value you need from among those sent by the Backend. If the value is found - return an array containing the value selected by the user.
The key logic is the use of the iif() operator. You form two observables:
1 - The one that will interact directly with the entered value and on the basis of this, all further processing will be performed;
2 - The one that will signal that the user has selected a value from the list.
The displayedSearchFn function is responsible for the value that will be displayed to the user (ex. FirstName) The onSelectSearchValue function stores the last element selected by the user (selectedSearchUserId)
It is also possible to refine the pipeline using the filter() operator, but I chose the above path for my needs
This is my first time commenting on stackoverflow, so don't judge harshly ;)
Upvotes: 1
Reputation: 568
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>
Upvotes: 0
Reputation: 480
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>
Upvotes: 10
Reputation: 623
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()
}
Upvotes: 5
Reputation: 18123
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.
Upvotes: 7