Angular 2 响应式表单在提交时触发验证

Angular 2 Reactive Forms trigger validation on submit

有没有一种方法可以在提交时触发反应式表单的所有验证器,而不仅仅是 "dirty" 和 "touch" 事件?

原因是我们有一个非常大的表单,它不指示某个字段是否为必填项,用户可能会错过一些必需的控件,因此在提交时,预计所有将显示最终用户遗漏的无效字段。

我尝试使用

将表单标记为 "touched"
FormGroup.markAsTouched(true);

它起作用了,所以我也尝试将其标记为 "dirty"

FormGroup.markAsDirty(true);

但是 class 的 css 仍然是 "ng-pristine",

有没有办法从组件手动触发它,我尝试谷歌搜索它无济于事,提前谢谢你!

更新

我已经通过迭代 FormGroup.controls 并将其标记为 "dirty" 使其工作,但是是否有 "standard" 方法可以做到这一点。

时隔数月归来,在此分享根据大家的评论改进后的版本,仅供记录:

markAsTouched(group: FormGroup | FormArray) {
  group.markAsTouched({ onlySelf: true });

  Object.keys(group.controls).map((field) => {
    const control = group.get(field);
    if (control instanceof FormControl) {
      control.markAsTouched({ onlySelf: true });
    } else if (control instanceof FormGroup) {
      this.markAsTouched(control);
    }
  });
}

希望有用!

更新:Angular 8 引入了 FormGroup.markAllAsTouched() 并且它做到了! :D

我发现了一些可能感兴趣的东西:

在提交时,我设置了 submitAttempt = true 并将其放在应该进行验证的 div 中:

nickname.touched || nickname.dirty || (nickname.untouched && submitAttempt)

意思是: 如果没有被触及,我们尝试提交,错误显示。

这个可以通过markAsTouched(). Until PR #26812合并来完成,可以用

function markAllAsTouched(group: AbstractControl) {
  group.markAsTouched({onlySelf: true});
  group._forEachChild((control: AbstractControl) => markAllAsTouched(control));
}

您可以在 source code 中找到更多信息。

有多种方法可以解决这个问题。如果您有嵌套的表单组,@Splaktar 的答案将不起作用。因此,这是适用于嵌套表单组的解决方案。

Solution 1: Iterate through all formgroups and formcontrols and programmatically touch them to trigger validations.

模板代码:

<form [formGroup]="myForm" (ngSubmit)="onSubmit()" novalidate>
...
<button type="submit" class="btn btn-success">Save</button>
</form>

component.ts代码:

    onSubmit() {
        if (this.myForm.valid) {
            // save data
        } else {
            this.validateAllFields(this.myForm); 
        }
    }

validateAllFields(formGroup: FormGroup) {         
        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.validateAllFields(control);  
            }
        });
    }

Solution 2: Use a variable to check if the form has been submitted or not. FYI: The submitted field for the ngForm is currently being tested and will be included in future Angular versions. So there will not be a need to create your own variable.

component.ts代码

private formSubmitAttempt: boolean;

onSubmit() {
        this.formSubmitAttempt = true;
        if (this.myForm.valid) {
            console.log('form submitted');
        }
   }

模板代码:

<form [formGroup]="myForm" (ngSubmit)="onSubmit()" novalidate>
    <div class="form-group">
        <label class="center-block">
            Name:
            <input class="form-control" formControlName="name">
        </label>
        <div class="alert alert-danger" *ngIf="myForm.get('name').hasError('required') && formSubmitAttempt">
            Name is required
        </div>
        ...
</form>

这可以通过提供的示例 here 实现,您可以在其中使用 NgForm 指令:

<form [formGroup]="heroForm" #formDir="ngForm">

然后在您的验证消息中检查表单是否已提交:

<small *ngIf="heroForm.hasError('required', 'formCtrlName') && formDir.submitted">
  Required!
</small>

编辑:现在也提供了 { updateOn: 'submit'},但只有在表单上没有 required 时才有效,因为 required 无论如何最初都会显示。您可以通过检查字段是否已被触摸来抑制它。

// fb is 'FormBuilder'
this.heroForm = this.fb.group({
 // ...
}, { updateOn: 'submit'})

我的应用程序有很多表单和输入,所以我创建了各种自定义表单组件(用于普通文本输入、textarea 输入、选择、复选框等),这样我就不需要重复冗长 HTML/CSS 和表单验证 UI 逻辑无处不在。

除了 FormControl 状态(validtouched, 等) 来决定需要在 UI.

上显示哪些验证状态和消息(如果有的话)

这个解决方案

  • 不需要遍历表单的控件并修改它们的状态
  • 不需要向每个控件添加一些额外的 submitted 属性
  • ngSubmit 绑定的 onSubmit 方法中不需要任何额外的表单验证处理
  • 不将模板驱动的表单与响应式表单相结合

形式-base.component:

import {Host, Input, OnInit, SkipSelf} from '@angular/core';
import {FormControl, FormGroupDirective} from '@angular/forms';


export abstract class FormBaseComponent implements OnInit {

  @Input() id: string;
  @Input() label: string;
  formControl: FormControl;

  constructor(@Host() @SkipSelf()
              private formControlHost: FormGroupDirective) {
  }

  ngOnInit() {
    const form = this.formControlHost.form;
    this.formControl = <FormControl>form.controls[this.id];
    if (!this.formControl) {
      throw new Error('FormControl \'' + this.id + '\' needs to be defined');
    }
  }

  get errorMessage(): string {
    // TODO return error message based on 'this.formControl.errors'
    return null;
  }

  get showInputValid(): boolean {
    return this.formControl.valid && (this.formControl.touched || this.formControlHost.submitted);
  }

  get showInputInvalid(): boolean {
    return this.formControl.invalid && (this.formControl.touched || this.formControlHost.submitted);
  }
}

形式-text.component:

import {Component} from '@angular/core';
import {FormBaseComponent} from '../form-base.component';

@Component({
  selector: 'yourappprefix-form-text',
  templateUrl: './form-text.component.html'
})
export class FormTextComponent extends FormBaseComponent {

}

形式-text.component.html:

<label class="x_label" for="{{id}}">{{label}}</label>
<div class="x_input-container"
     [class.x_input--valid]="showInputValid"
     [class.x_input--invalid]="showInputInvalid">
  <input class="x_input" id="{{id}}" type="text" [formControl]="formControl">
  <span class="x_input--error-message" *ngIf="errorMessage">{{errorMessage}}</span>
</div>

用法:

<form [formGroup]="form" novalidate>
  <yourappprefix-form-text id="someField" label="Some Field"></yourappprefix-form-text>
</form>

如果我得到你想要的。您只想在每次提交时更新验证消息。最好的方法是存储控件状态的历史记录。

export interface IValdiationField {
  submittedCount: number;
  valid: boolean;
}
class Component {
   // validation state management
   validationState: Map<string, IValdiationField | number> = new Map();

   constructor() {
      this.validationState.set('submitCount', 0);
   }

   validationChecker(formControlName: string): boolean {

    // get submitted count
    const submittedCount: number = (this.validationState.get('submitCount') || 0) as number;

    // form shouldn't show validation if form not submitted
    if (submittedCount === 0) {
       return true;
    }

    // get the validation state
    const state: IValdiationField = this.validationState.get(formControlName) as IValdiationField;

    // set state if undefined or state submitted count doesn't match submitted count
    if (state === undefined || state.submittedCount !== submittedCount) {
       this.validationState.set(formControlName, { submittedCount, valid: this.form.get(formControlName).valid } );
       return this.form.get(formControlName).valid;
     }

        // get validation value from validation state managment
       return state.valid;
   }
   submit() {
     this.validationState.set('submitCount', (this.validationState.get('submitCount') as number) + 1);
   } 
}

然后在 html 代码 *ngIf="!validationChecker('formControlName')" 中显示错误信息。

