I used the
angular-ng-autocomplete
for dropdown lists and its being quite useful and works extremely good with filter.
The issue I faced with validation and it does not behave as per the expectation.
When a user searches for an option by entering the keyword text (but doesn't pick any of the available options), then the required
validator failed here. If the input box is empty then the requried
validator is working fine on its way. But when the input box has some text, then the requried
validator will not trigger.
The ng-autocomplete
do not have the chance to raise the selected
event, and so do not properly set the binded control's value. Since the input box has some value, the form will pass the validation, and when submitted(with null value) it will take the undefined
value for the control to the server API.
The html for ng-autocomplete
is:
<div class="ng-autocomplete"> <ng-autocomplete [data]="citiesList" [searchKeyword]="cityName" formControlName="CityId" (selected)="citySelected($event)" (inputChanged)="onChangeCitySearch($event)" [itemTemplate]="itemTemplate" [notFoundTemplate]="notFoundTemplate" > </ng-autocomplete> <ng-template #itemTemplate let-item> <a [innerHTML]="item.Name"></a> </ng-template> <ng-template #notFoundTemplate let-notFound> <div [innerHTML]="notFound"></div> </ng-template> </div>
In .ts
file, I am filling the citiesList
from the API:
onChangeCitySearch(search: string) { //if user has entered at-least 2 characters, then call the api for search if (search && search.trim().length >= 2) { this.commonDataService .getCities(search) .subscribe((res) => { this.citiesList = res.Data; }); } }
I do not want to permit the user to post the form unless one of the suggested options is selected from the list. I fixed the issue by defining a custom validator.
We could have two possible scenarios with ng-autocomplete when validating against a list of options:
- Array of strings - Available options are defined as an array of strings.
- Array of objects - Available options are as (an object property i.e. id, name etc, defined on) an array of Objects.
Bind with Array of strings
To validate autocomplete against an array of string options, we can pass the array of options to the the validator, and check if the control's value is exists in the array.
function autocompleteStringValidator(validOptions: Array<string>): ValidatorFn { return (control: AbstractControl): { [key: string]: boolean } | null => { if (validOptions.indexOf(control.value) !== -1) { // null means we dont have to show any error, a valid option is selected return null; } //return non-null object, which leads to show the error because the value is invalid return { match: false }; } }
This is how we can add the validator to the FormControl along with other built-in validators.
public cityControl = new FormControl('', { validators: [Validators.required, autocompleteStringValidator(this.citiesList)] })
Bind with Array of Objects
We can validate the controls value when its binds to an array of objects by using the same technique as above. But I will use a slightly different version, instead of checking the index of input value in the array, here I am using filter method to find the matching item. If it founds any matching record, then the user has properly selected an option from the given list.
function autocompleteObjectValidator(myArray: any[]): ValidatorFn { return (control: AbstractControl): { [key: string]: boolean } | null => { let selectboxValue = control.value; let matchingItem = myArray.filter((x) => x === selectboxValue); if (matchingItem.length > 0) { // null means we dont have to show any error, a valid option is selected return null; } else { //return non-null object, which leads to show the error because the value is invalid return { match: false }; } }; }
The good thing about this technique is that, you can also check for any particular property of the object in the if condition. Lets suppose, if the object has a property Id
, we can check if the value of Id
is matched on both objects.
let matchingItem = myArray.filter((x) => x.Id === selectboxValue.Id);
Another simpler technique can be applied by checking the type of control.value
. For a valid option being selected from the list of objects, its type will be object
, and in case the user types the text manully, than the type of control.value
will be a simple string
. So we can check, if the type is string
, then it shows the fact that user has not selected any of the available options from objects list.
function autocompleteObjectValidator(): ValidatorFn { return (control: AbstractControl): { [key: string]: boolean } | null => { if (typeof control.value === 'string') { //return non-null object, which leads to show the error because the value is invalid return { match: false }; } // null means we dont have to show any error, a valid option is selected return null; } }