Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to compare/validate many multiple fields against each other without duplicating FormControls

I have these fields:

field1
field2
field3
field4

this.form = this.formbuilder.group({
  'field1' = ['', [<control-specific-validations>]],
  'field2' = ['', [<control-specific-validations>]]
 }, { validator: isField1GreaterThanField2Validator}
 });

More validations I need:

- field3.value must be greater than field4.value
- field3.value must be greater  than field2.value + 1
- field4.value must be less than field1.value

How do you integrate these new validation requirements into the constructed form?

What I do NOT want to do is to setup a

formControl.valueChanges.subscribe(value => { });

for each field and then have many if/else there.

Then I could remove the whole reactive forms module, use 2way-databinding and render a error string in the ui when the validation expression is true.

like image 297
Pascal Avatar asked Oct 15 '25 20:10

Pascal


2 Answers

You can use a custom validator to do this. https://angular.io/guide/form-validation#custom-validators

Here is a working example of what you want: https://stackblitz.com/edit/isgreaterthanotherfield-custom-validator

The validator function itself looks like this:

greaterThan(field: string): ValidatorFn {
  return (control: AbstractControl): {[key: string]: any} => {
    const group = control.parent;
    const fieldToCompare = group.get(field);
    const isLessThan = Number(fieldToCompare.value) > Number(control.value);
    return isLessThan ? {'lessThan': {value: control.value}} : null;
  }
}

I am using the parent property on the control to access the other field.

Note that you cannot set that validator in the form initialization, since the field you are basing it on is not yet defined.

this.myForm = this.fb.group({
  field1: 0,
  field2: 0
});

this.myForm.get('field2').setValidators(this.greaterThan('field1'));

Update: I took it a step further and implemented a custom validator which accepts a predicate function so you can use the same validator for all your comparisons.

See it in action here: https://stackblitz.com/edit/comparison-custom-validator

It uses the same approach as above, but is a bit more flexible since you can pass in any comparison. There are some edge cases the example doesn't consider, like passing a form field name that doesn't exist, if the form field you pass does not actually use numbers / the types don't match up, etc, but I believe that is outside the scope of the question.

This was an interesting question and I enjoyed working on it.

For quick reference, here is what the entire component looks like with the flexible custom validator:

@Component({
  selector: 'my-app',
  templateUrl: './app.component.html',
  styleUrls: [ './app.component.css' ]
})
export class AppComponent  {
  myForm: FormGroup;

  field2HasError: boolean;
  field3HasError: boolean;

  constructor(private fb: FormBuilder) {}

  ngOnInit() {
    this.myForm = this.fb.group({
      field1: 0,
      field2: 0,
      field3: 0,
      field4: 0
    });

    const field1MustBeGreaterThanField2 = 
      this.comparison('field1', (field2Val, field1Val) => {
        return Number(field2Val) < Number(field1Val);
      });

    const field3MustBeGreaterThanField2Plus1 =
      this.comparison('field2', (field3Val, field2Val) => {
        return Number(field3Val) > (Number(field2Val) + 1);
      });

    this.myForm.get('field2').setValidators(field1MustBeGreaterThanField2);
    this.myForm.get('field3').setValidators(field3MustBeGreaterThanField2Plus1);

    this.myForm.get('field2').valueChanges.subscribe(() => {
      this.field2HasError = this.myForm.get('field2').hasError('comparison');
    });

    this.myForm.get('field3').valueChanges.subscribe(() => {
      this.field3HasError = this.myForm.get('field3').hasError('comparison');
    });
  }

  comparison(field: string, predicate: (fieldVal, fieldToCompareVal) => boolean): ValidatorFn {
    return (control: AbstractControl): {[key: string]: any} => {
      const group = control.parent;
      const fieldToCompare = group.get(field);
      console.log('fieldToCompare.value', fieldToCompare.value);
      console.log('field.value', control.value);
      const valid = predicate(control.value, fieldToCompare.value);
      return valid ? null : {'comparison': {value: control.value}};
    }
  }
}
like image 149
vince Avatar answered Oct 18 '25 13:10

vince


//your .ts

  ngOnInit() {
    this.myForm = this.fb.group({
      field1: 0,
      field2: 0,
      field3: 0,
    }, { validator: this.customValidator }); //a unique validator

  }
  customValidator(formGroup: FormGroup) {         
    let errors:any={};
    let i:number=0;
    let valueBefore:any=null;
    Object.keys(formGroup.controls).forEach(field => {
      const control = formGroup.get(field);           
      if (valueBefore)
      {
        if (parseInt(control.value)<valueBefore)
        {
          let newError:any={['errorLess'+i]:true}  //create an object like,e.g. {errorLess1:true}
          errors={...errors,...newError};  //Concat errors
        }

      }
      valueBefore=parseInt(control.value);
      i++;
    });
    if (errors)
      return errors;
  }
}

The .html like

<form [formGroup]="myForm">
  field1:
  <input formControlName="field1">
  <br> field2:
  <input formControlName="field2">
  <div *ngIf="myForm.hasError('errorLess1')">Field 2 less that field 1</div>
  <br> field3:
  <input formControlName="field3">
  <div *ngIf="myForm.hasError('errorLess2')">Field 3 less that field 2</div>
</form>
like image 29
Eliseo Avatar answered Oct 18 '25 12:10

Eliseo