响应式表单 - 将字段标记为已触摸

137

我遇到了一个问题,不知道如何标记所有表单字段为已访问。主要的问题是,如果我没有触摸字段并尝试提交表单,则不会显示验证错误。我在控制器中有这段代码的占位符。
我的想法很简单:

  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() - Pankwood
21个回答

226

Angular 8开始,您可以直接使用

this.form.markAllAsTouched();

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

AbstractControl文档


3
如果某些控件似乎无法正常工作,那么它们可能不在该 FormGroup 中。 - Noumenon
2
我喜欢这种评论。其他100个评论都有30-50行代码来完成这个任务,然后出现了这个简单、干净且有效的评论! - Ste
无敌传奇!! - Rafael de Castro
这在 Angular 13 中也可以工作。谢谢 :) - ANANDAN SELVAGANESAN
这个解决方案不适用于HTML Select元素 - Brieg

180
以下函数递归遍历表单组中的控件并轻触它们。由于控件字段是一个对象,代码在表单组的控件字段上调用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);
          }
        });
      }

21
很遗憾,这在Internet Explorer上无法正常工作 :( 只需将 (<any>Object).values(formGroup.controls) 更改为Object.keys(formGroup.controls).map(x => formGroup.controls[x]) 即可(来源:https://dev59.com/HVgQ5IYBdhLWcg3wZDGd)。 - moi_meme
1
这对我来说非常有帮助,使用 FormGroup 和 FormControl,并想知道如何向用户显示他们没有触摸必填字段。谢谢。 - NAMS
@NAMS 没问题!我很高兴能帮到你 :] - masterwok
4
递归部分只有一个小问题。由于您在函数开始处已经迭代了 controls,因此应改为:if (control.controls) { markFormGroupTouched(control); } - zurfyx
4
"touched" 的意思是输入框被模糊化了一次。为了显示错误,我还必须在我的控件上调用 updateValueAndValidity() - adamdport

14

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

control.controls.forEach(c => 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

1
这个答案与@hovado的答案有何不同? - manjeet lama

9
循环遍历表单控件并将其标记为已触摸也可以起作用:
for(let i in this.form.controls)
    this.form.controls[i].markAsTouched();

1
谢谢,伙计,你的解决方案非常好,唯一需要补充的是因为tslint会抱怨,所以加上这个:for (const i in this.form.controls) { if (this.form.controls[i]) { this.form.controls[i].markAsTouched(); } } - Avram Virgil
1
如果您的 formGroup 包含其他的 formGroup,那么这种方法就不起作用了。 - adamdport

3

这是我的解决方案

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

2

我曾经遇到过这个问题,但是发现了一种“正确”的方法,尽管在任何我找到的Angular教程中都没有提到。

在你的HTML代码中,在form标签上添加与模板驱动表单示例使用的相同的模板引用变量#myVariable='ngForm'(“hashtag”变量),除了响应式表单示例使用的内容之外:

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

现在你可以在模板中访问myForm.submitted,你可以使用它来代替(或者除了)myFormGroup.controls.X.touched

<div *ngIf="myForm.submitted" class="text-error"> <span *ngIf="myFormGroup.controls.myFieldX.errors?.badDate">日期格式无效</span> <span *ngIf="myFormGroup.controls.myFieldX.errors?.isPastDate">日期不能为过去时间。</span> </div>

请注意,只要你不忘记="ngForm"部分,myForm.form === myFormGroup就是真的。如果你只使用#myForm,它将不起作用,因为该变量将被设置为HtmlElement而不是驱动该元素的指令。

请注意,根据响应式表单教程,myFormGroup在你的组件的typescript代码中是可见的,但是myForm并不可见,除非你通过方法调用将其传递进去,例如submit(myForm: NgForm): void {...}。(请注意,typescript中的NgForm是标题大写,HTML中是驼峰命名法。)


1
这是我实际使用的代码。

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


1
我遇到了同样的问题,但我不想在我的组件中添加处理此问题的代码。特别是因为我需要在许多表单中使用它,而且我不想在各种场合重复这些代码。
因此,我创建了一个指令(使用迄今为止发布的答案)。该指令装饰了NgForm的onSubmit方法:如果表单无效,则将所有字段标记为已触摸并中止提交。否则,通常的onSubmit方法会正常执行。
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>

1
这段代码对我来说有效:

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接