Angular Cross-Field Validation & Validation Messages
1. What is Cross-Field Validation?
Cross-field validation is validation where the rule depends on the values of two or more form controls together, rather than validating one field independently.
A classic example is:
Password: [ ******** ]
Confirm Password: [ ******** ]
↓
Password === Confirm Password?
Other examples include:
StartDatemust be beforeEndDateMinPricemust be less thanMaxPriceNewPasswordmust differ fromOldPasswordFromAmountmust be less thanToAmount
Because the rule involves multiple controls, the validator is usually attached to their parent FormGroup.
2. Full Simple Sample Program
We'll create a registration form with:
- Password
- Confirm Password
- Required validation
- Minimum-length validation
- Cross-field password matching
- Validation messages
app.ts
import { Component } from '@angular/core';
import {
AbstractControl,
FormControl,
FormGroup,
ReactiveFormsModule,
ValidationErrors,
ValidatorFn,
Validators
} from '@angular/forms';
@Component({
selector: 'app-root',
standalone: true,
imports: [ReactiveFormsModule],
templateUrl: './app.html'
})
export class App {
userForm = new FormGroup(
{
password: new FormControl('', [
Validators.required,
Validators.minLength(6)
]),
confirmPassword: new FormControl('', [
Validators.required
])
},
{
validators: [this.passwordMatchValidator()]
}
);
passwordMatchValidator(): ValidatorFn {
return (group: AbstractControl): ValidationErrors | null => {
const password = group.get('password')?.value;
const confirmPassword = group.get('confirmPassword')?.value;
if (password !== confirmPassword) {
return {
passwordMismatch: true
};
}
return null;
};
}
submitForm() {
if (this.userForm.valid) {
console.log('Form Submitted');
console.log(this.userForm.value);
} else {
this.userForm.markAllAsTouched();
}
}
}
app.html
<h2>Registration Form</h2>
<form
[formGroup]="userForm"
(ngSubmit)="submitForm()">
<!-- Password -->
<div>
<label>Password:</label>
<input
type="password"
formControlName="password">
@if (userForm.controls.password.touched) {
@if (userForm.controls.password.hasError('required')) {
<p>Password is required.</p>
}
@if (userForm.controls.password.hasError('minlength')) {
<p>Password must contain at least 6 characters.</p>
}
}
</div>
<br>
<!-- Confirm Password -->
<div>
<label>Confirm Password:</label>
<input
type="password"
formControlName="confirmPassword">
@if (userForm.controls.confirmPassword.touched) {
@if (userForm.controls.confirmPassword.hasError('required')) {
<p>Confirm Password is required.</p>
}
@if (
userForm.hasError('passwordMismatch') &&
!userForm.controls.confirmPassword.hasError('required')
) {
<p>Password and Confirm Password do not match.</p>
}
}
</div>
<br>
<button type="submit">
Submit
</button>
</form>
3. How Cross-Field Validation Works
Normally, a validator is attached to one control.
For example:
password: new FormControl('', [
Validators.required
])
That validator only needs to inspect:
password
But password matching requires:
password
+
confirmPassword
↓
Compare both values
Therefore, we attach the validator to the FormGroup:
new FormGroup(
{
password: ...,
confirmPassword: ...
},
{
validators: [this.passwordMatchValidator()]
}
);
Conceptually:
userForm (FormGroup)
│
├── password
│ └── required
│ └── minlength
│
├── confirmPassword
│ └── required
│
└── passwordMatchValidator
│
├── reads password
├── reads confirmPassword
│
└── compares them
4. Creating the Cross-Field Validator
Our validator is:
passwordMatchValidator(): ValidatorFn {
return (group: AbstractControl): ValidationErrors | null => {
const password = group.get('password')?.value;
const confirmPassword = group.get('confirmPassword')?.value;
if (password !== confirmPassword) {
return {
passwordMismatch: true
};
}
return null;
};
}
First, get the password:
const password = group.get('password')?.value;
Then get the confirmation:
const confirmPassword =
group.get('confirmPassword')?.value;
Then compare:
if (password !== confirmPassword)
If they don't match:
return {
passwordMismatch: true
};
If they match:
return null;
Remember:
ValidationErrors object
↓
INVALID
null
↓
VALID
5. Where Is the Error Stored?
This is an important point.
Because our validator is attached to:
userForm
the error belongs to the FormGroup.
Therefore, check:
userForm.hasError('passwordMismatch')
not:
userForm.controls.confirmPassword.hasError('passwordMismatch')
Conceptually:
userForm
│
├── password
│ └── required/minlength errors
│
├── confirmPassword
│ └── required error
│
└── passwordMismatch
↑
FormGroup-level error
6. Cross-Field Validation Example
Suppose the user enters:
Password: Angular123
Confirm Password: Angular456
The validator compares:
Angular123 !== Angular456
Therefore it returns:
{
passwordMismatch: true
}
The form becomes:
INVALID
If the user changes it to:
Password: Angular123
Confirm Password: Angular123
the validator returns:
null
and the cross-field rule passes.
7. What Other Cross-Field Validations Are Common?
Start Date vs End Date
Start Date: 10-Oct-2026
End Date: 05-Oct-2026
↓
INVALID
Rule:
StartDate <= EndDate
Minimum vs Maximum Price
Minimum Price: 5000
Maximum Price: 3000
↓
INVALID
Rule:
Minimum Price <= Maximum Price
These validators belong naturally at the group level because they need access to multiple controls.
8. How Do You Display Validation Messages?
Angular doesn't automatically decide how your validation errors should appear.
We check the control's validation state and display the appropriate message.
A common pattern is:
@if (control.invalid && control.touched) {
...
}
For example:
@if (
userForm.controls.password.invalid &&
userForm.controls.password.touched
) {
<p>Password is invalid.</p>
}
9. Display Different Messages for Different Errors
Suppose:
password: new FormControl('', [
Validators.required,
Validators.minLength(6)
])
There are two possible validation errors.
We shouldn't show the same message for both.
Instead:
@if (userForm.controls.password.touched) {
@if (userForm.controls.password.hasError('required')) {
<p>Password is required.</p>
}
@if (userForm.controls.password.hasError('minlength')) {
<p>Password must contain at least 6 characters.</p>
}
}
This gives the user a precise explanation.
10. What is hasError()?
hasError() checks whether a particular validation error currently exists.
For example:
userForm.controls.password.hasError('required')
checks:
Does password have a
"required" validation error?
Similarly:
userForm.controls.password.hasError('minlength')
checks the minimum-length error.
And:
userForm.hasError('passwordMismatch')
checks our custom FormGroup-level error.
11. Why Check touched?
Consider:
@if (
userForm.controls.password.hasError('required')
) {
<p>Password is required.</p>
}
As soon as the page loads:
Password = ""
↓
required fails
↓
Error displayed immediately
Usually we don't want that.
Instead:
@if (
userForm.controls.password.touched &&
userForm.controls.password.hasError('required')
) {
<p>Password is required.</p>
}
Now:
Page loads
↓
No error displayed
User enters Password field
↓
User leaves field
↓
touched = true
↓
If invalid
↓
Display error
12. touched vs dirty
Both are frequently used when deciding when to display validation messages.
touched
The user entered the control and then left it.
Focus field
↓
Leave field
↓
touched = true
dirty
The user changed the value.
Original value
↓
User modifies it
↓
dirty = true
A common condition is:
control.invalid &&
(control.touched || control.dirty)
Example:
@if (
userForm.controls.password.invalid &&
(
userForm.controls.password.touched ||
userForm.controls.password.dirty
)
) {
<p>Password is invalid.</p>
}
13. Displaying Messages After Submit
There is another problem.
Suppose the user opens the form and immediately clicks:
Submit
The controls may still be:
touched = false
So the error messages might not display if your template depends on touched.
That's why our submit method uses:
this.userForm.markAllAsTouched();
Example:
submitForm() {
if (this.userForm.valid) {
console.log(this.userForm.value);
} else {
this.userForm.markAllAsTouched();
}
}
Now:
User clicks Submit
↓
Form invalid
↓
markAllAsTouched()
↓
Controls become touched
↓
Validation messages displayed
This is a common and useful pattern.
14. Field-Level vs Cross-Field Validation Messages
It's important to distinguish them.
Field-Level Error
Example:
Validators.required
Check the individual control:
userForm.controls.password.hasError('required')
Cross-Field Error
Example:
passwordMismatch
Check the group:
userForm.hasError('passwordMismatch')
So:
Individual validation
↓
FormControl.errors
Cross-field validation
↓
FormGroup.errors
15. Complete Validation Flow
User enters Password
↓
FormControl validators
↓
required / minlength
↓
User enters Confirm Password
↓
FormControl validator
↓
required
↓
FormGroup validator executes
↓
Compare:
Password vs Confirm Password
↓
┌───────────────┐
│ │
Match No Match
│ │
↓ ↓
null passwordMismatch
│ │
↓ ↓
VALID INVALID
The template then reads those states and displays the appropriate messages.
16. Key Points
- Cross-field validation validates multiple related fields together.
- It is commonly implemented with a custom validator attached to a
FormGroup. - Password/Confirm Password is a classic example.
- Other examples include Start/End Date and Min/Max values.
- A valid custom validator returns
null. - An invalid validator returns a
ValidationErrorsobject. - A group-level validator's error normally exists on the
FormGroup. - Use
hasError()to check specific validation errors. - Use
touchedordirtyto control when validation messages appear. markAllAsTouched()is useful when the user submits an invalid form.- Show specific messages for specific validation failures.
17. Interview Questions and Answers
1. What is cross-field validation?
Cross-field validation is validation where the rule depends on multiple form controls.
For example:
Password === ConfirmPassword
It is commonly implemented using a custom validator attached to a FormGroup.
2. Why attach a cross-field validator to FormGroup?
Because the validator needs access to multiple controls within the group.
const password = group.get('password')?.value;
const confirmPassword =
group.get('confirmPassword')?.value;
3. Give examples of cross-field validation.
Common examples include:
- Password and Confirm Password match
- Start Date is before End Date
- Minimum value doesn't exceed Maximum value
- New Password differs from Old Password
4. What should a custom validator return?
Valid
↓
null
Invalid
↓
ValidationErrors object
Example:
return {
passwordMismatch: true
};
5. How do you display validation messages?
Check the control's state and specific validation error.
@if (
userForm.controls.password.touched &&
userForm.controls.password.hasError('required')
) {
<p>Password is required.</p>
}
6. What is hasError()?
hasError() checks whether a control or group currently contains a specific validation error.
control.hasError('required')
7. Where is a cross-field validation error stored?
If the validator is attached to the FormGroup, its error is normally stored on that FormGroup.
userForm.hasError('passwordMismatch')
8. Why use touched while displaying validation messages?
It prevents errors from being displayed before the user has interacted with the field.
9. What does markAllAsTouched() do?
It marks all descendant controls as touched.
this.userForm.markAllAsTouched();
It is commonly used after an invalid form submission so the validation messages become visible.
Interview Summary
Cross-field validation validates related controls together, such as Password and Confirm Password, and is typically implemented with a custom validator attached to a
FormGroup. Validation messages are displayed by checking control/group state such astouchedordirtytogether with specific errors usinghasError().