Validating Confirmation Fields in Angular Reactive Forms with Angular Material

Overview

When creating form-validation in Angular Reactive Forms with Angular Material, one challenging task is validating confirmation fields.  By confirmation fields, I mean, when you need to confirm an email address, password, and/or other input, by making the user type it in twice.  In this case, you need to show a validation error when the main input field and the confirmation field are not equal.  If you use <mat-error> (formerly <md-error>) to show your validation errors, the implementation is non-trivial.

I had to do a lot of research to figure this out, and there wasn’t a single source that gave me the whole answer.  I had to cobble together the information I learned from multiple sources to make my own solution to the problem.  Hopefully the following example will save you from the headache that I experienced.

The Form

Here is an example of a form which uses Angular Material elements for a user registration page.

<form [formGroup]="userRegistrationForm" novalidate>

    <mat-form-field>
        <input matInput placeholder="Full name" type="text" formControlName="fullName">
        <mat-error>
            {{errors.fullName}}
        </mat-error>
    </mat-form-field>

    <div formGroupName="emailGroup">
        <mat-form-field>
            <input matInput placeholder="Email address" type="email" formControlName="email">
            <mat-error>
                {{errors.email}}
            </mat-error>
        </mat-form-field>

        <mat-form-field>    
            <input matInput placeholder="Confirm email address" type="email" formControlName="confirmEmail" [errorStateMatcher]="confirmValidParentMatcher">
            <mat-error>
                {{errors.confirmEmail}}
            </mat-error>
        </mat-form-field>
    </div>

    <div formGroupName="passwordGroup">
        <mat-form-field>
            <input matInput placeholder="Password" type="password" formControlName="password">
            <mat-error>
                {{errors.password}}
            </mat-error>
        </mat-form-field>
        
        <mat-form-field>
            <input matInput placeholder="Confirm password" type="password" formControlName="confirmPassword" [errorStateMatcher]="confirmValidParentMatcher">
            <mat-error>
                {{errors.confirmPassword}}
            </mat-error>
        </mat-form-field>
    </div>

    <button mat-raised-button [disabled]="userRegistrationForm.invalid" (click)="register()">Register</button>
    
</form>

As you can see, I am using <mat-form-field>, <input matInput>, and <mat-error> tags from Angular Material. My first thought was to add the *ngIf directive to control when the <mat-error> sections show up, but this has no effect! The visibility is actually controlled by the validity (and “touched” status) of the <mat-form-field>, and there is no provided validator to test equality to another form field in HTML or Angular. That is where the errorStateMatcher directives on the confirmation fields, lines 19 and 35, come into play.

The errorStateMatcher directive is built in to Angular Material, and provides the ability to use a custom method to determine the validity of a <mat-form-field> form control, and allows access to the validity status of the parent to do so. To begin to understand how we can use errorStateMatcher for this use case, let’s first take a look at the component class.

The Component Class

Here is an Angular Component class that sets up validation for the form using FormBuilder.

export class App {
    userRegistrationForm: FormGroup;

    confirmValidParentMatcher = new ConfirmValidParentMatcher();

    errors = errorMessages;

    constructor(
        private formBuilder: FormBuilder
    ) {
        this.createForm();
    }

    createForm() {
        this.userRegistrationForm = this.formBuilder.group({
            fullName: ['', [
                Validators.required,
                Validators.minLength(1),
                Validators.maxLength(128)
            ]],
            emailGroup: this.formBuilder.group({
                email: ['', [
                    Validators.required,
                    Validators.email
                ]],
                confirmEmail: ['', Validators.required]
            }, { validator: CustomValidators.childrenEqual}),
            passwordGroup: this.formBuilder.group({
                password: ['', [
                    Validators.required,
                    Validators.pattern(regExps.password)
                ]],
                confirmPassword: ['', Validators.required]
            }, { validator: CustomValidators.childrenEqual})
        });
    }

    register(): void {
        // API call to register your user
    }
}

The class sets up a FormBuilder for the user registration form. Notice that there are two FormGroups in the class, one for confirming the email address, and one for confirming the password. The individual fields use appropriate validator functions, but both use a custom validator at the group level (lines 27 and 34), which checks to make sure that the fields in each group are equal to each other, and returns a validation error if they are not.

The combination of the custom validator for the groups and the errorStateMatcher directive is what provides us the complete functionality needed to appropriately show validation errors for the confirmation fields. Let’s take a look at the custom validation module to bring it all together.

