Validate a form field asynchronously (after HTTP request) in Angular 2

The idea is to let the user POST the form. And trigger the error returned by the API for set the email field as error if the user is already register.

I use reactive forms with FormBuilder and I trying calling the validator in the subscribe error catcher :

Constructor :

this.email = fb.control('', [Validators.required, Validators.pattern(/^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/), SignupComponent.alreadyExist()]);
this.username = fb.control('', [Validators.required, Validators.pattern(/^([A-ZÀ-ÿa-z0-9]{2,})+(?:[ _-][A-ZÀ-ÿa-z0-9]+)*$/)]);

this.userRegisterForm = fb.group({
    email: this.email,
    username: this.username
});

Custom alreadyExist() validator :

static alreadyExist(alreadyExist: boolean = false): any {

    return (control: FormControl): { [s: string]: boolean } => {

        if(alreadyExist) {
            return { emailAlreadyExist: true }
        }
    }
}

onSubmit() :

this.user.create(newUser)
    .subscribe(
        data => {
            this.router.navigate(['signin']);
        },
        error => {
            if(error.status === 401) {

                // CALL THE VALIDATOR HERE TO SET : FALSE
                SignupComponent.alreadyExist(true);
            }

            this.loading = false;
        });

It appear that the validator is called but the returned anonymous method inside it is never called... Maybe isn't a good practice, anyone can highlight me ? thx

Upvotes: 2

Views: 5515

Answers (2)

AngularChef
AngularChef

Reputation: 14087

Yo Kévin. :) If I followed, your current workflow is:

  1. Let the user submit the form (to create a new account).
  2. Manually attach an error to the form if creating the new account failed.

NOT GOOD. Now you have validation in two places: BEFORE and AFTER form submission.

You should use a custom validator (like you did in your initial code). But since checking whether a username or email is taken will likely trigger an HTTP call, you must use an asynchronous validator.

In the form declaration, async validators appear AFTER synchronous validators:

this.userForm = fb.group({
  // `userExists` is in 3rd position, it's an async validator
  username: ['', Validators.required, userExists],
  password: []
});

Now the code for userExists. Since it is an async validator, it must return an observable or a promise:

// This is just a standard function
// but you could also put that code in a class method.
function userExists(control: AbstractControl): Observable<{[key: string]: any}> {
  // The userName to test.
  const userName = control.value;

  // NB. In a real project, replace this observable with an HTTP request
  const existingUsernames = ['kevin', 'vince', 'bernard'];
  return Observable.create(observer => {
    // If the username is taken, emit an error
    if (existingUsernames.indexOf(userName) !== -1) {
      observer.next({usernameTaken: true});
    }
    // Username is available, emit null
    else {
      observer.next(null);
    }
    // The observable MUST complete.
    observer.complete();
  });
}

Play with the code in this Plunkr: https://plnkr.co/edit/aPtrp9trtmwUJDJHor6G?p=preview — Try to enter an existing username ('kevin', 'vince', or 'bernard') and you'll see an error message BEFORE you even submit the form.

Why wasn't your code working? I see several errors:

  • You used a synchronous validator instead of an async one.
  • Your validator function is a factory function (= a function that returns another function). It's required only if you need to customize the validator with specific parameters. This is not your case so you should just use a regular function (i.e. in alreadyExist(), only keep the code after the return).
  • Also, the logic of setting errors AFTER submitting the form was not ideal.

Upvotes: 2

Ok I found a good solution.

In my case, for the email FormControl, I don't need a custom validator (even if is possible with setValidators()).

I can delete alreadyExist() and remove its declaration from the list of validators.

Then, I use setErrors() method available on FormControl :

onSubmit() :

this.user.create(newUser)
    .subscribe(
        data => {
            this.router.navigate(['signin']);
        },
        error => {
            if(error.status === 401) {

                // CALL THE VALIDATOR HERE TO SET : FALSE
                this.userRegisterForm.controls['email'].setErrors({
                  "emailAlreadyExist": true
                });
            }

            this.loading = false;
        });

The email FormControl has a new error, so, by this way i can attach a error message for this error.

Upvotes: 7

Related Questions