Search code examples
angulartypescriptangular6angular2-observables

How to implement custom validator on a form, where the validation logic requires data from an observable


I am trying to implement a custom validator on a form, which will check the form input and compare it to an array of values, to ensure the input value is unique. The array for comparison comes from an Observable. However the array is appearing as undefined when I try to access it from the validation method, I'm assuming because I'm doing it in the wrong order in relation to the subscribe, however I cannot work out how to do this correctly.

Below is the stripped down code which should show what I'm trying to do. I've tried it with both of the validator functions. I have also tried moving the formgroup definition into the ngOnInit, and into the actual subscribe function, straight after allUserNames is populated.

export class userComponent implements OnInit {

  user: user = new user;
  allUserNames: string[];

  generalForm = new FormGroup({
    userName: new FormControl(
      this.user.name, [
        Validators.required,
        Validators.maxLength(20),
        this.UniqueNameValidator
        //,this.UniqueNameValidator1(this.alluserNames)
      ])
  })

  constructor(private userService: userService) { }

  ngOnInit() {
    this.subscribeUsers();
  }

  subscribeUsers(): void {
    this.getUsers().subscribe((users) => {
      this.allUserNames = Object.keys(users).map(itm => users[itm].name);
    })
  }

  getUsers(): Observable<user[]> {
    return this.userService.getUsers();
  }

  UniqueNameValidator(allUsers: String[]): ValidatorFn {
    return (control: AbstractControl): { [key: string]: boolean } | null => {
      allUsers.forEach((userName) => {
        if (userName === control.value) {
          return { 'notUnique': true };
        }
      });
      return null;
    }
  }

  UniqueNameValidator1(control: AbstractControl): { [key: string]: boolean } | null {
    this.allUserNames.forEach((userName) => {
      if (userName === control.value) {
        return { 'notUnique': true };
      }
    });
    return null;
  }
}

I expect the validate function to compare the input string and return not unique if it gets a match from allUserNames. However I keep getting the following error:

ERROR Error: Uncaught (in promise): TypeError: Cannot read property 'forEach' of undefined  

I would really appreciate it if someone could point me in the right direction!

Thanks.


Solution

  • You have a scope issue with this. If you were to console log this, you would see that it doesn't point to the component, but your function.

    This would be a proper place for an async validator as you are doing a http call. Also I would suggest to use FormBuilder when building form, so...

    generalForm: FormGroup;
    
    constructor(private userService: userService, private fb: FormBuilder) {
      this.generalForm = this.fb.group({
        userName: [this.user.name, 
                  [Validators.required, Validators.maxLength(20)], 
                  [this.UniqueNameValidator.bind(this)]]
      })
    }
    

    Async validators are place as the third argument. See we are also using bind(this) to get the correct scope of this.

    Then the validator would look something like this:

    import { of } from 'rxjs';
    import { switchMap } from 'rxjs/operators'
    
    UniqueNameValidator(ctrl: AbstractControl) {
      return this.userService.getUsers().pipe(
          switchMap((users: user[]) => {
            // do stuff, and either return error or "of(null)"
          })
      );