Angular Cross Field Validation

Tomasz Kula
3 min readApr 23, 2018

--

What is cross field validation?

It is validating one form control based on the value of another ๐Ÿ‘

Imagine you want to create a simple range component. You start by building the FormGroup in your component class, and you follow it up with proper html bindings.

@Component({
selector: 'range',
template: `
<form [formGroup]="form">
<input formControlName="rangeStart" placeholder="Range start" type="number">
<input formControlName="rangeEnd" placeholder="Range end" type="number">
</form>
<div> Valid: {{ form.valid ? '๐Ÿ‘' : '๐Ÿ‘Ž' }} </div>`
})
export class AppComponent {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
rangeStart: [null, Validators.required],
rangeEnd: [null, Validators.required]
});
}
}

Great! We have a working form. Letโ€™s input some numbers and see if it works.

As we quickly notice, our FormGroup is valid, even if the rangeStart control has a value higher than the rangeEnd control. Dear Lord. What kind of range is this. ๐Ÿ˜ฑ

The solution is simple. We should just add a validator to the rangeStart control, which checks the value of the rangeEnd control. Next we should add another validator to the rangeEnd control, which checks the value of the rangeStart control. In each validator, we compare the values to validate the controls. Right?

Wrong.

The simplest solution is to move the validation to the ancestor control. In this case it is our FormGroup control.

constructor(private fb: FormBuilder) { 
this.form = this.fb.group({
rangeStart: [null, Validators.required],
rangeEnd: [null, Validators.required]
}, { validator: MyAwesomeRangeValidator});
}

All that is left to do is to implement the MyAwersomeRangeValidator. It is really straightforward. We just have to implement the ValidatorFn interface:

interface ValidatorFn {    
(c: AbstractControl): ValidationErrors | null
}

As you can see itโ€™s a simple function that receives the AbstractForm as the only argument (FormGroup in our case) and returns either null if the form is valid, or ValidationErrors object if the form is invalid.

Here is the example implementation

const MyAwesomeRangeValidator: ValidatorFn = (fg: FormGroup) => {
const start = fg.get('rangeStart').value;
const end = fg.get('rangeEnd').value;
return start !== null && end !== null && start < end
? null
: { range: true };
};

Other than some boring null checks itโ€™s as simple as comparing the values from both child controls. Notice we cannot use !!start && !!end && start < end, because 0 is falsy in JavaScript and our range might contain 0 values and still be valid.

Also the null checks are necessary, because I donโ€™t know whether 0 < null is true and I am too afraid to find out. ๐Ÿ‘ป

And thatโ€™s all there is to it. We have a working range component with proper cross validation.

Complete example ๐Ÿ”ฅ๐Ÿ”ฅ๐Ÿ”ฅ

import { Component } from '@angular/core';
import { ValidatorFn, FormBuilder, FormGroup, Validators } from '@angular/forms';
const MyAwesomeRangeValidator: ValidatorFn = (fg: FormGroup) => {
const start = fg.get('rangeStart').value;
const end = fg.get('rangeEnd').value;
return start !== null && end !== null && start < end
? null
: { range: true };
};
@Component({
selector: 'range',
template: `
<form [formGroup]="form">
<input formControlName="rangeStart" placeholder="Range start" type="number">
<input formControlName="rangeEnd" placeholder="Range end" type="number">
</form>
<div> Valid: {{ form.valid ? '๐Ÿ‘' : '๐Ÿ‘Ž' }} </div>
`
})
export class AppComponent {
form: FormGroup;
constructor(private fb: FormBuilder) {
this.form = this.fb.group({
rangeStart: [null, Validators.required],
rangeEnd: [null, Validators.required]
}, { validator: MyAwesomeRangeValidator });
}
}

Live demo

If you like the content โ€ฆ please clap ๐Ÿ‘ ๐Ÿ‘ ๐Ÿ‘

--

--