Search code examples
angularcross-validationangular-reactive-forms

Angular reactive forms validate at least on field is not empty and 'lifecycle' of validator


I want to validate form that at least one field is not empty - at least one field is filed. I created custom validator:

import { FormControl, FormGroup, ValidationErrors, ValidatorFn } from "@angular/forms";

export const pfkValidator: ValidatorFn = (abstractControl: AbstractControl): ValidationErrors | null => {
  let fg = (abstractControl as FormGroup);
  let isValid = false;
  Object.keys(fg.controls).forEach(
    field => {
      const ctrl = fg.get(field);
      if (ctrl instanceof FormControl) {
        console.log('ctrl value', ctrl.value);
        if (ctrl.value) {
          isValid = true;
        }
      }
    });

  if (isValid) return { 'valid': true }
  return null;
}

This is how form is initialized:

ngOnInit(): void {
    this.form = this.fb.group({
      Field1: [null],
      Field2: [null],
      Field3: [null],
      Field4: [null],
      Field5: [null],
      Field6: [null],
      Field7: [null],
      Field8: [null],
      Field9: [false],
      Field10: [false],
    }, { validators: pfkValidator });
}

Issue is when forms loads, validation is activated for every field, which takes quite a time. My form has 10 fields and I have logged 10x10 inputs in console.

part of the log

Is this expected behavior?


Solution

  • Below is your implementation which shows 121 logs in the console

    Stackblitz demo

    However in below implementation using FormArrays I have tried to reduce the running operations to avoid the loops Demo using FormArrays. The console now shows only 14 logs initially with 1 log per edit. You may even add updateOn: 'blur' to improve perfomance even further

    export const pfkValidator: ValidatorFn = (
      abstractControl: AbstractControl
    ): ValidationErrors | null => {
      console.log("ctrl value", abstractControl.value.join(""));
      if (abstractControl.value.join("") !== "") {
        return { atLeastOne: false };
      }
      return null;
    };
    

    This can be implemented like below

    export class AppComponent {
      form: FormGroup;
      constructor(private fb: FormBuilder) {}
      get fields() {
        return this.form.get("fields") as FormArray;
      }
      ngOnInit(): void {
        this.form = this.fb.group({
          fields: this.fb.array(
            [null, null, null, null, null, null, null, null, false, false],
            { validators: [pfkValidator] }
          )
        });
      }
    }
    

    and in the html

    <form [formGroup]='form'>
      <ng-container formArrayName='fields'>
        <input *ngFor="let control of fields.controls; let i = index" [formControlName]="i">
      </ng-container>
    
    </form>