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
When I'm not coding...
Me and Tina are on a motorcycle adventure around Australia.
Come along for the ride!