"dirty"、"touched"、"submitted" 可以使用下一个方法组合:

<form [formGroup]="form" (ngSubmit)="doSomething()" #ngForm="ngForm">
<input type="text" placeholder="Put some text" formControlName="textField" required>
<div *ngIf="textField.invalid && (textField.dirty || textField.touched || ngForm.submitted)">
  <div *ngIf="textField.errors.required">Required!</div>
</div>
<input type="submit" value="Submit" />
</form>

现在有 updateOn:'submit' 选项,它会在提交时触发验证,使用如下:

this.myForm = new FormGroup({},{updateOn: ‘submit’});

使用开箱即用的验证器,最好的方法就是在用户点击提交时检查表单组是否有效。

markAllAsTouched() 然后可用于触发对表单每个字段的有效性检查:

Stackblitz example here.

// Component

loginForm: FormGroup;

ngOnInit() {
    this.loginForm = new FormGroup({
        username: new FormControl(null, [
            Validators.required,
            Validators.minLength(4)
        ]),
        password: new FormControl(null, [
            Validators.required,
            Validators.minLength(4)
        ]),
    });
}

submitLoginForm() {
    if (!this.loginForm.invalid) { // Checks form input validity
        // Form input is valid
        console.log('Valid login attempt - allow submission');
    } else {
        // Form input is not valid
        this.loginForm.markAllAsTouched(); // Trigger validation across form
        console.log('Invalid login attempt - block submission');
    }
}

// Template

<form id="login_wrapper" [formGroup]="loginForm" (ngSubmit)="submitLoginForm()">
    <h1>Log in</h1>
    <mat-form-field color="accent">
        <mat-label>Username</mat-label>
        <input matInput
            placeholder="Username"
            type="text"
            formControlName="username">
        <mat-error
            *ngIf="loginForm.controls.username.invalid && (loginForm.controls.username.dirty || loginForm.controls.username.touched)">
            Please enter a valid username
        </mat-error>
    </mat-form-field>
    <mat-form-field color="accent">
        <mat-label>Password</mat-label>
        <input matInput
            placeholder="Password"
            type="password"
            formControlName="password">
        <mat-error
            *ngIf="loginForm.controls.password.invalid && (loginForm.controls.password.dirty || loginForm.controls.password.touched)">
            Please enter a valid password
        </mat-error>
    </mat-form-field>
    <button id="login_btn"
        mat-flat-button
        color="primary"
        type="submit">Login</button>
</form>

有时:

  • 你不想添加 ngForm 只是为了知道控制器是否已经提交
  • 您想知道是否有任何控制器属于已提交的表单,即是否有已提交的祖先。

使用下面的函数,您可以将任何 Control 标记为已提交(FormControlFormGroupArrayGroup)并检查 Control 是否为或已提交 parent.

import { AbstractControl as AngularAbstractControl } from '@angular/forms';

const isSubmittedSymbol = Symbol('This control is flagged as submitted');

/**
 * Return true if the form control or it's parent has been flagged as submitted
 */
export function isSubmitted(control: AngularAbstractControl): boolean {
  if (control.parent) {
    return isSubmitted(control.parent);
  }
  return !!control[isSubmittedSymbol];
}

/**
 * Flag the form control
 */
export function setSubmitted(control: AngularAbstractControl, submitted: boolean = true) {
  control[isSubmittedSymbol] = submitted;
}

虚拟示例:

public onSubmit(){
 submitted(this.myFormGroup);
 // your logic
}

public onReset(){
 submitted(this.myFormGroup, false);
}

public isSubmitted(){
 isSubmitted(this.myFormGroup);
}

<form [formGroup]="myFormGroup">
        your form logic here
</form>
<button (click)="onSubmit()"
        [disabled]=isSubmitted()>
</button>

注意:由于 isSubmitted() 函数递归检查 parent 是否已提交,您当然可以将它用于 FormArray 中的 FormControl [=] 15=](或任何配置)。只要根 parent 会被标记为 isSubmitted,所有 children 实际上都会被标记为相同的。