Angular 4 基于AbstractControl自定义表单验证

Angular 为我们提供了多种方式和 API,进行表单验证。接下来我们将介绍如何利用 AbstractControl 实现 FormGroup 的验证。文章中会涉及 FormGroupFormControlFormBuilder 的相关知识,因此建议不了解上述知识的读者,阅读本文前先阅读 Angular 4 Reactive Forms 这篇文章。

Contents

  • What is a FormGroup
  • FormBuilder/FormGroup source code
  • AbstractControl
  • Custom validation properties
    • Custom validation Object hook

What is a FormGroup

我们先来看一下 Angular 4 Reactive Forms 中,使用 FormBuilder 的示例:

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 类。

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() 方法:

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

具体详情,可以查看线上示例

参考资源

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 201,552评论 5 474
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,666评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 148,519评论 0 334
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,180评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,205评论 5 363
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,344评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,781评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,449评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,635评论 1 295
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,467评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,515评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,217评论 3 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,775评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,851评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,084评论 1 258
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,637评论 2 348
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,204评论 2 341

推荐阅读更多精彩内容

  • 当我们打算自定义表单控件前,我们应该先考虑一下以下问题: 是否已经有相同语义的 native (本机) 元素?如:...
    semlinker阅读 1,820评论 1 5
  • 表单是几乎每个 Web 应用程序的一部分。虽然 Angular 为我们提供了几个内置 validators (验证...
    semlinker阅读 2,253评论 0 1
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,570评论 18 139
  • Angular 4.x 中有两种表单: Template-Driven Forms - 模板驱动式表单 (类似于...
    semlinker阅读 1,239评论 0 2
  • Angular 4.x 中有两种表单: Template-Driven Forms - 模板驱动式表单 (类似于...
    semlinker阅读 977评论 0 2