Reactive Forms - mark fields as touched
Asked Answered
S

21

142

I am having trouble finding out how to mark all form's fields as touched. The main problem is that if I do not touch fields and try to submit form - validation error in not shown up. I have placeholder for that piece of code in my controller.
My idea is simple:

  1. user clicks submit button
  2. all fields marks as touched
  3. error formatter reruns and displays validation errors

If anyone have other idea how to show errors on submit, without implementing new method - please share them. Thanks!


My simplified form:

<form class="form-horizontal" [formGroup]="form" (ngSubmit)="onSubmit(form.value)">
    <input type="text" id="title" class="form-control" formControlName="title">
    <span class="help-block" *ngIf="formErrors.title">{{ formErrors.title }}</span>
    <button>Submit</button>
</form>

And my controller:

import {Component, OnInit} from '@angular/core';
import {FormGroup, FormBuilder, Validators} from '@angular/forms';

@Component({
  selector   : 'pastebin-root',
  templateUrl: './app.component.html',
  styleUrls  : ['./app.component.css']
})
export class AppComponent implements OnInit {
  form: FormGroup;
  formErrors = {
    'title': ''
  };
  validationMessages = {
    'title': {
      'required': 'Title is required.'
    }
  };

  constructor(private fb: FormBuilder) {
  }

  ngOnInit(): void {
    this.buildForm();
  }

  onSubmit(form: any): void {
    // somehow touch all elements so onValueChanged will generate correct error messages

    this.onValueChanged();
    if (this.form.valid) {
      console.log(form);
    }
  }

  buildForm(): void {
    this.form = this.fb.group({
      'title': ['', Validators.required]
    });
    this.form.valueChanges
      .subscribe(data => this.onValueChanged(data));
  }

  onValueChanged(data?: any) {
    if (!this.form) {
      return;
    }

    const form = this.form;

    for (const field in this.formErrors) {
      if (!this.formErrors.hasOwnProperty(field)) {
        continue;
      }

      // clear previous error message (if any)
      this.formErrors[field] = '';
      const control = form.get(field);
      if (control && control.touched && !control.valid) {
        const messages = this.validationMessages[field];
        for (const key in control.errors) {
          if (!control.errors.hasOwnProperty(key)) {
            continue;
          }
          this.formErrors[field] += messages[key] + ' ';
        }
      }
    }
  }
}
Subcutaneous answered 10/11, 2016 at 14:19 Comment(1)
form.control.markAllAsTouched()Ianthe
S
183

The following function recurses through controls in a form group and gently touches them. Because the control's field is an object, the code call Object.values() on the form group's control field.

      /**
       * Marks all controls in a form group as touched
       * @param formGroup - The form group to touch
       */
      private markFormGroupTouched(formGroup: FormGroup) {
        (<any>Object).values(formGroup.controls).forEach(control => {
          control.markAsTouched();
    
          if (control.controls) {
            this.markFormGroupTouched(control);
          }
        });
      }
