angular2 formBuilder 组异步验证

Posted

技术标签:

【中文标题】angular2 formBuilder 组异步验证【英文标题】:angular2 formBuilder group async validation 【发布时间】:2017-10-23 09:52:37 【问题描述】:

我正在尝试实现异步验证器但没有成功...

我的组件创建了一个表单:

this.personForm = this._frmBldr.group(
  lastname:  [ '', Validators.compose([Validators.required, Validators.minLength(2) ]) ],
  firstname: [ '', Validators.compose([Validators.required, Validators.minLength(2) ]) ],
  birthdate: [ '', Validators.compose([ Validators.required, DateValidators.checkIsNotInTheFuture ]) ],
  driverLicenceDate: [ '', Validators.compose([ Validators.required, DateValidators.checkIsNotInTheFuture ]), this.asyncValidationLicenceDate.bind(this) ],
, 
  asyncValidator: this.validateBusiness.bind(this),
  validator: this.validateDriverLicenseOlderThanBirthdate,
);

我的验证方法

validateBusiness(group: FormGroup) 
  console.log('validateBusiness')
  return this._frmService
    .validateForm(group.value)
    .map((validationResponse: IValidationResponse) => 
      if (validationResponse) 
        validationResponse.validations.forEach( (validationError: IValidationErrorDescription) => 
                        let errorMsg = validationError.display;
                        let errorCode = validationError.code;
                        validationError.fields.forEach( (fieldName: string) => 
                            console.log(fieldName);
                            let control = this.personForm.controls[fieldName];
                            let existingErrors = control.errors || ;
                            existingErrors[errorCode] = errorMsg;
                            control.setErrors(existingErrors);
                        );
                    );
                
            );
    

所有验证都被成功调用,除了从未调用过的 validateBusiness 方法(在formbuilder.groupextra.asyncValidator 参数中)......有人可以告诉我我做错了什么吗?

发送

【问题讨论】:

【参考方案1】:

TL;DR:通过分析您的用例,您可能需要解决方案 2

问题

问题在于如何定义和使用异步验证器。

异步验证器定义为:

