Creating elegant reactive forms with RxWebValidators

Write elegant conditional validation, cross field validation code for managing Angular Reactive Forms.

Creating elegant reactive forms with RxWebValidators

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:
The output of the code of Angular Guide (Cross-Field Validation)
  • 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 dependent FormControl(notification) and use the setValidators method of respective conditional required FormControl(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 the FormControl.

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:

  1. 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.
  2. Another approach is to put the same validator on both FormControl instead of FormGroup level. The validator only triggered on assigned FormControl 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.

The output of cross-field validation by following the approach of RxWebValidators. 

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.

  1. Code maintainability concern, because it's challenging to track how many validation rules on a FormControl.
  2. Fair chances of inconsistent behavior, if we missed any validator to reassign the same during runtime.
  3. 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.

The output of conditional validation by following the approach of RxWebValidators.

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.

The output of on-demand validation by following the approach of RxWebValidators.

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.