Scab answered 24/5, 2017 at 6:42 Comment(5)
this sadly does not work in Internet Explorer :( simply change (<any>Object).values(formGroup.controls) to Object.keys(formGroup.controls).map(x => formGroup.controls[x]) (from #42830757)Fourpenny
This was a huge help to me using FormGroup and FormControl and wondering how to show the user that they didn't touch a required field. Thank you.Popelka
@Popelka no problem! I'm glad it helped :]Scab
+1 Just one minor issue at the recursive part. You are already iterating over controls at the start of the function so it should be the following instead: if (control.controls) { markFormGroupTouched(control); }Carnify
touched just means the input was blurred once. To make errors appear, I also had to also call updateValueAndValidity() on my controls.Vitrine
O
230

From Angular 8 you can simply use

this.form.markAllAsTouched();

To mark a control and it's descendant controls as touched.

AbstractControl doc

Oriana answered 22/5, 2019 at 18:53 Comment(3)
If this seems not to be working for some controls, they are probably not in that FormGroup.Formant
This is working in Angular 13 too. Thank you :)Damalis
This solution do not working for HTML Select elementJarman
S
183

The following function recurses through controls in a form group and gently touches them. Because the control's field is an object, the code call Object.values() on the form group's control field.

      /**
       * Marks all controls in a form group as touched
       * @param formGroup - The form group to touch
       */
      private markFormGroupTouched(formGroup: FormGroup) {
        (<any>Object).values(formGroup.controls).forEach(control => {
          control.markAsTouched();
    
          if (control.controls) {
            this.markFormGroupTouched(control);
          }
        });
      }
Scab answered 24/5, 2017 at 6:42 Comment(5)
this sadly does not work in Internet Explorer :( simply change (<any>Object).values(formGroup.controls) to Object.keys(formGroup.controls).map(x => formGroup.controls[x]) (from #42830757)Fourpenny
This was a huge help to me using FormGroup and FormControl and wondering how to show the user that they didn't touch a required field. Thank you.Popelka
@Popelka no problem! I'm glad it helped :]Scab
+1 Just one minor issue at the recursive part. You are already iterating over controls at the start of the function so it should be the following instead: if (control.controls) { markFormGroupTouched(control); }Carnify
touched just means the input was blurred once. To make errors appear, I also had to also call updateValueAndValidity() on my controls.Vitrine
P
14

Regarding to @masterwork's answer. I tried that solution, but I got an error when the function tried to dig, recursively, inside a FormGroup, because there is passing a FormControl argument, instead of a FormGroup, at this line:

control.controls.forEach(c => this.markFormGroupTouched(c));

Here is my solution

markFormGroupTouched(formGroup: FormGroup) {
 (<any>Object).values(formGroup.controls).forEach(control => {
   if (control.controls) { // control is a FormGroup
     markFormGroupTouched(control);
   } else { // control is a FormControl
     control.markAsTouched();
   }
 });
}
Polanco answered 23/1, 2018 at 15:42 Comment(0)
W
13

From Angular v8, you have this built-in with the help of the markAllAsTouched method.

As an example, you could use it like

form.markAllAsTouched();

See the official doc : https://angular.io/api/forms/AbstractControl#markallastouched

Worcester answered 20/6, 2019 at 9:3 Comment(1)
how is this answer different from @hovado's answer?Argentiferous
I
9

Looping through the form controls and marking them as touched would also work:

for(let i in this.form.controls)
    this.form.controls[i].markAsTouched();
Ivatts answered 7/1, 2018 at 14:32 Comment(2)
Thanks mate your solution is quite good the only thing I would add because tslint complains is this: for (const i in this.form.controls) { if (this.form.controls[i]) { this.form.controls[i].markAsTouched(); } }Wendellwendi
This doesn't works if your formGroup contains other formGroupsVitrine
S
3

This is my solution

      static markFormGroupTouched (FormControls: { [key: string]: AbstractControl } | AbstractControl[]): void {
        const markFormGroupTouchedRecursive = (controls: { [key: string]: AbstractControl } | AbstractControl[]): void => {
          _.forOwn(controls, (c, controlKey) => {
            if (c instanceof FormGroup || c instanceof FormArray) {
              markFormGroupTouchedRecursive(c.controls);
            } else {
              c.markAsTouched();
            }
          });
        };
        markFormGroupTouchedRecursive(FormControls);
      }
Schizogenesis answered 6/8, 2017 at 10:37 Comment(0)
R
2
onSubmit(form: any): void {
  if (!this.form) {
    this.form.markAsTouched();
    // this.form.markAsDirty(); <-- this can be useful 
  }
}
Routinize answered 10/11, 2016 at 14:42 Comment(5)
Just tried that and somehow it does not touch child form elements. Had to to write loop that marks all child elements manually. Do You have any clue why markAsTouched() does not touched child elements?Unterwalden
What angular versions you are using?Routinize
Angular version is 2.1.0Unterwalden
Looks like I found why markAsTouched() do not mark child elements - github.com/angular/angular/issues/11774 . TL;DR: It's not a bug.Unterwalden
Yep, i remember now. You can disable submit button if form is not valid, <button [disable]="!this.form">Submit</button>Routinize
A
2

I had this issue but found the "correct" way of doing so, despite it not being in any Angular tutorial I've ever found.

In your HTML, on the form tag, add the same Template Reference Variable #myVariable='ngForm' ('hashtag' variable) that the Template-Driven Forms examples use, in addition to what the Reactive Forms examples use:

<form [formGroup]="myFormGroup" #myForm="ngForm" (ngSubmit)="submit()">

Now you have access to myForm.submitted in the template which you can use instead of (or in addition to) myFormGroup.controls.X.touched:

<div *ngIf="myForm.submitted" class="text-error"> <span *ngIf="myFormGroup.controls.myFieldX.errors?.badDate">invalid date format</span> <span *ngIf="myFormGroup.controls.myFieldX.errors?.isPastDate">date cannot be in the past.</span> </div>

Know that myForm.form === myFormGroup is true... as long as you don't forget the ="ngForm" part. If you use #myForm alone, it won't work because the var will be set to the HtmlElement instead of the Directive driving that element.

Know that myFormGroup is visible in your Component's typescript code per the Reactive Forms tutorials, but myForm isn't, unless you pass it in through a method call, like submit(myForm) to submit(myForm: NgForm): void {...}. (Notice NgForm is in title caps in the typescript but camel case in HTML.)

Ascogonium answered 11/6, 2017 at 0:32 Comment(0)
F
1

I ran into the same problem, but I do not want to "pollute" my components with code that handles this. Especially since I need this in many forms and I do not want to repeat the code on various occasions.

Thus I created a directive (using the answers posted so far). The directive decorates NgForm's onSubmit-Method: If the form is invalid it marks all fields as touched and aborts submission. Otherwise the usual onSubmit-Method executes normally.

import {Directive, Host} from '@angular/core';
import {NgForm} from '@angular/forms';

@Directive({
    selector: '[appValidateOnSubmit]'
})
export class ValidateOnSubmitDirective {

    constructor(@Host() form: NgForm) {
        const oldSubmit = form.onSubmit;

        form.onSubmit = function (): boolean {
            if (form.invalid) {
                const controls = form.controls;
                Object.keys(controls).forEach(controlName => controls[controlName].markAsTouched());
                return false;
            }
            return oldSubmit.apply(form, arguments);
        };
    }
}

Usage:

<form (ngSubmit)="submit()" appValidateOnSubmit>
    <!-- ... form controls ... -->
</form>
Feretory answered 10/12, 2017 at 4:8 Comment(0)
N
1

This is the code that I am actually using.

validateAllFormFields(formGroup: any) {
    // This code also works in IE 11
    Object.keys(formGroup.controls).forEach(field => {
        const control = formGroup.get(field);

        if (control instanceof FormControl) {
            control.markAsTouched({ onlySelf: true });
        } else if (control instanceof FormGroup) {               
            this.validateAllFormFields(control);
        } else if (control instanceof FormArray) {  
            this.validateAllFormFields(control);
        }
    });
}    
Nanon answered 29/12, 2017 at 15:5 Comment(0)
P
1

This code works for me:

markAsRequired(formGroup: FormGroup) {
  if (Reflect.getOwnPropertyDescriptor(formGroup, 'controls')) {
    (<any>Object).values(formGroup.controls).forEach(control => {
      if (control instanceof FormGroup) {
        // FormGroup
        markAsRequired(control);
      }
      // FormControl
      control.markAsTouched();
    });
  }
}
Pelmas answered 23/1, 2018 at 15:33 Comment(0)
U
1

A solution without recursion

For those worried about performance, I've come up with a solution that doesn't use recursion, although it still iterates over all controls in all levels.

 /**
  * Iterates over a FormGroup or FormArray and mark all controls as
  * touched, including its children.
  *
  * @param {(FormGroup | FormArray)} rootControl - Root form
  * group or form array
  * @param {boolean} [visitChildren=true] - Specify whether it should
  * iterate over nested controls
  */
  public markControlsAsTouched(rootControl: FormGroup | FormArray,
    visitChildren: boolean = true) {

    let stack: (FormGroup | FormArray)[] = [];

    // Stack the root FormGroup or FormArray
    if (rootControl &&
      (rootControl instanceof FormGroup || rootControl instanceof FormArray)) {
      stack.push(rootControl);
    }

    while (stack.length > 0) {
      let currentControl = stack.pop();
      (<any>Object).values(currentControl.controls).forEach((control) => {
        // If there are nested forms or formArrays, stack them to visit later
        if (visitChildren &&
            (control instanceof FormGroup || control instanceof FormArray)
           ) {
           stack.push(control);
        } else {
           control.markAsTouched();
        }
      });
    }
  }

This solution works form both FormGroup and also FormArray.

You can play around with it here: angular-mark-as-touched

Unborn answered 24/1, 2019 at 9:37 Comment(3)
@VladimirPrudnikov The problem is that when making recursive call to a function there is usually more overhead associated. Because of that, the CPU will spend more time handling the call stack. When using loops the CPU will spend most time performing the algorithm itself. The advantage of recursion is that the code is usually more readable. So, if performance is not an issue, I would say you could stick with recursion.Unborn
"Premature optimization is the root of all evil."Whitewall
@DemPilafian I agree with the quotation. However it doesn't apply here, because if someone comes up to this thread they'll be able to get an optimized solution for free (no time spent on it). And, btw, in my case I really had reasons to optimize it =)Unborn
D
1

as per @masterwork

typescript code for the angular version 8

private markFormGroupTouched(formGroup: FormGroup) {
    (Object as any).values(formGroup.controls).forEach(control => {
      control.markAsTouched();
      if (control.controls) {
        this.markFormGroupTouched(control);
      }
    });   }
Danilodanio answered 29/3, 2020 at 14:29 Comment(0)
G
0

Here is how I do it. I don't want the error fields to show until after the submit button is pressed (or the form is touched).

import {FormBuilder, FormGroup, Validators} from "@angular/forms";

import {OnInit} from "@angular/core";

export class MyFormComponent implements OnInit {
  doValidation = false;
  form: FormGroup;


  constructor(fb: FormBuilder) {
    this.form = fb.group({
      title: ["", Validators.required]
    });

  }

  ngOnInit() {

  }
  clickSubmitForm() {
    this.doValidation = true;
    if (this.form.valid) {
      console.log(this.form.value);
    };
  }
}

<form class="form-horizontal" [formGroup]="form" >
  <input type="text" class="form-control" formControlName="title">
  <div *ngIf="form.get('title').hasError('required') && doValidation" class="alert alert-danger">
            title is required
        </div>
  <button (click)="clickSubmitForm()">Submit</button>
</form>
Guinna answered 10/11, 2016 at 14:53 Comment(1)
This one looks like it can get heavy over time, when adding new validation rules. But I got the point.Unterwalden
M
0

I completely understand the OP's frustration. I use the following:

Utility function:

/**
 * Determines if the given form is valid by touching its controls 
 * and updating their validity.
 * @param formGroup the container of the controls to be checked
 * @returns {boolean} whether or not the form was invalid.
 */
export function formValid(formGroup: FormGroup): boolean {
  return !Object.keys(formGroup.controls)
    .map(controlName => formGroup.controls[controlName])
    .filter(control => {
      control.markAsTouched();
      control.updateValueAndValidity();
      return !control.valid;
    }).length;
}

Usage:

onSubmit() {
  if (!formValid(this.formGroup)) {
    return;
  }
  // ... TODO: logic if form is valid.
}

Note that this function does not yet cater for nested controls.

Mace answered 28/5, 2017 at 6:36 Comment(0)
W
0

See this gem. So far the most elegant solution I've seen.

Full code

import { Injectable } from '@angular/core';
import { FormGroup } from '@angular/forms';

const TOUCHED = 'markAsTouched';
const UNTOUCHED = 'markAsUntouched';
const DIRTY = 'markAsDirty';
const PENDING = 'markAsPending';
const PRISTINE = 'markAsPristine';

const FORM_CONTROL_STATES: Array<string> = [TOUCHED, UNTOUCHED, DIRTY, PENDING, PRISTINE];

@Injectable({
  providedIn: 'root'
})
export class FormStateService {

  markAs (form: FormGroup, state: string): FormGroup {
    if (FORM_CONTROL_STATES.indexOf(state) === -1) {
      return form;
    }

    const controls: Array<string> = Object.keys(form.controls);

    for (const control of controls) {
      form.controls[control][state]();
    }

    return form;
  }

  markAsTouched (form: FormGroup): FormGroup {
    return this.markAs(form, TOUCHED);
  }

  markAsUntouched (form: FormGroup): FormGroup {
    return this.markAs(form, UNTOUCHED);
  }

  markAsDirty (form: FormGroup): FormGroup {
    return this.markAs(form, DIRTY);
  }

  markAsPending (form: FormGroup): FormGroup {
    return this.markAs(form, PENDING);
  }

  markAsPristine (form: FormGroup): FormGroup {
    return this.markAs(form, PRISTINE);
  }
}
Welltimed answered 8/3, 2019 at 11:1 Comment(0)
L
0
    /**
    * Marks as a touched
    * @param { FormGroup } formGroup
    *
    * @return {void}
    */
    markFormGroupTouched(formGroup: FormGroup) {
        Object.values(formGroup.controls).forEach((control: any) => {

            if (control instanceof FormControl) {
                control.markAsTouched();
                control.updateValueAndValidity();

            } else if (control instanceof FormGroup) {
                this.markFormGroupTouched(control);
            }
        });
    }
Lowestoft answered 13/8, 2019 at 10:51 Comment(0)
L
0

View:

<button (click)="Submit(yourFormGroup)">Submit</button>   

API

Submit(form: any) {
  if (form.status === 'INVALID') {
      for (let inner in details.controls) {
           details.get(inner).markAsTouched();
       }
       return false; 
     } 
     // as it return false it breaks js execution and return 
Lantha answered 20/9, 2019 at 9:2 Comment(0)
R
0

I made a version with some changes in the answers presented, for those who are using versions older than version 8 of the angular, I would like to share it with those who are useful.

Utility function:

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

function getAllControls(formGroup: FormGroup): FormControl[] {
  const controls: FormControl[] = [];
  (<any>Object).values(formGroup.controls).forEach(control => {
    if (control.controls) { // control is a FormGroup
      const allControls = getAllControls(control);
      controls.push(...allControls);
    } else { // control is a FormControl
      controls.push(control);
    }
  });
  return controls;
}

export function isValidForm(formGroup: FormGroup): boolean {
  return getAllControls(formGroup)
    .filter(control => {
      control.markAsTouched();
      return !control.valid;
    }).length === 0;
}

Usage:

onSubmit() {
 if (this.isValidForm()) {
   // ... TODO: logic if form is valid
 }
}
Rola answered 21/2, 2020 at 20:13 Comment(0)
V
0

Angular 13:

this.form.markAsDirty();
this.form.markAllAsTouched();
Villegas answered 2/8, 2022 at 12:43 Comment(0)
E
0
@Component()
export class AppComponent {
  public loginForm: FormGroup = new FormGroup({
    email: new FormControl('', Validators.required),
    password: new FormControl('', Validators.required)
  });

  public onSubmit(): void {
    this.loginForm.markAllAsTouched();  // calling mark as touch every time.
    if(this.loginForm.valid) { ... }
  }
}
Excalibur answered 6/1, 2023 at 1:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.