0%

Angular2 模板驱动表单校验

模板驱动表单,指的是通过html5标准校验的表单,优势在于使用起来简单,但要动态修改验证器、操纵控制器模型不是很方便。

Angular2对表单处理做了一系列封装(模板驱动表单以及响应式表单):

  1. 数据绑定

    这个自然不用说,使用ngModel可以双向绑定到组件里的对象字段。

  2. 控件状态检测

    Angular会自动根据控件状态加上相应的class,如果我们需要编辑input标签在不同状态下的样式,只需要在css里写相应的类就可以了。

    状态

    true时的css类

    false时的css类

    控件是否被访问过

    ng-touched

    ng-untouched

    控件值是否已经变化

    ng-dirty

    ng-pristine

    控件值是否有效

    ng-valid

    ng-invalid

  3. 表单校验

    模板驱动表单支持html5标准属性校验:

    • required:必填
    • minlength:最小长度
    • maxlength:最大长度
    • pattern:正则表达式校验

    另外支持自定义Validator.

    响应式表单内置了上面四种Validator,也可以自己扩展。

模板驱动表单相关指令封装在FormsModule模块中,app.module.ts里需要先导入:

import {FormsModule} from '@angular/forms';

@NgModule({
  declarations: [
    AppComponent,
    ...
  ],
  exports:[AppComponent],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    ...
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

FormsModule模块包含NgModule、NgModuleGroup、NgForm和InternalFormsSharedModule模块中包含的指令。

  • NgForm 标记一个表单
  • NgModelGroup 字段分组
  • NgModel 字段

InternalFormsSharedModule是Angular内部模块,FormsModule和ReactiveFormsModule都引用了它,所以可以不用显式引入,直接使用。

下面的例子演示了一个模板驱动表单,包括表单校验、字段分组、控件状态、数据绑定,以及自定义校验器。自定义校验器的功能是校验第二个密码是否与第一个密码相同。

自定义校验器:
repeat-password.directive.ts:

import {Directive, Input, OnChanges, SimpleChanges} from '@angular/core';
import {NG_VALIDATORS, FormControl, Validator, AbstractControl, ValidatorFn, NgModel} from "@angular/forms";
/**
 * 自定义指令,用于检验input标签的值是否跟指定input的值标签相同
 */
@Directive({
  selector: '[repeatPassword]',
  providers: [{provide: NG_VALIDATORS, useExisting: RepeatPasswordDirective, multi: true}]
})
export class RepeatPasswordDirective implements Validator,OnChanges{
  /**
   * 校验方法
   * @param c
   * @returns { {[p: string]: any}}
   */
  validate(c: AbstractControl): {[p: string]: any} {
    return verifyPassword(c,this.repeatPassword.control);
  }

  ngOnChanges(changes: SimpleChanges): void {
      this.repeatPassword=changes['repeatPassword'].currentValue;
  }

  /**
   * 通过属性传入另一个input标签的model
   * 名称与选择器一致,就不需要在使用的时候加额外的属性传入
   */
  @Input() repeatPassword:NgModel;
  constructor() { }


}
/**
 * 导出校验方法,供响应式表单使用
 * @param password1Controller
 * @returns {(currentControl:AbstractControl)=>{[p: string]: any}}
 */
export function repeatPassword(password1Controller:FormControl):ValidatorFn {
  return (currentControl: AbstractControl): {[key: string]: any} => {
    return verifyPassword(currentControl,password1Controller);
  };
}

function verifyPassword(currentControl: AbstractControl,password1Controller:FormControl):{[key: string]: any} {
    if(!password1Controller.valid) {
      console.log("密码1无效");
      return {password1InValid:{'errorMsg':''}}
    }
    if((!currentControl.untouched||currentControl.dirty)&&password1Controller.value!=currentControl.value) {
      return {passwordNEQ:{'errorMsg':'两次密码输入不一致!'}}
    }
}

创建指令后别忘了在app.module.ts里引入 :

@NgModule({
  declarations: [
    AppComponent,
    ...,
    RepeatPasswordDirective
  ],
  exports:[AppComponent],
  imports: [
    BrowserModule,
    FormsModule,
    HttpModule,
    ...
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

当然,如果使用ng g directive repeatPassword 命令创建指令,会自动添加。

模板:

  <!--(ngSubmit)绑定的表单提交事件,ajax不需要-->
<form #registerForm="ngForm" (ngSubmit)="doSubmit(registerForm.value)" >
  <div>
    <label for="userName">用户名:</label>
    <!--给input设置一个本地变量,可以读取errors显示错误信息-->
    <input type="text" id="userName" name="userName" [(ngModel)]="formData.userName" #userName="ngModel" required minlength="4">
    <div *ngIf="userName.errors && (userName.dirty || userName.touched)" class="error">
      <span [hidden]="!userName.errors.required">用户名必须输入</span>
      <span [hidden]="!userName.errors.minlength">用户名至少4位</span>
    </div>
  </div>
  <!--ngModelGroup指令可以给表单字段分组,值password是registerForm.value里该组的字段名,#passwordGroup是该组的本地变量名-->
  <fieldset ngModelGroup="passwordGroup" #passwordGroup="ngModelGroup" aria-required="true">
    <label for="password1">密码:</label>
    <input type="password" id="password1" name="password1" [(ngModel)]="formData.password1" #password1="ngModel"  required minlength="8">
    <label for="password2">重复密码:</label>
    <!--使用自定义的校验器,加入repeatPassword指令,传入第一个密码输入框的ngModel,即用#password1="ngModel"声明的password1-->
    <input type="password" id="password2" name="password2" [(ngModel)]="formData.password2" [repeatPassword]="password1">
    <span *ngIf="formErrors['passwordGroup.password2']" class="error">
      { { formErrors['passwordGroup.password2'] }} </span>
  </fieldset>

  <div>
    <label for="email">邮箱:</label>
    <input type="text" id="email" name="email" [(ngModel)]="formData.email" required pattern="[\w]+?@[\w]+?\.[a-z]+?">
    <!-- 可以通过表单的onValueChanged事件,读到当前的错误信息,写到指定字段里 -->
    <div *ngIf="formErrors.email" class="error">
      { { formErrors.email }}
    </div>
  </div>
  <div>
    <label>性别:</label>
    <input type="radio" name="sex" [(ngModel)]="formData.sex" value="male" checked="checked"> 男
    <input type="radio" name="sex" [(ngModel)]="formData.sex" value="female" > 女
  </div>
  <fieldset ngModelGroup="nameGroup" #nameGroup="ngModelGroup">
    <label>姓:</label>
    <input type="text" name="firstName" [(ngModel)]="formData.firstName" required><br />
    <label>名:</label>
    <input type="text" name="lastName" [(ngModel)]="formData.lastName">
  </fieldset>

  <button type="button" class="btn btn-default"
          [disabled]="!registerForm.valid" (click)="doSubmit(registerForm.value)">注册</button>
</form>

{ {registerForm.value|json}}

组件:

import {Component, OnInit, ViewChild, AfterViewInit} from "@angular/core";
import {NgForm} from "@angular/forms";

@Component({
  selector: 'app-form',
  templateUrl: './form.component.html',
  styleUrls: ['./form.component.css']
})
export class FormComponent implements OnInit,AfterViewInit {
  ngAfterViewInit(): void {
    //订阅表单值改变事件
    this.registerForm.valueChanges.subscribe(data => this.onValueChanged(data));
  }
  //找到表单
  @ViewChild('registerForm') registerForm: NgForm;


  constructor() {
  }

  formData = {} as any;
  ngOnInit() {
    //默认性别为male
    this.formData.sex = "male";

  }
  doSubmit(obj: any) {
    //表单提交
    console.log(JSON.stringify(obj));
  }

  onValueChanged(data) {

    for (const field in this.formErrors) {
      this.formErrors[field] = '';
      //取到表单字段
      const control = this.registerForm.form.get(field);
      //表单字段已修改或无效
      if (control && control.dirty && !control.valid) {
        //取出对应字段可能的错误信息
        const messages = this.validationMessages[field];
        //从errors里取出错误类型,再拼上该错误对应的信息
        for (const key in control.errors) {
          this.formErrors[field] += messages[key] + '';
        }
      }

    }

  }


  //存储错误信息
  formErrors = {
    'email': '',
    'userName': '',
    'passwordGroup.password1':'',
    'passwordGroup.password2':'',
    'sex':''
  };
  //错误对应的提示
  validationMessages = {
    'email': {
      'required': '邮箱必须填写.',
      'pattern': '邮箱格式不对',
    },
    'userName': {
      'required': '用户名必填.',
      'minlength': '用户名太短',
    },
    'passwordGroup.password1':{
      'required': '请输入密码',
      'minlength': '密码太短',
    },
    'passwordGroup.password2':{
      'required': '请重复输入密码',
      'minlength': '密码太短',
      'passwordNEQ':'两次输入密码不同',
      'password1InValid':''
    },
    'sex':{
      'required':'性别必填'
    }

  };
}