Custom Validation Module

I chose to break the custom validation functionality into its own module, so that it can easily be reused. I also chose to put other things related to my form validation in that module, namely, regular expressions and error messages, for the same reason. Thinking ahead a little, it is likely that you will allow a user to change their email address and password in a user update form as well, right? Here is the code for the entire module.

import { FormGroup, FormControl, FormGroupDirective, NgForm, ValidatorFn } from '@angular/forms';
import { ErrorStateMatcher } from '@angular/material';

/**
 * Custom validator functions for reactive form validation
 */
export class CustomValidators {
    /**
     * Validates that child controls in the form group are equal
     */
    static childrenEqual: ValidatorFn = (formGroup: FormGroup) => {
        const [firstControlName, ...otherControlNames] = Object.keys(formGroup.controls || {});
        const isValid = otherControlNames.every(controlName => formGroup.get(controlName).value === formGroup.get(firstControlName).value);
        return isValid ? null : { childrenNotEqual: true };
    }
}

/**
 * Custom ErrorStateMatcher which returns true (error exists) when the parent form group is invalid and the control has been touched
 */
export class ConfirmValidParentMatcher implements ErrorStateMatcher {
    isErrorState(control: FormControl | null, form: FormGroupDirective | NgForm | null): boolean {
        return control.parent.invalid && control.touched;
    }
}

/**
* Collection of reusable RegExps
*/
export const regExps: { [key: string]: RegExp } = {
   password: /^(?=.*[0-9])(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{7,15}$/
};

/**
 * Collection of reusable error messages
 */
export const errorMessages: { [key: string]: string } = {
    fullName: 'Full name must be between 1 and 128 characters',
    email: 'Email must be a valid email address (username@domain)',
    confirmEmail: 'Email addresses must match',
    password: 'Password must be between 7 and 15 characters, and contain at least one number and special character',
    confirmPassword: 'Passwords must match'
};

First let’s take a look at the custom validator function for the group, CustomValidators.childrenEqual(), on line 11. Since I come from an object-oriented programming background, I chose to make this function a static class method, but you could just as easily make it a standalone function. The function must be of type ValidatorFn (or the approprate literal signature), and take a single parameter of type AbstractControl, or any derivative type. I chose to make it FormGroup, since that is the use case it’s for.

The function’s code iterates over all of the controls in the FormGroup, and ensures that their values all equal that of the first control. If they do, it returns null (indicates no errors), otherwise is returns a childrenNotEqual error.

So now we have an invalid status on the group when the fields are not equal, but we still need to use that status to control when to show our error message. Our ErrorStateMatcher, ConfirmValidParentMatcher, is what can do this for us. The errorStateMatcher directive requires that you point to an instance of a class which implements the provided ErrorStateMatcher class in Angular Material. So that is the signature used here. ErrorStateMatcher requires the implementation of an isErrorState method, with the signature shown in the code (line 22). It returns true or false; true indicates that an error exists, which makes the input element’s status invalid.

The single line of code in this method is quite simple; it returns true (error exists) if the parent control (our FormGroup) is invalid, but only if the field has been touched. This aligns with the default behavior of <mat-error>, which we are using for the rest of the fields on the form.

To bring it all together, we now have a FormGroup with a custom validator that returns an error when our fields are not equal, and a <mat-error> which displays when the group is invalid. To see this functionality in action, here is a working plunker with an implementation of the code mentioned.

Author: Tom Bonanno

Tom Bonanno is a Staff Systems Engineer at VMware, specializing in vRealize Automation and vRealize Orchestrator. He currently helps large enterprise customers understand the value of VMware products by demonstrating and proving their technical abilities on a deep level. He often designs and builds creative solutions to customers' problems, and shares them with the world via blog articles and other community sites and tools. In fact, his deep-seated passion is software development, including building custom extensions and integrations for the IT world. Tom attended Fairleigh Dickinson University, Florham Park campus, for mathematics and computer science. His 17 years of experience includes system administration, IT management, software implementation consulting, and technical software pre-sales engineering. On his own time, he invests in himself and his passion by learning new programming languages and developing applications and new functionality for existing software. Tom has also been known to do some Android modding and theming on the side. He continues to strive to keep up on the latest trends in technology, as both a provider and a consumer.

Leave a Reply

Close Bitnami banner
Bitnami