Creating elegant reactive forms with RxWebValidators
Write elegant conditional validation, cross field validation code for managing Angular Reactive Forms.

In this article we will focus our attention on how to make a complex reactive form validations code more readable, extensible, and maintainable.
The most excellent benefit of the Angular Reactive Form approach removes the core validation logic from the template. It allows us to focus at one place (component) for form validation in a controlled manner. But when we are working on the complex form, our code becomes clumsy or less performant.
How?
- As per the Angular guide for Cross Control Validation, we have to set the custom validator on the
FormGroup
level. If the form has too many FormControls, it may create a performance issue because the custom validator trigger on every field value changes. Refer to the snap:

- Reactive Form APIs are cool but, it's complex to manage the code more cleanly where conditional validation scenarios come into the picture. For example, we want to make our field required based upon other
FormControl
value changes. To achieve this, we have to subscribe to the value changes of dependentFormControl(notification)
and use thesetValidators
method of respective conditional requiredFormControl(phone)
. It is a naive and buggy approach because there are fair chances to miss the initially applied validators while assigning the conditionally required validator to theFormControl
.
Let's learn the elegant solution of cross-field validation, conditional validation, and on-demand validation in Reactive Forms with RxWebValidators.
For using RxWebValidators, we have to install the package(@rxweb/reactive-form-validators) in the project. Fire below command for installation:
npm i @rxweb/reactive-form-validators
After installation, register the RxReactiveFormsModule
in the root module of the application.
import { RxReactiveFormsModule } from "@rxweb/reactive-form-validators"
@NgModule({
imports: [ RxReactiveFormsModule,...],
declarations: [...],
})
export class AppModule { }
Let's understand the use cases and difference between the standard and RxWebValidators approach.
Cross Field Validation
Use Case: We want to compare two FormControl
values and mark our FormControl
invalid if the control values are not the same.
Standard Approach
const userForm = new FormGroup({
'firstName': new FormControl(),
'password': new FormControl(),
'comparePassword': new FormControl()
},
{ validators: (control: FormGroup): ValidationErrors | null => {
const password = control.get('password');
const comparePassword = control.get('comparePassword');
return password && comparePassword && password.value !== comparePassword.value ? { 'compare': true } : null; }
});
According to the above-shown code, we have added the custom validator on the FormGroup
level; the validator is calling non-validation FormControl
as well, like the firstName
FormControl
. There are some other alternatives to overcome this problem:
- According to Deborah K suggested example, we can create a separate
FormGroup
, which includes 'password' and 'comparePassword'FormControl
. With this approach, we may have to restructure the form value object while passing it to the server. It's bit code overhead in the application and challenging to maintain the consistency. - Another approach is to put the same validator on both
FormControl
instead ofFormGroup
level. The validator only triggered on assignedFormControl
value changes. It seems good, but if there are multiple Cross Controls, then it's challenging to manage the code.
Let's try the same case with RxWebValidators.
let userForm =new FormGroup({
password:new FormControl(),
confirmPassword:new FormControl('', RxwebValidators.compare({fieldName:'password' })),
});
According to the above code, we have used the compare
validator on 'comparePassword' FormControl
. The validator triggers whenever the value of 'password' or 'comparePassword' FormControl
changes. With this approach our code is more readable.

Conditional Validation
Use Case: Once the notification checkbox is tick, the 'phone' field is required.
Standard Approach
setNotification(ticked: boolean): void {
const phoneControl = this.userForm.get('phone');
if (ticked) {
phoneControl.setValidators(Validators.required);
} else {
phoneControl.clearValidators();
}
phoneControl.updateValueAndValidity();
}
When the form gets bigger and involves complex validation rules, then below highlights may apply according to the above-shown code.
- Code maintainability concern, because it's challenging to track how many validation rules on a FormControl.
- Fair chances of inconsistent behavior, if we missed any validator to reassign the same during runtime.
- Possible chances of 'Code Smell' by putting multiple if/else clause to cover lots of conditional validation cases.
Let's refer to the below code, which solves all the concerns mentioned above superficially.
let userForm = new FormGroup({
notification:new FormControl(),
phone:new FormControl('', RxwebValidators.required({conditionalExpression:x => x.notification === true }))
});
By using the conditionalExpression
, the 'phone' FormControl
is only required when the notification
FormControl
value is 'true' With the above approach, the code is more understandable and maintainable.

On-Demand Validation Based Upon Value
Use Case: There are three fields Premium product charges, Purchase price and Resale price. The Resale price must be at least 30% more than Purchase price plus Premium product charges:
Standard Approach
ngOnInit() {
this.userInfoFormGroup = new FormGroup({
premiumCharge:new FormControl(),
purchasePrice:new FormControl(),
resalePrice: new FormControl()
});
this.userInfoFormGroup.controls.premiumCharge.valueChanges.subscribe(t=>{
this.setMinValidator(this.userInfoFormGroup.value)
})
this.userInfoFormGroup.controls.purchasePrice.valueChanges.subscribe(t=>{
this.setMinValidator(this.userInfoFormGroup.value)
})
}
setMinValidator(formValue:any){
const minimumPrice = ((parseInt(formValue.purchasePrice) + parseInt(formValue.premiumCharge)) * 30 / 100);
this.userInfoFormGroup.controls.resalePrice.clearValidators();
this.userInfoFormGroup.controls.resalePrice.setValidators(Validators.min(minimumPrice));
this.userInfoFormGroup.controls.resalePrice.updateValueAndValidity({onlySelf:true});
}
Managing such cases is writing too much code in the component, which makes our code less extensible at some point in time.
Let's transform this code with RxWebValidators elegantly.
this.userInfoFormGroup = new FormGroup({
premiumCharge:new FormControl(),
purchasePrice:new FormControl(),
resalePrice: new FormControl('', RxwebValidators.minNumber({
dynamicConfig: (x, y) => {
const minimumPrice = ((x.purchasePrice + x.premiumCharge) * 30 / 100);
return { value: minimumPrice };
}
}))
});
With this approach we have a comprehensive scope to extend our code. We don't need to subscribe to the value changes of every FormControl
as the library is taking care of dependent validation business calculation FormControls automatically.

Conclusion
We have learned the Cross Control Validation with the help of RxWebValidators, without subscribing to the ValueChanges
or calling the SetValidators
method. The RxWebValidators allows us to handle complex reactive form validation gracefully.
I hope you like this article. If you have any suggestions, please write your comment.