35

I need to assign a custom validator to a FormGroup. I can do this at the time the FormGroup is created like this:

let myForm : FormGroup;

myForm = this.formBuilder.group({
        myControl1: defaultValue,
        myControl2: defaultValue
      }, { validator: this.comparisonValidator })

comparisonValidator(g: FormGroup) {
      if (g.get('myControl1').value > g.get('myControl2'.value)) {
        g.controls['myControl1'].setErrors({ 'value2GreaterThanValue1': true });
        return;
      }
}

I have a situation though where I need to add the custom validator after I've instantiated the FormGroup, so I'm trying to do this after instantiating myForm, instead of adding the validator when the form is instantiated:

let myForm : FormGroup;

myForm = this.formBuilder.group({
        myControl1: defaultValue,
        myControl2: defaultValue
      })

this.myForm.validator = this.comparisonValidator;

This gives me a compiler error:

Type '(g: FormGroup) => void' is not assignable to type 'ValidatorFn'.
  Type 'void' is not assignable to type 'ValidationErrors'.

How do I assign a validator to my FormGroup so that the formGroup is passed as the argument to my comparisonValidator function?

Update - I've added a line showing where I'm doing a setErrors in my comparisonValidator, to make it clearer exactly how I'm trying to set a validation error.

Chris Halcrow
  • 28,994
  • 18
  • 176
  • 206
  • Adding dynamic validator can be done using [setValidators()](https://stackoverflow.com/a/38797547/6426617). but specific for a particular control you can update at a time – Keshan Nageswaran Jun 29 '18 at 05:34
  • You get this error because you return `void` from your `comparisonValidator` function. Change the function to return `null` on the last line (not inside the if-statement) and the error should be gone. – Crisu83 Aug 07 '20 at 12:10

5 Answers5

49

I've created a stackblitz take a look.

In the component.ts file

import { Component } from '@angular/core';
import {FormBuilder,FormGroup, ValidationErrors, ValidatorFn} from '@angular/forms'

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

constructor(private formBuilder: FormBuilder) {
  this.myForm = this.formBuilder.group({
        myControl1: this.defaultValue,
        myControl2: this.defaultValue
      });
      debugger
      this.myForm.setValidators(this.comparisonValidator())
}

 public comparisonValidator() : ValidatorFn{
       return (group: FormGroup): ValidationErrors => {
          const control1 = group.controls['myControl1'];
          const control2 = group.controls['myControl2'];
          if (control1.value !== control2.value) {
             control2.setErrors({notEquivalent: true});
          } else {
             control2.setErrors(null);
          }
          return;
    };
 }
}

In the component.html file

<div>
  <form [formGroup]="myForm">
    <input formControlName="myControl1" type="number">
    <input formControlName="myControl2"  type="number">
    <br>Errors: {{myForm.get('myControl2').errors | json}}
  </form>
</div>
Anuradha Gunasekara
  • 6,553
  • 2
  • 27
  • 37
  • 1
    Thanks Anuradha. To clarify what I'm asking, I'm trying to only add the validator *after* the `FormGroup` is instantiated, instead of adding it *when* the form is instantiated. I've edited the question a little to make it clearer. – Chris Halcrow Jun 29 '18 at 04:47
  • 1
    @ChrisHalcrow I've updated the ansewer take alook. nd You can find a working example in [here](https://stackblitz.com/edit/angular-form-custom-validation). – Anuradha Gunasekara Jun 29 '18 at 06:30
  • 1
    Shouldn't the `ValidatorFn` **return** the errors object? In this case, the `ValidatorFn` calls `setErrors()`, but doesn't `return;` anything – Nate Anderson Oct 15 '22 at 22:26
9

For setting any validators (predefined or customs) after instantiating the formGroup, you will need to use the setValiators() method of FormGroup, f.e.:

  let myFormGroup = this.    _fb.group({
      control1: new FormControl('1', [])
    });
  myFormGroup.setValidators(this.customValidators());
  customValidators(): ValidatorFn {
   let myFun = (cc: FormGroup): ValidationErrors => {
     if(cc.valid) return null;
     else return {something: 'someError'};
   };
   return myFun;
  }
