How to find the invalid controls in Angular(v2 onwards) reactive form

Viewed 166135

I have a reactive form in Angular like below:

this.AddCustomerForm = this.formBuilder.group({
    Firstname: ['', Validators.required],
    Lastname: ['', Validators.required],
    Email: ['', Validators.required, Validators.pattern(this.EMAIL_REGEX)],
    Picture: [''],
    Username: ['', Validators.required],
    Password: ['', Validators.required],
    Address: ['', Validators.required],
    Postcode: ['', Validators.required],
    City: ['', Validators.required],
    Country: ['', Validators.required]
});

createCustomer(currentCustomer: Customer) 
{
    if (!this.AddCustomerForm.valid)
    {
        //some app logic
    }
}

this.AddCustomerForm.valid returns false, but everything looks good.

I have tried to find with checking the status property in the controls collection. But I wonder if there is a way to find the invalid ones and display to the user?

18 Answers

An invalid Angular control has the CSS class named 'ng-invalid'.

Under DevTools in Chrome, select Console tab.

In console prompt run the following command in order to get the invalid Angular controls that bear the CSS class 'ng-invalid'

document.getElementsByClassName('ng-invalid')

The output should be similar to this: enter image description here

In this case, the underlined text is for the form control listen-address and the encircled text: .ng-invalid indicates that the control is invalid.

Note: Tested in chrome

I just battled this issue: Every form field is valid, but still the form itself is invalid.

Turns out that I had set 'Validator.required' on a FormArray where controls are added/removed dynamically. So even if the FormArray was empty, it was still required and therefore the form was always invalid, even if every visible control was correctly filled.

I didn't find the invalid part of the form, because my 'findInvalidControls' function only checked FormControl's and not FormGroup/FormArray. So I updated it a bit:

/* 
   Returns an array of invalid control/group names, or a zero-length array if 
   no invalid controls/groups where found 
*/
public findInvalidControlsRecursive(formToInvestigate:FormGroup|FormArray):string[] {
    var invalidControls:string[] = [];
    let recursiveFunc = (form:FormGroup|FormArray) => {
      Object.keys(form.controls).forEach(field => { 
        const control = form.get(field);
        if (control.invalid) invalidControls.push(field);
        if (control instanceof FormGroup) {
          recursiveFunc(control);
        } else if (control instanceof FormArray) {
          recursiveFunc(control);
        }        
      });
    }
    recursiveFunc(formToInvestigate);
    return invalidControls;
  }

Now, in angular 9, you can use the markAllAsTouched() method to show the invalid controls validators:

this.AddCustomerForm.markAllAsTouched();

There exists an .error property on each the control of the reactive form. If this .error is set to true it indicates that a control is invalid. Thus, looping through the controls and checking this .error field will let us know which fields/ controls are invalid.

The below code will log all the invalid the controls

for (let el in this.ReactiveForm.controls) {
      if (this.ReactiveForm.controls[el].errors) {
        console.log(el)
      }
 }          

One can alternatively append the field name to an array or a string and indicate the user which fields are invalid

try this

 findInvalidControls(f: FormGroup) {
    const invalid = [];
    const controls = f.controls;
    for (const name in controls) {
      if (controls[name].invalid) {
        invalid.push(name);
      }
    }
    return invalid;
  }

If you are not having much fields in the form, you can simply F12 and hover over the control, you will be able to see the pop-up with field's pristine/touched/valid values- "#fieldname.form-control.ng-untouched.ng-invalid".

I took the liberty to improve AngularInDepth.com-s code, so that it recursively searches for invalid inputs in nested forms also. Wether it be nested by FormArray-s or FormGroup-s. Just input the top level formGroup and it will return all the FormControls that are invalid.

You can possibly skim some of the "instanceof" type checks away, if you would separate the FormControl check and addition to invalid array functionality into a separate function. This would make the function look a lot cleaner, but I needed a global, single function, option to get a flat array of all the invalid formControls and this is the solution!

findInvalidControls( _input: AbstractControl, _invalidControls: AbstractControl[] ): AbstractControl[] {
    if ( ! _invalidControls ) _invalidControls = [];
    if ( _input instanceof FormControl  ) {
        if ( _input.invalid ) _invalidControls.push( _input );
        return _invalidControls;
    }

    if ( ! (_input instanceof FormArray) && ! (_input instanceof FormGroup) ) return _invalidControls;

    const controls = _input.controls;
    for (const name in controls) {
        let control = controls[name];
        switch( control.constructor.name )
        {
            case 'AbstractControl':
            case 'FormControl':
                if (control.invalid) _invalidControls.push( control );
                break;

            case 'FormArray':
                (<FormArray> control ).controls.forEach( _control => _invalidControls = findInvalidControls( _control, _invalidControls ) );
                break;

            case 'FormGroup':
                _invalidControls = findInvalidControls( control, _invalidControls );
                break;
        }
    }

    return _invalidControls;
}

Just for those that need it, so they don't have to code it themselves..

Edit #1

