Pass index into FormArray Control validator

Viewed 999

I have a FormArray with simple inputs. I want to pass validator function (custom) an index of the input in FormArray, since my validator is based on that. For example, I want each subsequent AbstractControl to have more letters than the one before (this is only just an example, passing an index is what I really need to achieve).

I mean if I can get something like this

ngOnInit(): void {
    this.items = this.items?.length ? this.items : this.getDefaults(this.size)

    const inputs: FormArray = this.formGroup.get('inputs') as FormArray
        for (let item of this.items) {
            inputs.push(this.fb.control(
            item.value,
            this.validator(),
            this.asyncValidator(),
            ))
        }
}

private validator(): ValidatorFn {
    return (abstractControl: AbstractControl/*, indexInArray */) => {
        // const lenOfPrev = this.items[indexInArray - 1].value.length
        if (abstractControl.value.length <= 0 /* lenOfPrev */) {
            return {
                notLargeEnough: 'should be bigger'
            }
        }
        return null
    }
}
2 Answers

Your aim is to validate a control against another control. The fact that they are in a form array is slightly irrelevant.

When writing a custom validator, you are bound by its interface:

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

So if you can't inject a reference to another control into the interface function, your only other option is to inject the other control into the function itself.

  ngOnInit(): void {
    this.items = this.items?.length ? this.items : this.getDefaults(this.size)

    const inputs: FormArray = this.formGroup.get('inputs') as FormArray
    this.items.forEach((item, i) => {
      const previousControl = i > 0 ? inputs.controls[i - 1] : null;      
      inputs.push(this.fb.control(
        item.value,
        this.validator(),
        this.asyncValidator(),
      ));
    });    
  }

  private validator(previousControl: AbstractControl): ValidatorFn {
    return (control: AbstractControl) => {
      if (!previousControl || control.value.length > previousControl.value.length) {
        return null;
      }

      return {
        notLargeEnough: 'should be bigger'
      };
    }
  }

You have to handle the fact that the first control doesn't have a previous control to compare against.

DEMO: https://stackblitz.com/edit/angular-9atrvn

You will probably want to add something that updates the validation on the related control when a value changes, as this doesn't happen by default.

EDIT

If your array changes during its lifetime, you can use FormControl.setValidators(validators) to update the validators for the affected items.

If you start off with the array

A <- B <- C <- D <- E

And remove one:

A <- B <- D <- E

Then the only dependency that has changed is D -> C now becomes D -> B. So you need to update the validator for D to depend on B.

Note: using setValidators will overwrite all existing validators. So if you are using additional validators, they will also need to be included.

Forked demo: https://stackblitz.com/edit/angular-f93sgn

just add and arguments to the function Validator

private validator(indexInArray:number): ValidatorFn { //<--here
    return (abstractControl: AbstractControl) => {
      //you can use indexInArray here
     if (indexInArray==0)  //For the first give always valid
         return null; 
      //I change this.items by this.form.value
      const lenOfPrev = this.form.value[indexInArray - 1].value.length
      if (abstractControl.value.length <= lenOfPrev.length) {
        return {
          notLargeEnough: 'should be bigger'
        }
      }
      return null
    }
  }

But, if you're using values of the component you need "bind" the validator

this.items.forEach((item, i) => {
      const previousControl = i > 0 ? inputs.controls[i - 1] : null;      
      inputs.push(this.fb.control(
        item.value,
        this.validator(i).bind(this), //<--see the argument and the "bind"
        this.asyncValidator(),
      ));
    }); 

But, sorry. This aproach has a relevant faul, What happens if you change,e.g. the 2nd input from 'aaa' to 'aaaaa'?. If your 3rt input is 'bbbb', before was valid, but not after the change . But angualr can has take accont of this because the 3rt input not change. So, Why not use a Validator over all the formArray. As a custom validator return an object, you can return, e.g. an array [false,false,true,false] and ask about this value

  private validatorArray()
  {
    return (formArray:FormArray)=>{
      if (!formArray.value)
        return null;
      const result= (formArray.value.map((x:any,i:number)=>{
        if (i==0)
           return false;
        return (x.length<=formArray.value[i-1].length)
      }))
      return result.find(x=>x)?{error:result}:null;
    }
  }

In form.errors?.error[i] you has if a control is invalid

You can see the two aproach in this stackblitz

NOTE: I use directly a formArray of FormControls, but it's the same

Upate after read the answer of Kurt, really it's not necesary bind(this) in the validator, simple pass the previous control as Kurt say.

Related