【问题标题】:Reactive Forms - mark fields as touched反应式表单 - 将字段标记为已触摸
【发布时间】:2017-03-24 14:41:15
【问题描述】:

我无法找出如何将所有表单的字段标记为已触摸。 主要问题是,如果我不触摸字段并尝试提交表单 - 验证错误不会显示出来。我的控制器中有那段代码的占位符。
我的想法很简单:

  1. 用户点击提交按钮
  2. 所有字段都标记为已触摸
  3. 错误格式化程序重新运行并显示验证错误

如果有人有其他想法如何在提交时显示错误,而不实施新方法 - 请分享。谢谢!


我的简化形式:

<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>

还有我的控制器:

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] + ' ';
        }
      }
    }
  }
}

【问题讨论】:

  • form.control.markAllAsTouched()

标签: angular angular-reactive-forms angular2-forms angular2-formbuilder


【解决方案1】:

Angular 8 你可以简单地使用

this.form.markAllAsTouched();

将控件及其后代控件标记为已触摸。

AbstractControl doc

【讨论】:

  • 如果这似乎不适用于某些控件,它们可能不在该 FormGroup 中。
  • 我喜欢这种 cmets。有 100 个其他 cmets 使用 30-50 行代码来执行此操作,然后出现这条评论;简单、干净且有效!
  • 无与伦比的传奇!!
  • 这也适用于 Angular 13。谢谢你:)
【解决方案2】:

根据@masterwork

angular 版本 8 的打字稿代码

private markFormGroupTouched(formGroup: FormGroup) {
    (Object as any).values(formGroup.controls).forEach(control => {
      control.markAsTouched();
      if (control.controls) {
        this.markFormGroupTouched(control);
      }
    });   }

