October 21, 2021

Angular - Validate autocomplete against available options

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;
  }
}

References:

1 comment: