Cross Field validation with Reactive Forms in Angular

Dayananda - Oct 31 - - Dev Community

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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

}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

Lets see how it works

Image description

Additional Considerations

  1. Use debounceTime and distinctUntilChanged operators to optimize real-time validation and prevent excessive API calls.
  2. Use asynchronous validators for complex checks that require external data or API calls.
  3. 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.

. . . . .