反应形式-将字段标记为已触摸


82

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

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

Answers:


145

以下功能通过窗体组中的控件重复出现,并轻轻触摸它们。因为控件字段是一个对象,所以代码在表单组的控件字段上调用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);
      }
    });
  }

18
这个可悲的是没有在Internet Explorer中不工作:(只需更改(<any>Object).values(formGroup.controls)Object.keys(formGroup.controls).map(x => formGroup.controls[x])(从stackoverflow.com/questions/42830257/...
moi_meme

1
这对我使用FormGroup和FormControl带来了巨大的帮助,并且想知道如何向用户显示他们没有触摸必填字段。谢谢。
NAMS

@NAMS没问题!我很高兴能对您有所帮助:]
masterwok

4
+1递归部分只有一个小问题。您已经controls在函数的开始处进行迭代,因此应改为以下内容:if (control.controls) { markFormGroupTouched(control); }
zurfyx

3
touched只是意味着输入被模糊了一次。为了使错误出现,我还必须调用updateValueAndValidity()控件。
adamdport

100

Angular 8/9中,您可以简单地使用

this.form.markAllAsTouched();

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

抽象控件文档


2
这应该是采用了棱角分明8那些公认的答案
雅各布·罗伯茨

1
这是一个更简单干净的解决方案。
HDJEMAI

1
这是角度8及更高角度的推荐解决方案,太好了!
阮阮

1
如果这似乎不适用于某些控件,则它们可能不在该FormGroup中。
Noumenon

11

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


8

循环浏览表单控件并将其标记为已触摸也可以:

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

1
如果您formGroup包含其他formGroups,则此方法不起作用
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'除了使用Reactive Forms示例外,还添加与Template-Driven Forms示例所使用的相同的Template Reference Variable (模板引用变量)(“ 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">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而不是Directive驱动该元素。

myFormGroup根据Reactive Forms教程,您可以在Component的打字稿代码中看到该可见性,但myForm要知道这一点,除非您通过诸如submit(myForm)to的方法调用将其传递给您submit(myForm: NgForm): void {...}。(注意NgForm在打字稿的标题中,但在HTML中为驼峰式。)


1
onSubmit(form: any): void {
  if (!this.form) {
    this.form.markAsTouched();
    // this.form.markAsDirty(); <-- this can be useful 
  }
}

只是尝试了一下,以某种方式它不会触及子表单元素。必须编写手动标记所有子元素的循环。您有什么线索为什么markAsTouched()不触及子元素?
GiedriusKiršys's

您正在使用什么角度版本?
Vlado Tesanovic

Angular版本为2.1.0
GiedriusKiršys16

1
看起来我发现了为什么markAsTouched()不标记子元素-github.com/angular/angular/issues/11774。TL; DR:这不是错误。
GiedriusKiršys's

1
是的,我现在记得。如果表单无效,则可以禁用“提交”按钮,<button [disable] =“!this.form”>提交</ button>
Vlado Tesanovic,2016年

1

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

因此,我创建了一条指令(使用到目前为止发布的答案)。指令装饰NgForm的-Method onSubmit:如果表单无效,则将所有字段标记为已触摸并中止提交。否则,通常的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>

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

该代码对我有用:

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

1

无递归的解决方案

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

 /**
  * 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上使用。

您可以在此处试用它:触碰的角标记


@VladimirPrudnikov问题是,当递归调用函数时,通常会有更多的开销。因此,CPU将花费更多时间处理调用堆栈。使用循环时,CPU将花费大部分时间执行算法本身。递归的优点是代码通常更具可读性。因此,如果性能不是问题,我会说您可以坚持使用递归。
亚瑟席尔瓦

“过早的优化是万恶之源。”
Dem Pilafian

@DemPilafian我同意这句话。但是,它不适用于此处,因为如果有人使用此线程,他们将能够免费获得优化的解决方案(无需花时间)。而且,顺便说一句,就我而言,我确实有理由对其进行优化=)
Arthur Silva

1

按照@masterwork

角度版本8的打字稿代码

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

0

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

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>


当添加新的验证规则时,随着时间的流逝,它看起来可能会变得繁重。但是我明白了。
GiedriusKiršys's

0

我完全理解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.
}

请注意,此功能尚不能满足嵌套控件的需要。


0

看到这个宝石。到目前为止,我所见过的最优雅的解决方案。

完整代码

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

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

0

视图:

<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 

0

我制作了一个版本,对给出的答案进行了一些更改,对于那些使用的版本比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
 }
}
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.