Angular 4.x 基於AbstractControl自定義表單驗證

Angular 爲咱們提供了多種方式和 API,進行表單驗證。接下來咱們將介紹如何利用 AbstractControl 實現 FormGroup 的驗證。文章中會涉及 FormGroupFormControlFormBuilder 的相關知識,所以建議不瞭解上述知識的讀者,閱讀本文前先閱讀 Angular 4.x Reactive Forms 這篇文章。html

Contents

  • What is a FormGroupreact

  • FormBuilder/FormGroup source codetypescript

  • AbstractControlsegmentfault

  • Custom validation propertiesangular2

    • Custom validation Object hook異步

What is a FormGroup

咱們先來看一下 Angular 4.x Reactive Forms 中,使用 FormBuilder 的示例:async

signup-form.component.ts函數

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { User } from './signup.interface';

@Component({...})
export class SignupFormComponent implements OnInit {
  user: FormGroup;
  constructor(private fb: FormBuilder) {}
           
  ngOnInit() {
    this.user = this.fb.group({
      name: ['', [Validators.required, Validators.minLength(2)]],
      account: this.fb.group({
        email: ['', Validators.required],
        confirm: ['', Validators.required]
      })
    });
  }

  onSubmit({ value, valid }: { value: User, valid: boolean }) {
    console.log(value, valid);
  }
}

上面示例中,咱們經過 FormBuilder 對象提供的 group() 方法,方便的建立 FormGroupFormControl 對象。接下來咱們來詳細分析一下 FormBuilder 類。ui

FormBuilder/FormGroup source code

FormBuilder source code

// angular2/packages/forms/src/form_builder.ts 片斷
@Injectable()
class FormBuilder {
  
  // 基於controlsConfig、extra信息,建立FormGroup對象
  group(controlsConfig: {[key: string]: any}, extra: 
      {[key: string]: any} = null): FormGroup {}
  
  // 基於formState、validator、asyncValidator建立FormControl對象
  control(
      formState: Object, validator: ValidatorFn|ValidatorFn[] = null,
      asyncValidator: AsyncValidatorFn|AsyncValidatorFn[] = null): FormControl {}
  
  //基於controlsConfig、validator、asyncValidator建立FormArray對象
  array(
      controlsConfig: any[], validator: ValidatorFn = null,
      asyncValidator: AsyncValidatorFn = null): FormArray {}
}

首先,咱們先來看一下 group() 方法:this

group(controlsConfig: {[key: string]: any}, extra: {[key: string]: any} = null): 
  FormGroup {}

group() 方法簽名中,能夠清楚的知道該方法的輸入參數和返回類型。具體的使用示例以下:

this.user = this.fb.group({
     name: ['', [Validators.required, Validators.minLength(2)]],
     account: this.fb.group({
        email: ['', Validators.required],
        confirm: ['', Validators.required]
     })
});

接下來咱們來看一下 group() 方法的內部實現:

group(controlsConfig: {[key: string]: any}, extra: {[key: string]: any} = null):     
     FormGroup {
        // 建立controls對象集合
        const controls = this._reduceControls(controlsConfig); 
       // 獲取同步驗證器
        const validator: ValidatorFn = extra != null ? extra['validator'] : null;
        // 獲取異步驗證器
        const asyncValidator: AsyncValidatorFn = extra != null ?
          extra['asyncValidator'] : null;
    return new FormGroup(controls, validator, asyncValidator);
  }

咱們在來看一下 _reduceControls() 方法的內部實現:

_reduceControls(controlsConfig: {[k: string]: any}): {[key: string]: AbstractControl} {
    const controls: {[key: string]: AbstractControl} = {};
    // controlsConfig - {name: [...], account: this.fb.group(...)}
    Object.keys(controlsConfig).forEach(controlName => {
      // 獲取控件的名稱,而後基於控件對應的配置信息,建立FormControl控件,並保存到controls對象上
      controls[controlName] = this._createControl(controlsConfig[controlName]);
    });
    return controls;
}

繼續看一下 _createControl() 方法的內部實現:

_createControl(controlConfig: any): AbstractControl {
    if (controlConfig instanceof FormControl || controlConfig instanceof FormGroup ||
        controlConfig instanceof FormArray) {
      return controlConfig;
    } else if (Array.isArray(controlConfig)) {
      // controlConfig - ['', [Validators.required, Validators.minLength(2)]]
      const value = controlConfig[0]; // 獲取初始值
      // 獲取同步驗證器
      const validator: ValidatorFn = controlConfig.length > 1 ? controlConfig[1] : null;
      // 獲取異步驗證器
      const asyncValidator: AsyncValidatorFn = controlConfig.length > 2 ? 
            controlConfig[2] : null;
      // 建立FormControl控件
      return this.control(value, validator, asyncValidator);
    } else {
      return this.control(controlConfig);
    }
  }

