Published: February 20 2023

Angular - Multiple Field (Cross Field) Validation with Template-Driven Forms

Tutorial built with Angular 15.1.5 and Template-Driven Forms

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

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


Angular Component Template with Cross Field Validation

The app component template contains an example form with two input fields for password and confirmPassword. Both fields are marked as required with the required validator directive, the password field has a minlength directive set to six characters, and the custom [mustMatch] validator directive is attached to the parent form to compare the two password fields to ensure they both have the same value.

The form submit event is bound to the onSubmit() method of the app component with the event binding (ngSubmit)="onSubmit(f)". The template variable #f="ngForm" creates a FormGroup instance to provide access to the form data and validation status in the app component.

Validators and directives in template-driven forms

Validation is implemented by validator functions that are attached to fields or forms using directives in template-driven forms. The required and minlength validators are included in the Angular framework, the mustMatch validator is a custom validator to add support for cross field validation.

Input fields registered with parent form

Each input field is registered with the form using the ngModel directive. In the context of a parent form the directive can be used without data binding ([()]), instead the control is registered using the input name attribute and the form keeps the data in sync.

<div class="card m-3">
    <h5 class="card-header text-center">Angular + Template-Driven Forms - Cross Field Validation Example</h5>
    <div class="card-body">
        <form #f="ngForm" (ngSubmit)="onSubmit(f)" [mustMatch]="['password', 'confirmPassword']" novalidate>
            <div class="row">
                <div class="col mb-3">
                    <label class="form-label">Password</label>
                    <input type="password" name="password" ngModel #password="ngModel" class="form-control" [ngClass]="{ 'is-invalid': f.submitted && password.invalid }" required minlength="6">
                    <div *ngIf="f.submitted && password.errors" class="invalid-feedback">
                        <div *ngIf="password.errors.required">Password is required</div>
                        <div *ngIf="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" name="confirmPassword" ngModel #confirmPassword="ngModel" class="form-control" [ngClass]="{ 'is-invalid': f.submitted && confirmPassword.invalid }" required>
                    <div *ngIf="f.submitted && confirmPassword.errors" class="invalid-feedback">
                        <div *ngIf="confirmPassword.errors.required">Confirm Password is required</div>
                        <div *ngIf="confirmPassword.errors.mustMatch">Passwords must match</div>
                    </div>
                </div>
            </div>
            <div class="text-center">
                <button class="btn btn-primary me-1">Submit</button>
                <button class="btn btn-secondary" type="reset">Reset</button>
            </div>
        </form>
    </div>
</div>


App Component that handles Form Data

The component doesn't do much when using angular template-driven forms since the form fields and validators are configured in the component template.

The onSubmit() method is called with the NgForm template variable when the form is submitted, it is bound to the form element in the template using Angular event binding syntax ((ngSubmit)="onSubmit(f)"). If valid a simple javascript alert is displayed with the values entered into the form.

import { Component } from '@angular/core';
import { NgForm } from '@angular/forms';

@Component({ selector: 'app-root', templateUrl: 'app.component.html' })
export class AppComponent {
    onSubmit(f: NgForm) {
        // stop here if form is invalid
        if (f.invalid) {
            return;
        }

        alert('SUCCESS!! :-)\n\n' + JSON.stringify(f.value, null, 4));
    }
}


Custom MustMatch Cross Field Validator

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

It works slightly differently than a typical validator, usually a validator expects an input control parameter whereas this one expects a form group because it's validating two inputs instead of one.

Also instead of returning an error which would be attached to the parent form group, the validator calls matchingControl.setErrors() to attach the error to the confirm password field. I thought this made more sense because the validation error is displayed below the confirmPassword field in the template.

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;
    }
}


Custom mustMatch Validator Directive

The custom [mustMatch] directive wraps the custom MustMatch validator so we can attach it to the form. A custom validator directive is required when using template-driven forms because we don't have direct access to the FormGroup like in reactive forms.

The directive implements the Validator interface and registers itself with the NG_VALIDATORS provider to let angular know that it's a custom validator directive.

It accepts an array with the names of 2 form controls that must match in order for form validation to pass, e.g. [mustMatch]="['field1', 'field2']" will validate that field1 and field2 contain the same value, otherwise a validation error will be set on field2. You can see it's usage in the form tag of the app template above.

import { Directive, Input } from '@angular/core';
import { NG_VALIDATORS, Validator, ValidationErrors, FormGroup } from '@angular/forms';

import { MustMatch } from './must-match.validator';

@Directive({
    selector: '[mustMatch]',
    providers: [{ provide: NG_VALIDATORS, useExisting: MustMatchDirective, multi: true }]
})
export class MustMatchDirective implements Validator {
    @Input('mustMatch') mustMatch: string[] = [];

    validate(formGroup: FormGroup): ValidationErrors | null {
        return MustMatch(this.mustMatch[0], this.mustMatch[1])(formGroup);
    }
}

 


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