export interface AsyncValidatorFn 
    (c: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null>;

这是因为FormBuilder.group() 实际上是在调用FormGroup 构造函数:

constructor(controls: 
    [key: string]: AbstractControl;
, validator?: ValidatorFn | null, asyncValidator?: AsyncValidatorFn | null);

因此,异步验证器函数将接收AbstractControl 实例,在本例中为FormGroup 实例,因为验证器位于FormGroup 级别。验证器需要返回 PromiseObservableValidationErrors,如果不存在验证错误,则返回 null。

ValidationErrors 被定义为字符串键和值的映射(任何你喜欢的)。键实际上是定义验证错误类型的字符串(例如:“必需”)。

export declare type ValidationErrors = 
    [key: string]: any;
;

AbstractControl.setErrors()? - 在您的示例中,您正在定义一个不返回任何内容但实际上直接更改控制错误的函数。调用 setErrors 仅适用于手动调用验证并因此仅手动设置错误的情况。相反,在您的示例中,这些方法是混合的,FormControls 附加了将自动运行的验证功能,而FormGroup async 验证功能也会自动运行,它会尝试手动设置错误并因此设置有效性。这行不通。

您需要采用以下两种方法之一:

    附加将自动运行的验证函数,从而设置错误和有效性。不要尝试在附加了验证功能的控件上手动设置任何内容。 手动设置错误并因此设置有效性,而无需将任何验证函数附加到受影响的 AbstractControl 实例。

如果您想保持一切清洁,那么您可以实施单独的验证功能。 FormControl 验证将只处理一个控件。 FormGroup 验证会将表单组的多个方面视为一个整体。

如果您想使用验证服务,它实际上验证整个表单,就像您所做的那样,然后将每个错误委托给每个适当的控件验证器,那么您可以使用 解决方案 2。这有点难。

但是,如果您可以在FormGroup 级别使用您的验证服务的验证器,那么这可以使用解决方案 1 来实现。

解决方案 1 - 在 FormGroup 级别创建错误

假设我们要输入名字和姓氏,但名字需要与姓氏不同。并假设这个计算需要 1 秒。

模板

<form [formGroup]="personForm">
  <div>
    <input type="text" name="firstName" formControlName="firstName" placeholder="First Name" />
  </div>
  <div>
    <input type="text" name="lastName" formControlName="lastName" placeholder="Last Name" />
  </div>

  <p style="color: red" *ngIf="personForm.errors?.sameValue">First name and last name should not be the same.</p>

  <button type="submit">Submit</button>
</form>

组件

以下validateBusiness 验证函数将返回Promise

import  Component, OnInit  from '@angular/core';
import AbstractControl, FormBuilder, FormGroup, ValidationErrors, Validators from "@angular/forms";
import Observable from "rxjs/Observable";
import "rxjs/add/operator/delay";
import "rxjs/add/operator/map";
import "rxjs/add/observable/from";

@Component(
  selector: 'app-async-validation',
  templateUrl: './async-validation.component.html',
  styleUrls: ['./async-validation.component.css']
)
export class AsyncValidationComponent implements OnInit 

  personForm: FormGroup;

  constructor(private _formBuilder: FormBuilder)  

  ngOnInit() 

    this.personForm = this._formBuilder.group(
      firstName:  [ '', Validators.required ],
      lastName: [ '', Validators.required ],
    , 
      asyncValidator: this.validateBusiness.bind(this)
    );
  

  validateBusiness(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> 

    return new Promise((resolve, reject) => 
      setTimeout(() => 
          if (control.value.firstName !== control.value.lastName) 
            resolve(null);
          
          else 
            resolve(sameValue: 'ERROR...');
          
        ,
        1000);
    );
  

或者,验证函数可以返回Observable

  validateBusiness(control: AbstractControl): Promise<ValidationErrors | null> | Observable<ValidationErrors | null> 

    return Observable
      .from([control.value.firstName !== control.value.lastName])
      .map(valid => valid ? null : sameValue: 'ERROR...')
      .delay(1000);
  

解决方案 2 - 协调多个控件的验证错误

另一种选择是在表单更改时手动验证,然后将结果传递给一个 observable,供FormGroupFormControl 异步验证器使用。

我创建了一个POC here。

IValidationResponse

来自用于验证表单数据的验证服务的响应。

import IValidationErrorDescription from "./IValidationErrorDescription";

export interface IValidationResponse 
  validations: IValidationErrorDescription[];

IValidationErrorDescription

验证响应错误说明。

export interface IValidationErrorDescription 
  display: string;
  code: string;
  fields: string[];

BusinessValidationService

验证服务,实现表单数据验证业务。

import  Injectable  from '@angular/core';
import Observable from 'rxjs/Observable';
import 'rxjs/add/observable/from';
import 'rxjs/add/operator/map';
import IValidationResponse from "../model/IValidationResponse";

@Injectable()
export class BusinessValidationService 

  public validateForm(value: any): Observable<IValidationResponse> 
    return Observable
      .from([value.firstName !== value.lastName])
      .map(valid => valid ?
        validations: []
        :
        
          validations: [
            
              code: 'sameValue',
              display: 'First name and last name are the same',
              fields: ['firstName', 'lastName']
            
          ]
        
      )
      .delay(500);
  

FormValidationService

验证服务,用于为 FormGroupFormControl 构建异步验证器并订阅表单数据的更改,以便将验证委托给验证回调(例如:BusinessValidationService)。

它提供以下内容:

validateFormOnChange() - 当表单更改时,它会调用验证回调validateFormCallback,当它使用control.validateFormGroup() 触发FormGroupFormControls 的验证时。 createGroupAsyncValidator() - 为 FormGroup 创建一个异步验证器 createControlAsyncValidator() - 为 FormControl 创建一个异步验证器

代码:

import  Injectable  from '@angular/core';
import Observable from 'rxjs/Observable';
import 'rxjs/add/observable/from';
import 'rxjs/add/operator/switchMap';
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/share';
import 'rxjs/add/operator/debounceTime';
import AbstractControl, AsyncValidatorFn, FormGroup from '@angular/forms';
import ReplaySubject from 'rxjs/ReplaySubject';
import IValidationResponse from "../model/IValidationResponse";

@Injectable()
export class FormValidationService 

  private _subject$ = new ReplaySubject<IValidationResponse>(1);
  private _validationResponse$ = this._subject$.debounceTime(100).share();
  private _oldValue = null;

  constructor() 
    this._subject$.subscribe();
  

  public get onValidate(): Observable<IValidationResponse> 
    return this._subject$.map(response => response);
  

  public validateFormOnChange(group: FormGroup, validateFormCallback: (value: any) => Observable<IValidationResponse>) 
    group.valueChanges.subscribe(value => 
      const isChanged = this.isChanged(value, this._oldValue);
      this._oldValue = value;

      if (!isChanged) 
        return;
      

      this._subject$.next(validations: []);
      this.validateFormGroup(group);

      validateFormCallback(value).subscribe(validationRes => 
        this._subject$.next(validationRes);
        this.validateFormGroup(group);
      );
    );
  

  private isChanged(newValue, oldValue): boolean 
    if (!newValue) 
      return true;
    

    return !!Object.keys(newValue).find(key => !oldValue || newValue[key] !== oldValue[key]);
  

  private validateFormGroup(group: FormGroup) 
    group.updateValueAndValidity( emitEvent: true, onlySelf: false );

    Object.keys(group.controls).forEach(controlName => 
      group.controls[controlName].updateValueAndValidity( emitEvent: true, onlySelf: false );
    );
  

  public createControlAsyncValidator(fieldName: string): AsyncValidatorFn 
    return (control: AbstractControl) => 
      return this._validationResponse$
        .switchMap(validationRes => 
          const errors = validationRes.validations
            .filter(validation => validation.fields.indexOf(fieldName) >= 0)
            .reduce((errorMap, validation) => 
              errorMap[validation.code] = validation.display;
              return errorMap;
            , );

          return Observable.from([errors]);
        )
        .first();
    ;
  

  public createGroupAsyncValidator(): AsyncValidatorFn 
    return (control: AbstractControl) => 

      return this._validationResponse$
        .switchMap(validationRes => 
          const errors = validationRes.validations
            .reduce((errorMap, validation) => 
              errorMap[validation.code] = validation.display;
              return errorMap;
            , );

          return Observable.from([errors]);
        )
        .first();
    ;
  

AsyncFormValidateComponent 模板

定义firstNamelastName FormControls 内的personForm FormGroup。对于此示例,条件是 firstNamelastName 应该不同。

<form [formGroup]="personForm">
  <div>
    <label for="firstName">First name:</label>

    <input type="text"
           id="firstName"
           name="firstName"
           formControlName="firstName"
           placeholder="First Name" />

    <span *ngIf="personForm.controls['firstName'].errors?.sameValue">Same as last name</span>
  </div>
  <div>
    <label for="lastName">Last name:</label>

    <input type="text"
           id="lastName"
           name="lastName"
           formControlName="lastName"
           placeholder="Last Name" />

    <span *ngIf="personForm.controls['lastName'].errors?.sameValue">Same as first name</span>
  </div>

  <p style="color: red" *ngIf="personForm.errors?.sameValue">First name and last name should not be the same.</p>

  <button type="submit">Submit</button>
</form>

AsyncValidateFormComponent

用作示例的组件使用FrmValidationService 实现验证。由于providers: [FormValidationService],这个组件有它自己的服务实例。由于 Angular 分层注入器功能,一个注入器将与该组件关联,并且将为 AsyncValidateFormComponent 的每个实例创建此服务的一个实例。因此,能够以每个组件实例为基础跟踪该服务内部的验证状态。

import  Component, OnInit  from '@angular/core';
import FormBuilder, FormGroup, Validators from '@angular/forms';
import 'rxjs/add/operator/delay';
import 'rxjs/add/operator/map';
import 'rxjs/add/observable/from';
import FormValidationService from "../services/form-validation.service";
import BusinessValidationService from "../services/business-validation.service";

@Component(
  selector: 'app-async-validate-form',
  templateUrl: './async-validate-form.component.html',
  styleUrls: ['./async-validate-form.component.css'],
  providers: [FormValidationService]
)
export class AsyncValidateFormComponent implements OnInit 

  personForm: FormGroup;

  constructor(private _formBuilder: FormBuilder,
              private _formValidationService: FormValidationService,
              private _businessValidationService: BusinessValidationService) 
  

  ngOnInit() 
    this.personForm = this._formBuilder.group(
      firstName: ['', Validators.required, this._formValidationService.createControlAsyncValidator('firstName')],
      lastName: ['', Validators.required, this._formValidationService.createControlAsyncValidator('lastName')],
    , 
      asyncValidator: this._formValidationService.createGroupAsyncValidator()
    );

    this._formValidationService.validateFormOnChange(this.personForm, value => this._businessValidationService.validateForm(value));
  

AppModule

它使用ReactiveFormsModule 来处理FormBuilderFormGroupFormControl。还提供了BusinessValidationService

import  BrowserModule  from '@angular/platform-browser';
import  NgModule  from '@angular/core';
import FormsModule, ReactiveFormsModule from '@angular/forms';
import  HttpModule  from '@angular/http';

import  AppComponent  from './app.component';
import  AsyncValidateFormComponent  from './async-validate-form/async-validate-form.component';
import BusinessValidationService from "./services/business-validation.service";

@NgModule(
  declarations: [
    AppComponent,
    AsyncValidateFormComponent
  ],
  imports: [
    BrowserModule,
    FormsModule,
    ReactiveFormsModule,
    HttpModule
  ],
  providers: [
    BusinessValidationService
  ],
  bootstrap: [AppComponent]
)
export class AppModule  

【讨论】:

花了我一段时间来阅读您的解决方案 :) 但它帮助我找到了我的错误并纠正了它们。谢谢 @andreim Miam84 我遵循了第一个解决方案,但它对我不起作用,请您将解决方案放入 plunker 中

以上是关于angular2 formBuilder 组异步验证的主要内容,如果未能解决你的问题,请参考以下文章

在表单组中动态创建formcontrolname

从javascript函数调用angular2方法

表单验证不适用于 Ionic 2 中的 Angular 2 FormBuilder

没有 Bootstrap 库的 Angular Formbuilder Select 和 Dropdown

Angular2:父子组件通信

angular2表单验证本机警报[重复]