【讨论】:

    【解决方案3】:

    我做了一个版本,对给出的答案进行了一些更改,对于那些使用 angular 8 版本之前的版本的人,我想与那些有用的人分享。

    实用功能:

    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;
    }
    

    用法:

    onSubmit() {
     if (this.isValidForm()) {
       // ... TODO: logic if form is valid
     }
    }
    

    【讨论】:

      【解决方案4】:

      查看:

      <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 
      

      【讨论】:

        【解决方案5】:
            /**
            * 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);
                    }
                });
            }
        

        【讨论】:

          【解决方案6】:

          从 Angular v8 开始,您可以借助 markAllAsTouched 方法内置此功能。

          作为一个例子,你可以像这样使用它

          form.markAllAsTouched();
          

          查看官方文档:https://angular.io/api/forms/AbstractControl#markallastouched

          【讨论】:

          • 这个答案与@hovado 的答案有何不同?
          【解决方案7】:

          this gem。 迄今为止我见过的最优雅的解决方案。

          完整代码

          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);
            }
          }
          

          【讨论】:

            【解决方案8】:

            没有递归的解决方案

            对于那些担心性能的人,我想出了一个不使用递归的解决方案,尽管它仍然迭代所有级别的所有控件。

             /**
              * 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();
                    }
                  });
                }
              }
            

            此解决方案适用于 FormGroup 和 FormArray。

            你可以在这里玩它:angular-mark-as-touched

            【讨论】:

            • @VladimirPrudnikov 问题是当对函数进行递归调用时,通常会产生更多的相关开销。因此,CPU 将花费更多时间处理调用堆栈。当使用循环时,CPU 将花费大部分时间来执行算法本身。递归的优点是代码通常更具可读性。所以,如果性能不是问题,我会说你可以坚持使用递归。
            • “过早的优化是万恶之源。”
            • @DemPilafian 我同意这个报价。但是它不适用于这里,因为如果有人提出这个线程,他们将能够免费获得优化的解决方案(没有时间花在上面)。而且,顺便说一句,就我而言,我确实有理由对其进行优化 =)
            【解决方案9】:

            以下函数通过表单组中的控件递归并轻轻触摸它们。因为控件字段是一个对象,所以代码在表单组的控件字段上调用了 Object.values()。

              /**
               * 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);
                  }
                });
              }
            

            【讨论】:

            • 遗憾的是,这在 Internet Explorer 中不起作用 :( 只需将 (&lt;any&gt;Object).values(formGroup.controls) 更改为 Object.keys(formGroup.controls).map(x =&gt; formGroup.controls[x])(来自 stackoverflow.com/questions/42830257/…
            • 这对我使用 FormGroup 和 FormControl 有很大帮助,我想知道如何向用户展示他们没有触及必填字段。谢谢。
            • @NAMS 没问题!我很高兴它有帮助:]
            • +1 递归部分只有一个小问题。您已经在函数的开头迭代了controls,所以它应该是以下内容:if (control.controls) { markFormGroupTouched(control); }
            • touched 只是意味着输入被模糊了一次。为了使错误出现,我还必须在我的控件上调用updateValueAndValidity()
            【解决方案10】:

            这段代码对我有用:

            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();
                });
              }
            }
            

            【讨论】:

              【解决方案11】:

              关于@masterwork 的回答。 我尝试了该解决方案,但是当函数尝试在 FormGroup 内递归挖掘时出现错误,因为在这一行传递了 FormControl 参数,而不是 FormGroup:

              control.controls.forEach(c =&gt; this.markFormGroupTouched(c));

              这是我的解决方案

              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();
                 }
               });
              }
              

              【讨论】:

                【解决方案12】:

                遍历表单控件并将它们标记为已触摸也可以:

                for(let i in this.form.controls)
                    this.form.controls[i].markAsTouched();
                

                【讨论】:

                • 谢谢你的解决方案非常好我唯一要添加的,因为 tslint 抱怨是这样的: for (const i in this.form.controls) { if (this.form.controls[i]) { this.form.controls[i].markAsTouched(); } }
                • 如果您的formGroup 包含其他formGroups,这将不起作用
                【解决方案13】:

                这是我实际使用的代码。

                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);
                        }
                    });
                }    

                【讨论】:

                  【解决方案14】:

                  我遇到了同样的问题,但我不想用处理这个问题的代码“污染”我的组件。特别是因为我需要以多种形式使用它,并且我不想在各种场合重复代码。

                  因此,我创建了一个指令(使用到目前为止发布的答案)。该指令修饰 NgForm 的onSubmit-Method:如果表单无效,它将所有字段标记为已触摸并中止提交。否则通常的 onSubmit-Method 会正常执行。

                  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);
                          };
                      }
                  }
                  

                  用法:

                  <form (ngSubmit)="submit()" appValidateOnSubmit>
                      <!-- ... form controls ... -->
                  </form>
                  

                  【讨论】:

                    【解决方案15】:

                    这是我的解决方案

                          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);
                          }
                    

                    【讨论】:

                      【解决方案16】:

                      我遇到了这个问题,但找到了“正确”的方法,尽管它不在我所找到的任何 Angular 教程中。

                      在您的 HTML 中,在 form 标记上,添加模板驱动表单示例使用的相同模板引用变量 #myVariable='ngForm'('hashtag' 变量),以及响应式表单示例使用的内容:

                      &lt;form [formGroup]="myFormGroup" #myForm="ngForm" (ngSubmit)="submit()"&gt;

                      现在您可以在模板中访问myForm.submitted,您可以使用它来代替(或补充)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>

                      知道myForm.form === myFormGroup 是真的……只要你不要忘记="ngForm" 部分。如果您单独使用#myForm,它将不起作用,因为 var 将设置为 HtmlElement 而不是驱动该元素的指令。

                      知道myFormGroup 在响应式表单教程中组件的打字稿代码中可见,但 myForm 不可见,除非您通过方法调用将其传递,例如 submit(myForm)submit(myForm: NgForm): void {...}。 (注意 NgForm 在打字稿中的标题大写,但在 HTML 中是驼峰式。)

                      【讨论】:

                        【解决方案17】:

                        我完全理解 OP 的挫败感。我使用以下内容:

                        实用功能

                        /**
                         * 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;
                        }
                        

                        用法

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

                        请注意,此函数尚不支持嵌套控件。

                        【讨论】:

                          【解决方案18】:

                          这是我的做法。我不希望在按下提交按钮(或触摸表单)之前显示错误字段。

                          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>

                          【讨论】:

                          • 当添加新的验证规则时,这个看起来会随着时间的推移而变得沉重。但我明白了。
                          【解决方案19】:
                          onSubmit(form: any): void {
                            if (!this.form) {
                              this.form.markAsTouched();
                              // this.form.markAsDirty(); <-- this can be useful 
                            }
                          }
                          

                          【讨论】:

                          • 刚刚尝试过,不知何故它不会触及子表单元素。必须编写手动标记所有子元素的循环。你知道为什么markAsTouched() 没有触及子元素吗?
                          • 您使用的是什么角度版本?
                          • Angular 版本是 2.1.0
                          • 看起来我找到了为什么 markAsTouched() 不标记子元素 - github.com/angular/angular/issues/11774 。 TL;DR:这不是错误。
                          • 是的,我现在想起来了。如果表单无效,您可以禁用提交按钮,
                          猜你喜欢
                          • 1970-01-01
                          • 2019-06-25
                          • 1970-01-01
                          • 2020-03-16
                          • 2019-07-23
                          • 2019-06-30
                          • 2019-07-27
                          • 2019-09-03
                          • 2012-12-04
                          相关资源
                          最近更新 更多