DEV Community

Michael Musatov
Michael Musatov

Posted on • Edited on

Angular Forms Validation: Part II - FormGroup Validation

I've recently embarked on a journey to document my experiences with Angular development, with a special emphasis on validation. The first part of this series can be found here. Today, I'll be expanding on this topic, focusing specifically on FormGroup validation and the validation of dependent controls.

Example: Password Confirmation Form

As is customary, let's begin with the code. Below, you will find the declaration of the password confirmation form model. We've applied some Validators provided by Angular to the fields.



this. Form = new FormGroup({
  'email': new FormControl(null, [Validators.required, Validators.email]),
  'password': new FormControl(null, [Validators.required]),
  'confirmation': new FormControl(null, [Validators.required])
});


Enter fullscreen mode Exit fullscreen mode

Html markup with Angular bindings



<div class="form" [formGroup]="form">
  <label for="email">Email</label>
  <input name="email" type="text" formControlName="email">
  <div class="errors">
    <span *ngIf="form.get('email').hasError('required')">Email is Required</span> 
    <span *ngIf="form.get('email').hasError('email')">Email is mailformed</span> 
  </div>
  <label for="password">Password</label>
  <input name="password" type="password" formControlName="password">
  <div class="errors">
    <span *ngIf="form.get('password').hasError('required')">Password is Required</span>
  </div>
  <label for="confirmation">Password Confirmation</label>
  <input name="confirmation" type="password" formControlName="confirmation">
  <div class="errors">
    <span *ngIf="form.get('confirmation').hasError('required')">Password confirmation is Required</span> 
  </div>
  <button [disabled]="!form.valid" type="submit">SUBMIT</button>
</div>


Enter fullscreen mode Exit fullscreen mode

The code should yield results similar to those demonstrated in the GIF below:
Sample form with basic validation

Everything is functioning quite well, thanks to Angular! However, we're still missing some crucial features. For example, we aren't checking if the password and confirmation match. Let's address this.

Implementing FormGroup Validator

We have the flexibility to apply Validators not only to FormControl but also to all descendants of AbstractFormControl, including FormGroup or FormArray. To implement a check that verifies a match between the password and its confirmation, we'll need a custom validator for the FormGroup. Below, you'll see the implementation of this validator.



function passwordConfirmationMissmatch(control: FormGroup): ValidationErrors | null {
  const password = control.get('password');
  const confirmation = control.get('confirmation');
  if (!password || !confirmation || password.value === confirmation.value) {
    return null;
  }

  return { 'password-confirmation-mismatch': true };
}


Enter fullscreen mode Exit fullscreen mode

NOTE: The validator's implementation is specifically designed with the form group structure in mind, so it is not suitable for general use, unfortunately.

With our validator ready, it's time to incorporate it into our form.



this. Form = new FormGroup({
    ...
}, [passwordConfirmationMissmatch]);


Enter fullscreen mode Exit fullscreen mode

And the validation results should be incorporated into the markup.



<div class="form" [formGroup]="form">
  ...
  <div class="form-errors-summary"> 
    <div *ngIf="form.hasError('password-confirmation-mismatch')">Password confirmation does not match the password</div>
  </div>
  <button [disabled]="!form.valid" type="submit">SUBMIT</button>
</div>


Enter fullscreen mode Exit fullscreen mode

The result of our enhancements can be seen here.
Form validation with password match check
This improvement is quite significant and sufficient for many cases. However, sometimes we need to validate our form against server-side data, and this is where asynchronous validators come into play.

Implementing a FormGroup Async Validator

In the case of our password setting form, it would be beneficial to check if the provided password has already been used with the given email. Let's proceed with implementing an asynchronous validator for this scenario and applying it to the FormGroup.



function passwordMustBeDifferentFromThePrevious(control: FormGroup): Observable<ValidationErrors | null> {
  const email = control.get('email');
  const password = control.get('password');
  const confirmation = control.get('confirmation');
  if (!email || !password || !confirmation) {
    return null;
  }

  return password.value === 'password' 
    // 'delay' is used to simulate server call
    ? of({'password-previously-used': true}).pipe(delay(2000))
    : of(null).pipe(delay(2000));
}


Enter fullscreen mode Exit fullscreen mode

Now, let's apply the async validator to the FormGroup.



this. Form = new FormGroup({
    ...
}, [passwordConfirmationMissmatch], [passwordMustBeDifferentFromThePrevious]);