Shadab Faiz
  • 2,380
  • 1
  • 18
  • 28
5

Thanks to @Anuradha Gunasekara - his answer is the most correct and complete solution. A 'quick fix' for my error was just to add a return type of any on the validator. I can still assign the custom validator to the FormGroup, and the FormGroup will be passed implicitly as the argument to my custom validator. This code will work:

let myForm : FormGroup;

myForm = this.formBuilder.group({
           myControl1: defaultValue,
           myControl2: defaultValue
         })

this.myForm.validator = this.comparisonValidator;

comparisonValidator(g: FormGroup) : any {
      if (g.get('myControl1').value > g.get('myControl2'.value)) {
        g.controls['myControl1'].setErrors({ 'value2GreaterThanValue1': true });
      }
}
Chris Halcrow
  • 28,994
  • 18
  • 176
  • 206
3

Remove all form control white space form Form Group

custom validator :

    export function formGroupRemoveWhitespaceValidator(form: FormGroup): ValidationErrors | null {
    const keys: string[] = Object.keys(form.controls);

    if (keys.length) {
        keys.forEach(k => {
            let control = form.controls[k];
            
            if (control && control.value && !control.value.replace(/\s/g, '').length) {
                control.setValue('');
            }
        });
    }

    return null;
}

component :

let myForm : FormGroup;

myForm = this.formBuilder.group({
           myControl1: defaultValue,
           myControl2: defaultValue
         }, { validators: formGroupRemoveWhitespaceValidator });
sweetnandha cse
  • 705
  • 7
  • 16
0

These answers doesn't work for me, because I have other validators too. In my usecase, I have to set the errors also for the opposite field.
Perhaps it doesn't met the requirements of the OP exactly, but it hopefully helps others (like me), which landed on this site.

Here my is solution, which can be safely combined with other validators and also ensure, that the opposite field got an update.

// ...
this.form = this.formBuilder.group({
        myControl1: this.formBuilder.control(defaultValue, [Validators.required, this.validateIsEqualTo('myControl2')]),
        myControl2: this.formBuilder.control(defaultValue, this.validateIsEqualTo('myControl1')
      });

this.form.get('myControl1').valueChanges
   .pipe(
      takeUntil(this.destroy$),
      disinctUntilChanged())
   .subscribe(() => this.form.get('myControl2').updateValueAndValidity());

this.form.get('myControl2').valueChanges
   .pipe(
      takeUntil(this.destroy$),
      disinctUntilChanged())
   .subscribe(() => this.form.get('myControl1').updateValueAndValidity());
// ...

public validateIsEqualTo(otherComponentId): ValidatorFn {
   return (control: FormControl): ValidationErrors => {
      const otherControl = this.form.get(otherComponentId);
      return control.value !== otherControl.value ? {notEquivalent: true} : null;
   }
}
akop
  • 5,981
  • 6
  • 24
  • 51
  • @aokp - Will this work ? the line const otherControl = group.controls[otherComponentId];, how are you accessing group without passing it ? – Mohammed Aamir K Dec 15 '22 at 07:50
  • Sorry, trying to understand why other solutions do not work for you. Would it not be better to have a validator on the formGroup rather than having to pass other control names in the validator ? This way there would be no overhead of passing around control names if the controls list ever increases. – Mohammed Aamir K Dec 15 '22 at 18:56
  • They other answers don't work for me, because of the "double" relation. A change in `controlA` will trigger in a check `controlB`. And then it is endless loop. Special note: I need unique errors and my checks aren't just a equal-check. I simplified it for StackOverflow-answer. – akop Dec 15 '22 at 22:43
  • tldr; all other answers causing a endless loop for my use cases. :) – akop Dec 15 '22 at 22:44