Published: February 20 2023

Angular - Multiple Field (Cross Field) Validation with Reactive Forms

Tutorial built with Angular 15.1.5 and Reactive Forms

This is a quick example of how to implement cross field validation in Angular to compare and validate multiple fields with Reactive Forms. For a more detailed registration form example that includes a bunch of other fields see Angular 14 - Reactive Forms Validation Example.

Here it is in action: (See on StackBlitz at https://stackblitz.com/edit/angular-reactive-forms-cross-field-validation-example)


Angular Cross Field Validation App Component

The app component contains an example form (FormGroup) with two string fields for password and confirmPassword. Both fields are marked as required with the required validator, the password field has a minLength of six characters, and the custom MustMatch validator is used to compare the two fields to ensure they both have the same value.

The onSubmit() method simply displays the form data in a javascript alert when the form is valid.

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

// import custom validator to validate that password and confirm password fields match
import { MustMatch } from './_helpers';

@Component({ selector: 'app-root', templateUrl: 'app.component.html' })
export class AppComponent implements OnInit {
    form!: FormGroup;
    submitted = false;

    constructor(private formBuilder: FormBuilder) { }

    ngOnInit() {
        this.form = this.formBuilder.group({
            password: ['', [Validators.required, Validators.minLength(6)]],
            confirmPassword: ['', Validators.required]
        }, {
            validators: MustMatch('password', 'confirmPassword')
        });
    }

    // convenience getter for easy access to form fields
    get f() { return this.form.controls; }

    onSubmit() {
        this.submitted = true;

        // stop here if form is invalid
        if (this.form.invalid) {
            return;
        }

        // display form values on success
        alert('SUCCESS!! :-)\n\n' + JSON.stringify(this.form.value, null, 4));
    }

    onReset() {
        this.submitted = false;
        this.form.reset();
    }
}


App Component Template

The app component template contains the html markup for displaying the example cross field validation form in the browser. The form element uses the [formGroup] directive to bind to the form FormGroup in the app component above.

A specific validation message is displayed for each validator by checking the errors object for each input field (e.g. *ngIf="f.password.errors.required", *ngIf="f.confirmPassword.errors.mustMatch").

The form binds the form submit event to the onSubmit() handler in the app component using the Angular event binding (ngSubmit)="onSubmit()". Validation messages are displayed only after the user attempts to submit the form for the first time, this is controlled with the submitted property of the app component.

The reset button click event is bound to the onReset() handler in the app component using the Angular event binding (click)="onReset()".

<div class="card m-3">
    <h5 class="card-header">Angular + Reactive Forms - Cross Field Validation Example</h5>
    <div class="card-body">
        <form [formGroup]="form" (ngSubmit)="onSubmit()">
            <div class="row">
                <div class="col mb-3">
                    <label class="form-label">Password</label>
                    <input type="password" formControlName="password" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.password.errors }" />
                    <div *ngIf="submitted && f.password.errors" class="invalid-feedback">
                        <div *ngIf="f.password.errors.required">Password is required</div>
                        <div *ngIf="f.password.errors.minlength">Password must be at least 6 characters</div>
                    </div>
                </div>
                <div class="col mb-3">
                    <label class="form-label">Confirm Password</label>
                    <input type="password" formControlName="confirmPassword" class="form-control" [ngClass]="{ 'is-invalid': submitted && f.confirmPassword.errors }" />
                    <div *ngIf="submitted && f.confirmPassword.errors" class="invalid-feedback">
                        <div *ngIf="f.confirmPassword.errors.required">Confirm Password is required</div>
                        <div *ngIf="f.confirmPassword.errors.mustMatch">Passwords must match</div>
                    </div>
                </div>
            </div>
            <div class="text-center">
                <button class="btn btn-primary me-2">Submit</button>
                <button class="btn btn-secondary" type="reset" (click)="onReset()">Reset</button>
            </div>
        </form>
    </div>
</div>


Custom "Must Match" Cross Field Validator

Angular Reactive Forms contains a bunch of built-in Validators including required and minLength. For anything not supported out of the box you can create a custom validator.

The custom MustMatch validator is used in the example to validate that the password and confirm password fields match. However it could be used to validate any two fields (e.g. email and confirm email).

Error attached to confirmPassword field

The custom validator works a bit different to "normal", it attaches the error to the confirm password field (matchingControl) instead of returning it from the function which would attach it to the form group. I think it makes the app component template (above) a bit cleaner and more intuitive since the mustMatch validation error is displayed below the confirmPassword field, so it keeps the code consistent to check the error with f.confirmPassword.errors.mustMatch.

import { AbstractControl } from '@angular/forms';

// custom validator to check that two fields match
export function MustMatch(controlName: string, matchingControlName: string) {
    return (group: AbstractControl) => {
        const control = group.get(controlName);
        const matchingControl = group.get(matchingControlName);

        if (!control || !matchingControl) {
            return null;
        }

        // return if another validator has already found an error on the matchingControl
        if (matchingControl.errors && !matchingControl.errors.mustMatch) {
            return null;
        }

        // set error on matchingControl if validation fails
        if (control.value !== matchingControl.value) {
            matchingControl.setErrors({ mustMatch: true });
        } else {
            matchingControl.setErrors(null);
        }
        return null;
    }
}

 


Need Some Angular Help?

Search fiverr for freelance Angular developers.


Follow me for updates

On Twitter or RSS.


When I'm not coding...

Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!


Comments


Supported by