Angular Forms Validation: Part II - FormGroup Validation
Michael Musatov
Posted on February 2, 2020
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])
});
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>
The code should yield results similar to those demonstrated in the GIF below:
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 };
}
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]);
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>
The result of our enhancements can be seen here.
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));
}
Now, let's apply the async validator to the FormGroup.
this. Form = new FormGroup({
...
}, [passwordConfirmationMissmatch], [passwordMustBeDifferentFromThePrevious]);
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>
Let's take a look at the user interface after implementing these changes.
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'];
}
})
);
}
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>
Let's take a look at how our complete validation works within the user interface.
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.
Posted on February 2, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.