【问题标题】:angular2 formBuilder group async validationangular2 formBuilder 组异步验证
【发布时间】: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.group 的extra.asyncValidator 参数中)......有人可以告诉我我做错了什么吗?

发送

【问题讨论】:

    标签: angular validation angular2-forms angular2-formbuilder


    【解决方案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 级别。验证器需要返回 Promise 或 Observable 的 ValidationErrors,如果不存在验证错误,则返回 null。

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

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

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

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

    1. 附加将自动运行的验证函数,从而设置错误和有效性。不要尝试在附加了验证功能的控件上手动设置任何内容。
    2. 手动设置错误并因此设置有效性,而无需将任何验证函数附加到受影响的 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,供FormGroup 和FormControl 异步验证器使用。

    我创建了一个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

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

    它提供以下内容:

    • validateFormOnChange() - 当表单更改时,它会调用验证回调validateFormCallback,当它使用control.validateFormGroup() 触发FormGroup 和FormControls 的验证时。
    • 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 模板

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

    <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 来处理FormBuilder、FormGroup 和FormControl。还提供了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 中
    猜你喜欢
    • 2018-04-16
    • 2017-04-08
    • 2016-07-26
    • 2018-02-10
    • 2016-02-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-01-30
    相关资源
    最近更新 更多