Enter fullscreen mode Exit fullscreen mode

Additionally, let's utilize the validation result within the markup. It would also be helpful to visually indicate that the async validation is in progress by displaying the FormGroup's status as 'PENDING'.



<div class="form" [formGroup]="form">
  <div *ngIf="(form.statusChanges | async) === 'PENDING'" class="progress">VALIDATION IN PROGRESS</div>
  ...
  <div class="form-errors-summary"> 
    <div *ngIf="form.hasError('password-confirmation-mismatch')">Password confirmation does not match the password</div>
    <div *ngIf="form.hasError('password-previously-used')">Password was used already. Please select a different password.</div>
  </div>
  <button [disabled]="!form.valid" type="submit">SUBMIT</button>
</div>


Enter fullscreen mode Exit fullscreen mode

Let's take a look at the user interface after implementing these changes.
Form validation with password used check
As you can see, this covers most of what can be achieved with simple form validation. However, there are instances where we may need to highlight the fields that require changes to address FormGroup validation errors.

Highlighting FormGroup Validation Errors on the Controls

When using FormGroup validation, the error state is applied to the FormGroup itself, rather than the individual FormControls within it. As a result, all the FormControls are considered valid (which makes sense). However, what if we want to highlight the specific controls that require changes to address the validation errors? In our case, we want to highlight the password and password confirmation fields. To achieve this, let's enhance our validator and markup accordingly.



function passwordMustBeDifferentFromThePrevious(control: FormGroup): Observable<ValidationErrors | null> {
  const email = control.get('email');
  const password = control.get('password');
  const confirmation = control.get('confirmation');
  if (!email || !password || !confirmation) {
    return null;
  }

  const result$ = password.value === 'password' 
    // 'delay' is used to simulate server call
    ? of({'password-previously-used': true}).pipe(delay(2000))
    : of(null).pipe(delay(2000));

  return result$.pipe(
    tap(result => {
        if (result) {
          password.setErrors({...password.errors, ...result});
          confirmation.setErrors({...password.errors, ...result});
        } else if (password.errors) {
          const passwordErrors = { ...password.errors };
          delete passwordErrors['password-previously-used'];
          const confirmationErrors = { ...confirmation.errors };
          delete confirmationErrors['password-previously-used'];
        }
    })
  );  
}


Enter fullscreen mode Exit fullscreen mode

NOTE: The code within the validator is responsible for setting the error state on specific controls ('password' and 'confirmation') while taking into account any other errors that may have already been set.

There are no changes required in the form declaration, but we do need to handle the 'password-previously-used' error for the 'password' and 'confirmation' controls within the markup.



<div class="form" [formGroup]="form">
  <label for="email">Email</label>
  <input name="email" type="text" formControlName="email">
  <div class="errors">
    <span *ngIf="form.get('email').hasError('required')">Email is Required</span> 
    <span *ngIf="form.get('email').hasError('email')">Email is mailformed</span> 
  </div>
  <label for="password">Password</label>
  <input name="password" type="password" formControlName="password">
  <div class="errors">
    <span *ngIf="form.get('password').hasError('required')">Password is Required</span>
    <span *ngIf="form.get('password').hasError('password-previously-used')">Password was previously used</span>
  </div>
  <label for="confirmation">Password Confirmation</label>
  <input name="confirmation" type="password" formControlName="confirmation">
  <div class="errors">
    <span *ngIf="form.get('confirmation').hasError('required')">Password confirmation is Required</span> 
    <span *ngIf="form.get('confirmation').hasError('password-previously-used')">Password was previously used</span>
  </div>
  <div class="form-errors-summary"> 
    <div *ngIf="form.hasError('password-confirmation-mismatch')">Password confirmation does not match the password</div>
    <div *ngIf="form.hasError('password-previously-used')">Password was used already. Please select a different password.</div>
  </div>
  <button [disabled]="!form.valid" type="submit">SUBMIT</button>
  <div *ngIf="(form.statusChanges | async) === 'PENDING'" class="progress">VALIDATION IS IN PROGRESS</div>
</div>


Enter fullscreen mode Exit fullscreen mode

Let's take a look at how our complete validation works within the user interface.
FormGroup complete validation

Conclusion

Thank you for reading! I hope you found this information valuable and enjoyable. If you're interested, you can find all the code samples on Github.

There are two other articles available on the topic:
Angular forms validation. Part I. Single control validation.
Angular forms validation. Part III. Async Validators gotchas.

Top comments (0)