Cross-field validation is a important aspect of form validation in Angular applications. It involves checking the relationship between multiple fields to ensure data consistency and accuracy. By implementing strong cross-field validation, we can prevent invalid data from being submitted thus improving the overall user experience.
Implementing Cross-Field Validation in Angular
Lets see how we can add dynamic validation on postal code length based on selected country in customer details page.
Create custom validator
Validator takes country field name, postal code field name and data which is holding postal code length of countries.
function validatePostCodeLengh(firstControl: string, matcherControl: string, postalCodeLength: PostCodeLength[]): ValidatorFn {
return (control: AbstractControl): ValidationErrors | null => {
const sourceValue = control.get(firstControl)?.value;
const targetValue = control.get(matcherControl)?.value;
if (sourceValue && targetValue) {
const matchedRecord = postalCodeLength.find(el => el.key === sourceValue);
if (matchedRecord?.value !== targetValue.length) {
control.get(matcherControl)?.setErrors({ invalidLength: "Postal code length must be " + matchedRecord?.value });
}
}
return null
}
}
Create component using reactive form
Use the FormBuilder service to create a reactive form. Apply custom validator to the form.
@Component({
selector: 'app-address',
templateUrl: './address.component.html',
styleUrl: './address.component.scss'
})
export class AddressComponent implements OnInit {
private formBuilder = inject(FormBuilder);
profileForm!: FormGroup;
countries!: Country[];
postalCodeLenght!: PostCodeLength[];
ngOnInit(): void {
//You get this from API
this.countries = JSON.parse('[{"name":"United States","code":"US"},{"name":"India","code":"IN"}]');
//You get this from API
this.postalCodeLenght = JSON.parse('[{"value":5,"key":"US"},{"value":6,"key":"IN"}]');
this.profileForm = this.formBuilder.group({
firstName: ['', Validators.required],
lastName: ['', Validators.required],
address: this.formBuilder.group({
country: ['', Validators.required],
postalCode: ['', [Validators.required]],
}, { validators: validatePostCodeLengh('country', 'postalCode', this.postalCodeLenght) }),
});
}
profileFormSubmit() {
if (this.profileForm.valid) {
alert('Data validation is successful');
} else {
console.log(this.profileForm.get('address.postalCode')?.getError('invalidLength'));
alert('Please fill all required details');
}
}
}
Create angular template
Angular Material is used for styling purpose.
<mat-card appearance="outlined">
<mat-card-content>
<form [formGroup]="profileForm" class="profile-form" (submit)="profileFormSubmit()">
<mat-form-field class="profile-full-width">
<mat-label>First Name</mat-label>
<input matInput placeholder="First Name" formControlName="firstName">
@if (profileForm.get('firstName')?.hasError('required')) {
<mat-error>Required</mat-error>
}
</mat-form-field>
<mat-form-field class="profile-full-width">
<mat-label>Last Name</mat-label>
<input matInput placeholder="Last Name" formControlName="lastName">
@if (profileForm.get('lastName')?.hasError('required')) {
<mat-error>Required</mat-error>
}
</mat-form-field>
<div formGroupName="address">
<mat-form-field class="profile-full-width">
<mat-label>Country</mat-label>
<mat-select formControlName="country">
@for (country of countries; track country) {
<mat-option [value]="country.code">{{country.name}}</mat-option>
}
</mat-select>
@if (profileForm.get('address.country')?.hasError('required')) {
<mat-error>Required</mat-error>
}
</mat-form-field>
<mat-form-field class="profile-full-width">
<mat-label>Postal Code</mat-label>
<input matInput placeholder="Postal Code" formControlName="postalCode">
@if (profileForm.get('address.postalCode')?.hasError('required')) {
<mat-error>Required</mat-error>
}
@if (profileForm.get('address.postalCode')?.hasError('invalidLength')) {
<mat-error>{{profileForm.get('address.postalCode')?.getError('invalidLength')}}</mat-error>
}
</mat-form-field>
</div>
<button mat-raised-button style="float: right; ">Submit</button>
</form>
</mat-card-content>
</mat-card>
Lets see how it works
Additional Considerations
- Use debounceTime and distinctUntilChanged operators to optimize real-time validation and prevent excessive API calls.
- Use asynchronous validators for complex checks that require external data or API calls.
- Provide clear and concise error messages to guide the user in correcting invalid input.
You can find code at GitHub
Please drop a comment if you have any question.