最後咱們看一下 control() 方法的內部實現:

control(
      formState: Object, 
      validator: ValidatorFn|ValidatorFn[] = null,
      asyncValidator: AsyncValidatorFn|AsyncValidatorFn[] = null): FormControl {
    return new FormControl(formState, validator, asyncValidator);
}

如今先來總結一下,經過分析 FormBuilder 類的源碼,咱們發現:

this.fb.group({...}, { validator: someCustomValidator })

等價於

new FormGroup({...}, someCustomValidator)

在咱們實現自定義驗證規則前,咱們在來介紹一下 FormGroup 類。

FormGroup source code

// angular2/packages/forms/src/model.ts  片斷
export class FormGroup extends AbstractControl {
  constructor(
      public controls: {[key: string]: AbstractControl}, 
      validator: ValidatorFn = null,
      asyncValidator: AsyncValidatorFn = null) {
    super(validator, asyncValidator);
    this._initObservables();
    this._setUpControls();
    this.updateValueAndValidity({onlySelf: true, emitEvent: false});
  }
}

經過源碼咱們發現,FormGroup 類繼承於 AbstractControl 類。在建立 FormGroup 對象時,會把 validatorasyncValidator 做爲參數,而後經過 super 關鍵字調用基類 AbstractControl 的構造函數。

AbstractControl

接下來咱們來看一下 AbstractControl 類:

// angular2/packages/forms/src/model.ts 片斷
export abstract class AbstractControl {
  _value: any;
  ...
  private _valueChanges: EventEmitter<any>;
  private _statusChanges: EventEmitter<any>;
  private _status: string;
  private _errors: ValidationErrors|null;
  private _pristine: boolean = true;
  private _touched: boolean = false;
 
  constructor(public validator: ValidatorFn, public asyncValidator: AsyncValidatorFn) {}
  // 獲取控件的valid狀態,用於表示控件是否經過驗證    
  get valid(): boolean { return this._status === VALID; } 

  // 獲取控件的invalid狀態,用於表示控件是否經過驗證
  get invalid(): boolean { return this._status === INVALID; } 

  // 獲取控件的pristine狀態,用於表示控件值未改變
  get pristine(): boolean { return this._pristine; } 

  // 獲取控件的dirty狀態,用於表示控件值已改變
  get dirty(): boolean { return !this.pristine; }

  // 獲取控件的touched狀態,用於表示控件已被訪問過
  get touched(): boolean { return this._touched; } 
  ...
}

使用 AbstractControl 不是實現咱們自定義 FormGroup 驗證的關鍵,由於咱們也能夠注入 FormGroup 來實現與表單控件進行交互。如今咱們再來觀察一下最初的代碼:

@Component({...})
export class SignupFormComponent implements OnInit {
  user: FormGroup;
  constructor(private fb: FormBuilder) {}
  ngOnInit() {
    this.user = this.fb.group({
      name: ['', [Validators.required, Validators.minLength(2)]],
      account: this.fb.group({
        email: ['', Validators.required],
        confirm: ['', Validators.required]
      })
    });
  }
}

接下來咱們要實現的自定義驗證規則是,確保 email 字段的值與 confirm 字段的值可以徹底一致。咱們能夠經過 AbstractControl 來實現該功能,首先咱們先來定義驗證函數:

email-matcher.ts

export const emailMatcher = () => {};

下一步,咱們須要注入 AbstractControl

export const emailMatcher = (control: AbstractControl): {[key: string]: boolean} => {
    
};

Angular 4.x Reactive Forms 文章中,咱們介紹了經過 FormGroup 對象 (FormGroup 類繼承於AbstractControl),提供的 get() 方法,能夠獲取指定的表單控件。get() 方法的簽名以下:

get(path: Array<string|number>|string): AbstractControl { return _find(this, path, '.'); }

// 使用示例 - 獲取sub-group的表單控件
this.form.get('person.name'); 
-OR-
this.form.get(['person', 'name']);

具體示例以下:

<div class="error" *ngIf="user.get('foo').touched && 
  user.get('foo').hasError('required')">
       This field is required
</div>

瞭解完 AbstractControl,接下來咱們來更新一下 emailMatcher 函數:

export const emailMatcher = (control: AbstractControl): {[key: string]: boolean} => {
  const email = control.get('email');
  const confirm = control.get('confirm');
};

上面的示例中,control 表示的是 FormGroup 對象,emailconfirm 都是表示 FormControl 對象。咱們能夠在控制檯中輸出它們的值:

► FormGroup {asyncValidator: null, _pristine: true, _touched: false, _onDisabledChange: Array[0], controls: Object…}
► FormControl {asyncValidator: null, _pristine: true, _touched: false, _onDisabledChange: Array[1], _onChange: Array[1]…}
► FormControl {asyncValidator: null, _pristine: true, _touched: false, _onDisabledChange: Array[1], _onChange: Array[1]…}

Custom validation properties

實際上 emailMatcher 自定義驗證規則,就是比較 emailconfirm 控件的值是否一致。若是它們的值是一致的,那麼返回 null,表示驗證經過,沒有出現錯誤。具體代碼以下:

export const emailMatcher = (control: AbstractControl): {[key: string]: boolean} => {
  const email = control.get('email');
  const confirm = control.get('confirm');
  if (!email || !confirm) return null;
  if (email.value === confirm.value) {
    return null;
  }
};

上述代碼意味着若是一切正常,咱們都不會返回任何錯誤。如今咱們須要添加自定義驗證。

Custom validation Object hook

咱們先來看一下,在 HTML 模板中,咱們自定義驗證規則的預期使用方式:

...
  <div formGroupName="account">
    <label>
      <span>Email address</span>
      <input type="email" placeholder="Your email address" formControlName="email">
    </label>
    <label>
      <span>Confirm address</span>
      <input type="email" placeholder="Confirm your email address" 
           formControlName="confirm">
    </label>
    <div class="error" *ngIf="user.get('account').touched && 
        user.get('account').hasError('nomatch')">
        Email addresses must match
    </div>
  </div>
...

忽略掉其它無關的部分,咱們只關心如下的代碼片斷:

user.get('account').hasError('nomatch')

這意味着,咱們須要先獲取 account 對象 (FormGroup實例),而後經過 hasError() 方法,判斷是否存在 nomatch 的錯誤。接下來咱們按照該需求更新 emailMatcher 函數,具體以下:

export const emailMatcher = (control: AbstractControl): {[key: string]: boolean} => {
  const email = control.get('email');
  const confirm = control.get('confirm');
  if (!email || !confirm) return null;
  return email.value === confirm.value ? null : { nomatch: true };
};

最後,咱們須要導入咱們的自定義驗證規則,而後在調用 fb.group() 建立 account FormGroup對象時,設置第二個參數,具體示例以下:

...
import { emailMatcher } from './email-matcher';
...
  ngOnInit() {
    this.user = this.fb.group({
      name: ['', Validators.required],
      account: this.fb.group({
        email: ['', Validators.required],
        confirm: ['', Validators.required]
      }, { validator: emailMatcher })
    });
  }
...

完整的示例代碼以下:

signup.interface.ts

export interface User {
  name: string;
  account: {
    email: string;
    confirm: string;
  }
}

email-matcher.ts

export const emailMatcher = (control: AbstractControl): {[key: string]: boolean} => {
  const email = control.get('email');
  const confirm = control.get('confirm');
  if (!email || !confirm) {
    return null;
  }
  return email.value === confirm.value ? null : { nomatch: true };
};

signup-form.component.ts

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { emailMatcher } from './email-matcher';

@Component({
  selector: 'signup-form',
  template: `
    <form class="form" novalidate (ngSubmit)="onSubmit(user)" [formGroup]="user">
      <label>
        <span>Full name</span>
        <input type="text" placeholder="Your full name" formControlName="name">
      </label>
      <div class="error" *ngIf="user.get('name').touched && 
        user.get('name').hasError('required')">
        Name is required
      </div>
      <div formGroupName="account">
        <label>
          <span>Email address</span>
          <input type="email" placeholder="Your email address" formControlName="email">
        </label>
        <label>
          <span>Confirm address</span>
          <input type="email" placeholder="Confirm your email address" 
            formControlName="confirm">
        </label>
        <div class="error" *ngIf="user.get('account').touched && 
            user.get('account').hasError('nomatch')">
          Email addresses must match
        </div>
      </div>
      <button type="submit" [disabled]="user.invalid">Sign up</button>
    </form>
  `
})
export class SignupFormComponent implements OnInit {
  user: FormBuilder;
  constructor(public fb: FormBuilder) {}
  ngOnInit() {
    this.user = this.fb.group({
      name: ['', Validators.required],
      account: this.fb.group({
        email: ['', Validators.required],
        confirm: ['', Validators.required]
      }, { validator: emailMatcher })
    });
  }
  onSubmit({ value, valid }) {
    console.log(value, valid);
  }
}

具體詳情,能夠查看線上示例

參考資源

相關文章
相關標籤/搜索