Angular Material: How to validate Autocomplete against suggested options?
Asked Answered
M

4

34

We have an autocomplete input with required validation. When a user searches for an option by entering a query (but doesn't pick any of the options) then the input is valid, even though it doesn't match any of the suggested options.

What I want to achieve is not permitting the user to post the form unless one of the suggested options is selected. How do I achieve this?

<mat-form-field>
   <input matInput placeholder="Pick one" aria-label="pick one" [matAutocomplete]="auto" [formControl]="form.controls['SELECTBOX_VALUE']">
      <mat-autocomplete #auto="matAutocomplete">
           <mat-option *ngFor="let option of myOptions | async" [value]="option.name"> <span>{{ option.name }}</span> </mat-option>
            </mat-autocomplete>
</mat-form-field>
<small *ngIf="!form.controls['SELECTBOX_VALUE'].valid && form.controls['SELECTBOX_DEGER'].touched" class="mat-text-warn">Please select.</small>

ngOnInit() {
    this.form = this.fb.group({
        ... some other fields
        SELECTBOX_VALUE: [null, Validators.required]
    });

My filter code for Autocomplate is pretty straight forward:

this.form.get('SELECTBOX_VALUE').valueChanges
        .pipe(
            startWith(''),
            map(option => secenek ? this.doFilter(option) : this.options.slice())
        );

doFilter (name: string) {
    return this.myOptions.filter(option =>
        option.name.toLowerCase().indexOf(name.toLowerCase()) === 0);
}
Mikael answered 16/8, 2018 at 7:25 Comment(1)
Maybe this could be helpful github.com/angular/material2/issues/3334Adalineadall
M
14

For those who may need a similar approach. Here's my solution. I've built a custom validation rule according to my needs.

SELECTBOX_VALUE: [
  null,
  Validators.compose([
    Validators.required,
    FormCustomValidators.valueSelected(this.myArray),
  ]),
];

export class FormCustomValidators {
  static valueSelected(myArray: any[]): ValidatorFn {
    return (c: AbstractControl): { [key: string]: boolean } | null => {
      let selectboxValue = c.value;
      let pickedOrNot = myArray.filter(
        (alias) => alias.name === selectboxValue
      );

      if (pickedOrNot.length > 0) {
        // everything's fine. return no error. therefore it's null.
        return null;
      } else {
        //there's no matching selectboxvalue selected. so return match error.
        return { match: true };
      }
    };
  }
}
Mikael answered 19/8, 2018 at 11:42 Comment(4)
Thanks, I used your method with a slight modification because my array of values were initialized later from an Observable. My version passes in a function instead of an array like: static valueSelected<T>(exists: (value: T) => boolean): ValidatorFn and the condition becomes if(exists(selectedboxValue)) {Reservation
You could actually use some instead of filter, so it return a boolean saying if there is a mathc or not (which is exactly what you are looking for). just replace "filter" by "some", and the if becomes if (pickedOrNot) {Hesperides
@JeanMarois I have the same situation as yours but I haven't understood your proposed solution : the selectbox_value is part of a form which is defined in the constructor of a component. I don't know at that stage the list of options, that is coming later as the result of a service which is returning an observable list...so how can I "wait" to pass the list of options until I have it?Michaelmichaela
@Michaelmichaela I posted this answer https://mcmap.net/q/438675/-angular-material-how-to-validate-autocomplete-against-suggested-options with some example code. It's not tested, so try it and feel free to ask questions in the answer.Reservation
Q
15

There are two scenarios with Angular Material Autocomplete that may need to be validated against a list of selected options:

  1. Array of strings - Suggested options are defined in an array of strings
  2. Array of Objects - Suggested options are an object property defined on an array of Objects (In this scenario, displayWith Input would be used.)

** Stackblitz Demo **

Validate Options from Array of strings

To validate autocomplete against an array of string options, the validator may accept the array of options and check if the control value is included.

function autocompleteStringValidator(validOptions: Array<string>): ValidatorFn {
  return (control: AbstractControl): { [key: string]: any } | null => {
    if (validOptions.indexOf(control.value) !== -1) {
      return null  /* valid option selected */
    }
    return { 'invalidAutocompleteString': { value: control.value } }
  }
}

The validator could be added to a FormControl along with other built-in validators such as Validators.required:

public autocompleteControl = new FormControl('', 
      { validators: [autocompleteStringValidator(this.options), Validators.required] })

Validate Options from Array of Objects

To validate autocomplete against an array of object options, the validator can leverage the fact that the control.value will only be a string if a valid Object option has not been selected.

function autocompleteObjectValidator(): ValidatorFn {
  return (control: AbstractControl): { [key: string]: any } | null => {
    if (typeof control.value === 'string') {
      return { 'invalidAutocompleteObject': { value: control.value } }
    }
    return null  /* valid option selected */
  }
}
Quadrant answered 5/1, 2020 at 17:33 Comment(0)
M
14

For those who may need a similar approach. Here's my solution. I've built a custom validation rule according to my needs.

SELECTBOX_VALUE: [
  null,
  Validators.compose([
    Validators.required,
    FormCustomValidators.valueSelected(this.myArray),
  ]),
];

export class FormCustomValidators {
  static valueSelected(myArray: any[]): ValidatorFn {
    return (c: AbstractControl): { [key: string]: boolean } | null => {
      let selectboxValue = c.value;
      let pickedOrNot = myArray.filter(
        (alias) => alias.name === selectboxValue
      );

      if (pickedOrNot.length > 0) {
        // everything's fine. return no error. therefore it's null.
        return null;
      } else {
        //there's no matching selectboxvalue selected. so return match error.
        return { match: true };
      }
    };
  }
}
Mikael answered 19/8, 2018 at 11:42 Comment(4)
Thanks, I used your method with a slight modification because my array of values were initialized later from an Observable. My version passes in a function instead of an array like: static valueSelected<T>(exists: (value: T) => boolean): ValidatorFn and the condition becomes if(exists(selectedboxValue)) {Reservation
You could actually use some instead of filter, so it return a boolean saying if there is a mathc or not (which is exactly what you are looking for). just replace "filter" by "some", and the if becomes if (pickedOrNot) {Hesperides
@JeanMarois I have the same situation as yours but I haven't understood your proposed solution : the selectbox_value is part of a form which is defined in the constructor of a component. I don't know at that stage the list of options, that is coming later as the result of a service which is returning an observable list...so how can I "wait" to pass the list of options until I have it?Michaelmichaela
@Michaelmichaela I posted this answer https://mcmap.net/q/438675/-angular-material-how-to-validate-autocomplete-against-suggested-options with some example code. It's not tested, so try it and feel free to ask questions in the answer.Reservation
R
0

Here's a solution that validates the selected option against an array obtained from an observable.

export class FormCustomValidators {
  static valueSelected<T>(exists: (value: T) => boolean): ValidatorFn {

    return (c: AbstractControl): { [key: string]: boolean } | null => {
      let selectboxValue = c.value;

      if (exists(selectboxValue)) {
        // everything's fine. return no error. therefore it's null.
        return null;
      } else {
        //there's no matching selectboxvalue selected. so return match error.
        return { 'match': true };
      }
    }
  }
}

export class MyComponent implements OnInit {

  myObjects: MyObject[] = [];

  ngOnInit(): void {
    this.formControl = new FormControl('', Validators.compose([Validators.required, FormCustomValidators.valueSelected<MyObject>(this.myObjectExists.bind(this))]));
    this.service.getMyObjects().subscribe((myObjects) => {
      this.myObjects = myObjects
    }
  }
  
  private myObjectExists(value): boolean {
    return this.myObjects.some(v => v === value);
  }
Reservation answered 19/12, 2022 at 18:19 Comment(0)
H
-4

You can add on your Submit function a validation check for the fields,

Submit() {
 if(this.form.valid) {
   // send to API
 } else {
   // show error
 }
}
Histochemistry answered 16/8, 2018 at 7:31 Comment(3)
I want to achieve this inside html. Do you think there's no way of this?Mikael
you can disable the submit button, [disabled]="!form.valid"Histochemistry
but I as mentioned in the original question, form is always valid because validation mechanism only checks for requirement of whatever string value of selectbox. so I do not think [disabled]="!form.valid" would work.Mikael

© 2022 - 2024 — McMap. All rights reserved.