It was requested that it also returns invalid FormArray-s and FormGroups, so if you need that also, use this code

findInvalidControls( _input: AbstractControl, _invalidControls: AbstractControl[] ): AbstractControl[] {
    if ( ! _invalidControls ) _invalidControls = [];
    if ( _input instanceof FormControl  ) {
        if ( _input.invalid ) _invalidControls.push( _input );
        return _invalidControls;
    }

    if ( ! (_input instanceof FormArray) && ! (_input instanceof FormGroup) ) return _invalidControls;

    const controls = _input.controls;
    for (const name in controls) {
        let control = controls[name];
        if (control.invalid) _invalidControls.push( control );
        switch( control.constructor.name )
        {    
            case 'FormArray':
                (<FormArray> control ).controls.forEach( _control => _invalidControls = findInvalidControls( _control, _invalidControls ) );
                break;

            case 'FormGroup':
                _invalidControls = findInvalidControls( control, _invalidControls );
                break;
        }
    }

    return _invalidControls;
}

you can log value of form console.log(this.addCustomerForm.value), it will console all control's value then null or ""(empty) fields indicate invalid controls

I think you should try using this.form.updateValueAndValidity() or try executing that same method in each of the controls.

So I too have countered this dragon. And like the brave knight I am, I first gathered my weapons, read the maps and then fought this awful beast.

Note

This is not an acceptable answer for complex forms or structures, but I found it working for the easy ones without to many complexity

The code does the following:

  • Get the form controls as array
  • loop over and check if form control is invalid
  • if invalid the filter will INCLUDE it
  • if any was invalid, result array will be filled with that controls
  • if the length of this result is equal then 0, we can state that no controls were invalid and by that the whole form is valid
isFormValid = () :boolean => 
    Object.values(this.form.controls)
        .filter(c => c.invalid).length === 0

// or single lined
isFormValid = () :boolean => Object.values(this.form.controls).filter(c => c.invalid).length === 0

You can use it in the place you want, on submit buttons, onSubmit or on your own sweet spot.

A cleaner and immutable recursive version of solution to above problem:

P.S: you will need both methods.

Working tested uptill Angular 11

In case compiler complains about flatMap, refer to this(Typescript flatMap, flat, flatten doesn't exist on type any[]), and don't forger to restart ng serve

findInvalidControls(controls = this.defaultFormGroup.controls) {
    const ctrls = Object.values(controls);
    const names = Object.keys(controls);
    return ctrls.map((a,i) => [a, i])
      .filter(a => (a[0] as FormControl).invalid)
      .flatMap(a => {
        if (a[0] instanceof FormArray) {
          return this.findInvalidArrayControls(a[0].controls);
        } else if (a[0] instanceof FormGroup) {
          return this.findInvalidControls(a[0].controls);
        } else {
          return names[a[1] as number];
        }
      });
  }

  findInvalidArrayControls(controls: AbstractControl[]) {
    const ctrls = Object.values(controls);
    const names = Object.keys(controls);
    return ctrls.map((a,i) => [a, i])
      .filter(a => (a[0] as FormControl).invalid)
      .flatMap(a => {
        if (a[0] instanceof FormArray) {
          return this.findInvalidArrayControls(a[0].controls);
        } else if (a[0] instanceof FormGroup) {
          return this.findInvalidControls(a[0].controls);
        }
         else {
          return names[a[1] as number];
        }
     });
  }

Create the flag

inProcess: boolean= false
   
   this.AddCustomerForm = this.formBuilder.group({
       Firstname: ['', Validators.required],
       Lastname: ['', Validators.required],
       Username: ['', Validators.required],
       Password: ['', Validators.required],
       Address: ['', Validators.required],
       Postcode: ['', Validators.required],
       City: ['', Validators.required],
       Country: ['', Validators.required]
   });

onSubmit()
{
   if(this.AddCustomerForm.invalid)
   {
     return
   }

  this.inProcess = true

// pass form value to restapi

}

and this inProcess flag you used in HTML form disable button

<button mat-button [disable]="inProcess"> ADD </button>

Once all form values are correct then only ADD button is visible

hope it will help u guys!!!

For anybody who needs to filter formArray, to only have valid controls here's the solution;

const validControls = this.participantsDetails.controls.filter(control => control.valid);

and for invalid ones of course;

const validControls = this.participantsDetails.controls.filter(control => control.invalid);

Worked for FormGroup and FormArray. This method returns invalid control names.

private getInvalidControlNames(input: FormGroup | FormArray): string[] {
  let invalidControlNames: string[] = [];
  Object.keys(input.controls).forEach((controlName) => {
  const control = input.get(controlName)!;
  if (control.invalid && control instanceof FormControl) {
    invalidControlNames.push(controlName);
  } else if (
    control.invalid &&
    (control instanceof FormGroup || control instanceof FormArray)
  ) {
    invalidControlNames.push(...this.getInvalidControlNames(control));
  }
  });
  return [...new Set(invalidControlNames)];
}

Check empty or null form control value in html page

Form controls value: {{formname.value | json}}
Related