Angular4 - 表单

1. 纯html5 表单

纯HTML5表单提供功能如下
显示表单项
校验用户输入

提交表单数据

先看一个纯html5表单的例子:
<form action="/register" method="post">
  <div>UserName: <input type="text"></div>
  <div>TelePhone: <input type="text"></div>
  <div>Password: <input type="password"></div>
  <div>datetime: <input type="month"></div>
  <div>ConfimrPassword: <input type="password"></div>
  <button type="submit">Register</button>
</form>
对于一个SPA应用来说,一般我们会需要我们的表单具有以下的几个功能:
a. 每一个输入字段都可以独立的指定校验规则
b. 如果输入内容不符合校验规则,应该对应的字段应该演示错误信息(用户可以明确理解的)
c. 彼此依赖的字段应该一起校验(上面例子中的密码和确认密码)
d. 用户的输入可以提交到服务端,并且在提交之前应用可以校验用户的输入以及格式化用户的输入。
e. 应用可以控制表单如何提交到服务,可以是默认的http请求,或者ajax异步请求,或者是websocket请求方式。


对于上面的例子对于上面的几个要求,可以部分满足前两个要求,使用html5的表单属性去进行验证,例如required, pattern(title显示错误的信息),和使用input标签的type属性来规范用户的输入格式。

为什么说是部分呢,举一个例子假如用户需要输入自己所在的城市,这个其实是需要和相关数据一起验证,此时这个验证单靠纯html5就无法实现了。(当然,就算是angular表单也是需要html与js集成封装去完成验证,但是不需要开发人员去做相关的处理了)。

还有最重要的一点,对于html5表单属性的浏览器兼容性,其实并不是所有的表单属性在各大浏览器都可以使用的,在这里可以使用HTML5测试 – 你的浏览器能有多好的支持HTML5查看属性的支持。


2. Angular表单的分类

模板式表单

表单的数据模型是通过组件模板中的相关指令来定义的,因为使用这种凡是定义的表单数据模型时,我们首先于HTML的语法,所以,模板驱动范式只适合简单的场景。

响应式表单

使用响应式表单时,你通过编写TypeScript的代码而不是html代码来创建一个底层的数据模型,在这个模型定义好之后,使用一些特定的指令,将木板上的html元素与底层的数据模型链接在一起。

3. Angular两种表单的不同

a.表单数据模型的不同 

模板式表单的数据模型是由angular基于组件模板中的指令隐式创建的。响应式表单时通过代码明确的创建数据模型,然后将木板上的html元素与底层的数据模型链接在一起。

b. 表单数据模型的访问

数据模型并不是一个任意的对象,它是由angular/forms模块中的一些特性的类,如FormControl,FormGroup,FormArray等组成的。在模板式表单中,是不能直接访问到这些累的,但是在响应式表单中是可以的。

c. 响应式表单不会生成html,模板仍然需要自己来编写。(模板式表单同样)。

d. 引入的模块不同

模板式表单引入FormsModule,响应式表单引入ReactiveFormsModule.

e. 异步 vs. 同步(摘抄与官方文档)

响应式表单是同步的。模板驱动表单是异步的。这个不同点很重要。

使用响应式表单,我们会在代码中创建整个表单控件树。 我们可以立即更新一个值或者深入到表单中的任意节点,因为所有的控件都始终是可用的。

模板驱动表单会委托指令来创建它们的表单控件。 为了消除“检查完后又变化了”的错误,这些指令需要消耗一个以上的变更检测周期来构建整个控件树。 这意味着在从组件类中操纵任何控件之前,我们都必须先等待一个节拍。

f. 模板中访问数据(本地模板变量)

模板式表单可以通过本地模板量访问,但是响应式表单不行

4. 模板式表单

模板式表单是使用指令来定义数据模型,可以使用NgForm,NgModel,NgModelGroup指令。


(1) NgForm

Angular会在<form>标签上自动创建并附加一个NgForm指令。那么和原先的form表单有什么不同呢?

首先引进FormsModule,这个时候当我们在点击提交button的时候,form表单不会在提交这个表单。因为现在整个form的处理都已经交给angular了。angular会拦截自动提交事件,因为form表单的自动提交会造成页面的刷新,对于单页面来说,这很不友好,所以会通过自定义的事件ngSubmit来进行表单的提交。

然后我们先将上面的表单改写一下:

<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)">
  <div>UserName: <input type="text"></div>
  <div>TelePhone: <input type="text"></div>
  <div>Password: <input type="password"></div>
  <div>ConfimrPassword: <input type="password"></div>
  <button type="submit">Register</button>
</form>
<div>
  {{myForm.value | json}}
</div>

div是将form的值打出来,便于我们观察,这里使用本地模板变量(前面模板语法说过)来访问form。

NgForm会隐式的创建FormGroup这个类的实例,这个类用来代表表单的数据模型。

它会控制那些带有ngModel指令和name属性的元素,监听他们的属性(包括其有效性)。 它还有自己的valid属性,这个属性只有在它包含的每个控件都有效时才是真。

如果不想让angular处理form表单,需要在form上标明ngNoForm

<form action="/register" method="post" ngNoForm>

(2) NgModel

但是在上面的例子中当我们填入input内容的时候,内容却不会打出来,这是因为这个input还没有假如数据模型中去,这时候我们就需要NgModel指令了。

在angular表单API中NgModel指令代表表单中一个字段,这个指令会隐式的创建一个FormControl类的实例,来代表字段的数据类型,并用这个FormControl来存储字段的值。然后我们在改写一下表单

<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)">
  <div>UserName: <input ngModel #name="ngModel" name="username" type="text"></div>
  <div>TelePhone: <input ngModel name="phone" type="text"></div>
  <div>Password: <input ngModel name="password" type="password"></div>
  <div>ConfimrPassword:  <input  ngModel name="confimrPassword" type="password"></div>
  <button type="submit">Register</button>
</form>
<div>
  {{myForm.value | json}}
</div>
这时候页面的输入会打出来,可以使用#name="ngModel"本地模板变量来访问具体的字段的值。

(3) NgModelGroup

代表表单的一部分,与NgForm一样也会创建FormGroup的实例。会在NgForm.value中以嵌套的形式存在。

用于我们去形成表单的层次关系。

改写form如下:

<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)">
  <div ngModelGroup="userInfo">
    <div>UserName: <input ngModel name="username" type="text"></div>
    <div>TelePhone: <input ngModel name="phone" type="text"></div>
  </div>
  <div>Password: <input ngModel name="password" type="password"></div>
  <div>ConfimrPassword:  <input  ngModel name="confimrPassword" type="password"></div>
  <button type="submit">Register</button>
</form>
<div>
  {{myForm.value | json}}
</div>
看一下页面:


(4) 重构表单

<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)">
  <div>UserName: <input ngModel name="username" type="text"></div>
  <div>TelePhone: <input ngModel name="phone" type="text"></div>
  <div ngModelGroup="passwordGroup">
    <div>Password: <input ngModel name="password" type="password"></div>
    <div>ConfimrPassword:  <input  ngModel name="confimrPassword" type="password"></div>
  </div>
  <button type="submit">Register</button>
</form>
将password形成一个group便于我们进行验证,然后控制台打出提交的value:

具体的验证放在后面了解。

5. 响应式表单

创建响应式表单的两个步骤
a. 创建数据模型

b. 使用指令将数据模型链接到html上

创建数据模型有定义在angular中三个类构成FormControl,FormGroup,FormArray

AbstractControl是三个具体表单类的抽象基类。 并为它们提供了一些共同的行为和属性,其中有些是可观察对象(Observable)。

FormControl 用于跟踪一个单独的表单控件的值和有效性状态。它对应于一个HTML表单控件,比如输入框和下拉框。

FormGroup用于 跟踪一组AbstractControl的实例的值和有效性状态。 该组的属性中包含了它的子控件。 组件中的顶级表单就是一个FormGroup。

FormArray用于跟踪AbstractControl实例组成的有序数组的值和有效性状态。

//template
<form [formGroup]="formModel" (ngSubmit)="onSubmit()">
  <div>UserName: <input formControlName="username" type="text"></div>
  <div>TelePhone: <input formControlName="phone" type="text"></div>
  <div formGroupName="passwordGroup">
    <div>Password: <input formControlName="password" type="password"></div>
    <div>ConfimrPassword:  <input  formControlName="confimrPassword" type="password"></div>
  </div>
  <button type="submit">Register</button>
</form>
//ts
import { Component, OnInit } from '@angular/core';
import {FormControl, FormGroup} from '@angular/forms';

@Component({
  selector: 'app-template-form',
  templateUrl: './template-form.component.html',
  styleUrls: ['./template-form.component.css']
})
export class TemplateFormComponent implements OnInit {
  formModel: FormGroup;

  constructor() { }

  ngOnInit() {
    this.formModel = new FormGroup({
      username: new FormControl(),
      phone: new FormControl(),
      passwordGroup : new FormGroup({
        password: new FormControl(),
        confimrPassword: new FormControl(),
      })
    });
  }

  onSubmit() {
    console.log(this.formModel.value);
  }
}
接下来会以这个例子进行学习。


(1)FormGroup

FormGroup中有两个指令,FormGroup和FormGroupName.

formGroup是一个响应式表单的指令,它拿到一个现有FormGroup实例,并把它关联到一个HTML元素上。 这种情况下,它关联到的是form元素上的FormGroup实例。

FormGroupName可以将已有的表单组合绑定到一个DOM元素上,它仅作为FormGroupDirective指令的子元素使用。如上面的passwordGroup。对应模板式表单中的ngModelGroup。


(2) FormControl

FormControl类中有两个指令,FormControl和FormControlName.

formControlName 指令,绑定我们创建的 FormControl 控件到html元素上,用法请看上面例子。

FormControl:FormControlDirective指令可以将一个已有的FormControl控件绑定到一个DOM元素。

FormControl指令不能使用到FormGroup指令中去。以前没有父FormGroup的时候,[formControl]="name"也能正常工作,因为该指令可以独立工作,也就是说,不在FormGroup中时它也能用。 有了FormGroup,name输入框就需要再添加一个语法formControlName=name,以便让它关联到类中正确的FormControl上。 这个语法告诉Angular,查阅父FormGroup,然后在这个FormGroup中查阅一个名叫name的FormControl。但是不能再FormGroup中使用FormControl.


(3) FormArray

FormArray只有一个指令FormArrayName。用于操控FormGroup中的数组。

先看一个例子:我想在提交表单的时候按照用户的自定义来确定由多少个email需要提交到server。这个时候就需要用到FormArrayName。

HTML模板显示单个的地址FormGroup。 我们要把它修改成能显示0、1或更多个表示英雄地址的FormGroup。

要改的部分主要是把以前表示地址的HTML模板包裹进一个<div>中,并且使用*ngFor来重复渲染这个<div>。

诀窍在于要知道如何编写*ngFor。主要有三点:

a. 在*ngFor的<div>之外套上另一个包装<div>,并且把它的formArrayName指令设为"secretLairs"。 这一步为内部的表单控件建立了一个FormArray型的secretLairs作为上下文,以便重复渲染HTML模板。
b. 这些重复条目的数据源是FormArray.controls而不是FormArray本身。 每个控件都是一个FormGroup型的地址对象,与以前的模板HTML所期望的格式完全一样。
c. 每个被重复渲染的FormGroup都需要一个独一无二的formGroupName,它必须是FormGroup在这个FormArray中的索引。 我们将复用这个索引,以便为每个地址组合出一个独一无二的标签。
//template
<form [formGroup]="formModel" (ngSubmit)="onSubmit()">
  <div formGroupName="dateRange">
    <div>start time: <input formControlName="from" type="date"></div>
    <div>end time:  <input  formControlName="to" type="date"></div>
  </div>
  <div>
    <ul formArrayName="emails">
      <li *ngFor="let e of formModel.get('emails').controls; let i = index">
        <input type="text" [formControlName]="i">
      </li>
    </ul>
    <button type="button" (click)="addEmail()">Add Email</button>
  </div>


  <button type="submit">Submit</button>
</form>
//ts
import { Component, OnInit } from '@angular/core';
import {FormArray, FormControl, FormGroup} from '@angular/forms';

@Component({
  selector: 'app-template-form',
  templateUrl: './template-form.component.html',
  styleUrls: ['./template-form.component.css']
})
export class TemplateFormComponent implements OnInit {
  formModel: FormGroup = new FormGroup({
    dateRange: new FormGroup({
      from: new FormControl(),
      to: new FormControl(),
    }),
    emails: new FormArray([
      new FormControl('[email protected]'),
      new FormControl('[email protected]')
    ])
  });

  constructor() { }

  ngOnInit() {
  }

  onSubmit() {
    console.log(this.formModel.value);
  }

  addEmail() {
    (this.formModel.get('emails') as FormArray).push(new FormControl());
  }
}

假如我是用户,我在添加两个email,然后点击提交,看一些页面输出:




* 在ts代码的编写中。对于FormArray中的FormControl只能通过索引来访问,所以template的formControlName的值是索引值i。
* this.formModel.get('emails') 方式获取FormGroup中的FormControl字段或者FormGroup数组。

(4) FormBuilder

再回到原先的例子中去,现在我们看到走到现在响应式表单的代码比模板式表单的代码要多。我们可以使用FormBuild去简化代码。

FormBuilder类能通过处理控件创建的细节问题来帮我们减少重复劳动。

先看一下FormBuild内部的方法:


上面的方法分别实现FormGroup,FromControl,FormArray。

先改写一下:

import { Component, OnInit } from '@angular/core';
import {FormBuilder, FormControl, FormGroup} from '@angular/forms';

@Component({
  selector: 'app-template-form',
  templateUrl: './template-form.component.html',
  styleUrls: ['./template-form.component.css']
})
export class TemplateFormComponent implements OnInit {
  formModel: FormGroup;

  constructor(private formBuild: FormBuilder) { }

  ngOnInit() {
    this.formModel = this.formBuild.group({
      username: this.formBuild.control(''),
      phone: this.formBuild.control(''),
      passwordGroup : this.formBuild.group({
        password: this.formBuild.control(''),
        confimrPassword: this.formBuild.control(''),
      })
    });
  }

  onSubmit() {
    console.log(this.formModel.value);
  }
}
但是这样看起来还是不太简单,反而更复杂了,this.formBuild.control()在formBuild中允许我们使用数组进行创建,如下:
import { Component, OnInit } from '@angular/core';
import {FormBuilder, FormControl, FormGroup} from '@angular/forms';

@Component({
  selector: 'app-template-form',
  templateUrl: './template-form.component.html',
  styleUrls: ['./template-form.component.css']
})
export class TemplateFormComponent implements OnInit {
  formModel: FormGroup;

  constructor(private formBuild: FormBuilder) { }

  ngOnInit() {
    this.formModel = this.formBuild.group({
      username: [],
      phone: [],
      passwordGroup : this.formBuild.group({
        password: [],
        confimrPassword: [],
      })
    });
  }

  onSubmit() {
    console.log(this.formModel.value);
  }
}
终于看起来简单多了,对于这些方法的用法说两点:
group方法允许在多加一个参数,用于对于整个表单的校验。
[]内部的三个参数,第一个为初始值,第二个参数为校验方法,第三个为异步的校验方法。具体的校验方式后面会说到。

(5) 使用setValue和patchValue来操纵表单模型

a. setValue

setValue方法会在赋值给任何表单控件之前先检查数据对象的值。

它不会接受一个与FormGroup结构不同或缺少表单组中任何一个控件的数据对象。 这种方式下,如果我们有什么拼写错误或控件嵌套的不正确,它就能返回一些有用的错误信息。 patchValue会默默地失败。而setValue会捕获错误,并清晰的报告它。

this.formModel.setValue({
  username: '1',
  phone: 1,
  passwordGroup: {
    password: 1,
    confimrPassword: 1
  }
});

b. patchValue

借助patchValue,我们可以通过提供一个只包含要更新的控件的键值对象来把值赋给FormGroup中的指定控件。 但是和setValue不同,patchValue不会检查缺失的控件值,并且不会抛出有用的错误信息。
this.formModel.patchValue({
  username: '1'
});

c. 什么时候设置表单的模型值(ngOnChanges)

设置表单的模型值在生命周期钩子ngOnChanges。

6. 表单校验

(1) 响应式表单的验证

a. 内置验证器

class Validators {
  static min(min: number): ValidatorFn
  static max(max: number): ValidatorFn
  static required(control: AbstractControl): ValidationErrors | null
  static requiredTrue(control: AbstractControl): ValidationErrors | null
  static email(control: AbstractControl): ValidationErrors | null
  static minLength(minLength: number): ValidatorFn
  static maxLength(maxLength: number): ValidatorFn
  static pattern(pattern: string | RegExp): ValidatorFn
  static nullValidator(c: AbstractControl): ValidationErrors | null
  static compose(validators: (ValidatorFn | null | undefined)[] | null): ValidatorFn | null
  static composeAsync(validators: (AsyncValidatorFn | null)[]): AsyncValidatorFn | null
}
我们还是将上面的例子拿下来:
this.formModel = this.formBuild.group({
  username: ['', [Validators.required, Validators.minLength(6)]],
  phone: ['', Validators.compose([Validators.required, Validators.minLength(4)])],
  passwordGroup : this.formBuild.group({
    password: [],
    confimrPassword: [],
  })
});
这里就简单的举个例子,可以看到FormBuild的comtrol方法内部的第二个参数集成了Validators.compose的方法。

b. 验证状态码

valid - 表单控件有效
invalid - 表单控件无效
pristine - 表单控件值未改变
dirty - 表单控件值已改变
touched - 表单控件已被访问过
untouched - 表单控件未被访问过
errors - 表单控件校验错误的信息

pending - 进行异步校验的时候

* formModel的valid属性依赖于所有的控件的valid属性,所有的valid属性都为true的时候,表单的valid属性才是true。

 this.formModel.get('username').errors

c. 自定义同步验证

export class TemplateFormComponent implements OnInit {
  formModel: FormGroup;

  mobileValidator(control: FormControl): any {
    let myreq = /^(((13[0-9])|(14[0-9])|(15([0-9]))|(18[0-9]))+\d{8})$/;
    let valid = myreq.test(control.value);
    console.log('phone的校验结果是:' + valid);
    return valid ? null : {phone: true};
  }

  constructor(private formBuild: FormBuilder) { }

  ngOnInit() {
    this.formModel = this.formBuild.group({
      username: ['', [Validators.required, Validators.minLength(6)]],
      phone: ['', [this.mobileValidator]],
      passwordGroup : this.formBuild.group({
        password: [],
        confimrPassword: [],
      })
    });
  }

  onSubmit() {
  }
}
然后看一下界面的校验:


如图,每一次的输入都会有校验,直到最后的字母输入进来,才返回true。
* 自定义校验的返回值null的时候是证明校验成功。

对于上面的password group的验证如下:
export class TemplateFormComponent implements OnInit {
  formModel: FormGroup;
  
  equalValidator(group: FormGroup): any {
    let password: FormControl = group.get('password') as FormControl;
    let confirmPassword: FormControl = group.get('confimrPassword') as FormControl;
    let valid: boolean = (password.value === confirmPassword.value);
    console.log('password的校验结果是:' + valid);
    return valid ? null : {equal: true};
  }

  constructor(private formBuild: FormBuilder) { }

  ngOnInit() {
    this.formModel = this.formBuild.group({
      username: ['', [Validators.required, Validators.minLength(6)]],
      phone: ['', []],
      passwordGroup : this.formBuild.group({
        password: [],
        confimrPassword: [],
      }, {validator: this.equalValidator})
    });
  }

  onSubmit() {
  }
}
这里的用法是在group的第二个方法中传入自定义的校验器,并且格式是这样的{ validator: this.equalValidator}



d. 自定义异步校验器

angular允许校验器通过server异步校验用户的输入。其实写法和同步校验器相同,只是在内部我们可以通过向server发送请求验证。
export class TemplateFormComponent implements OnInit {
  formModel: FormGroup;

  mobileValidator(control: FormControl): any {
    let myreq = /^(((13[0-9])|(14[0-9])|(15([0-9]))|(18[0-9]))+\d{8})$/;
    let valid = myreq.test(control.value);
    console.log('phone的校验结果是:' + valid);
    return valid ? null : {phone: true};
  }

  mobileAsynxValidator(control: FormControl): any {
    let myreq = /^(((13[0-9])|(14[0-9])|(15([0-9]))|(18[0-9]))+\d{8})$/;
    let valid = myreq.test(control.value);
    return Observable.of(valid ? null : {phone: true}).delay(5000);
  }

  constructor(private formBuild: FormBuilder) { }

  ngOnInit() {
    this.formModel = this.formBuild.group({
      username: ['', [Validators.required, Validators.minLength(6)]],
      phone: ['', [this.mobileValidator], this.mobileAsynxValidator],
      passwordGroup : this.formBuild.group({
        password: [],
        confimrPassword: [],
      })
    });
  }

  onSubmit() {
  }
}
上面的例子中mobileAsynxValidator为异步校验,当进行异步校验的时候,表单的status是pending。这里是使用Rxjs来模拟一个5S之后验证结果返回。

e. 表示控件状态的 CSS 类

像 AngularJS 中一样,Angular 会自动把很多控件属性作为 CSS 类映射到控件所在的元素上。我们可以使用这些类来根据表单状态给表单控件元素添加样式。目前支持下列类:
.ng-valid
.ng-invalid
.ng-pending
.ng-pristine
.ng-dirty
.ng-untouched
.ng-touched
在组件中加上对应的class,则会在对应的表单控件的状态的时候,适应对应的css样式。

f. 显示相应的错误信息

举个简单的例子吧
<div>UserName: <input formControlName="username" type="text"></div>
<div [hidden]="!formModel.hasError('required','username')">
请输入用户名!
</div>
这里也可以使用其他的属性值去判断,valid。说一下这个方法hasError(),第一个参数是校验器的返回结果,还记得自定义校验器的时候返回true吗!就是这个值。第二个参数是校验对应的控件。所以这里的formModel前面加了一个取反。

(2) 模板式表单的验证

模板式表单的验证呢,由于本人用的也不多,加上今天的时间有限,这里就说上面不同的点吧。

a. 内置校验器对应的指令。如required, minlength都有着对应的指令,可以应用。

<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm.value)" novalidate>
  <div>UserName: <input ngModel required name="username" type="text"></div>
  <div [hidden]="!myForm.form.hasError('required','username')">
    请输入用户名!
  </div>
  <div>TelePhone: <input ngModel name="phone" type="text"></div>
  <div ngModelGroup="passwordGroup">
    <div>Password: <input ngModel name="password" type="password"></div>
    <div>ConfirmPassword: <input ngModel name="ConfirmPassword" type="password"></div>
  </div>
  <button type="submit">Register</button>
</form>

* 模板式表单是不可以使用表单状态的值的,像valid,如下:
TemplateFormComponent.html:9 ERROR TypeError: Cannot read property 'valid' of null

想要使用的话需要使用事件绑定和本地模板变量传值到ts代码中去操作。

* 使用required的时候如果担心浏览器的默认行为,可以使用novalidate禁止浏览器的默认校验行为。


b. 自定义校验器的使用

我们只有将我们的自定义校验器以指令的形式创建,然后应用到html中去。
import { Directive } from '@angular/core';
import {FormControl, NG_VALIDATORS} from '@angular/forms';

@Directive({
  selector: '[Mobile]',
  providers: [{provide: NG_VALIDATORS, useValue: mobileValidator, multi:true}]
})
export class MobileDirective {

  constructor() { }
}

export function mobileValidator(control: FormControl): any {
  let myreq = /^(((13[0-9])|(14[0-9])|(15([0-9]))|(18[0-9]))+\d{8})$/;
  let valid = myreq.test(control.value);
  console.log('phone的校验结果是:' + valid);
  return valid ? null : {phone: true};
}
这里说两点
NG_VALIDATORS是校验器的默认token,都需要使用这个token。
multi:true是因为如果项目中有多个校验器指令的话,但是使用的是同一个token,那么就需要加上multi:true。


一天下来,很辛苦,但是终于搞完了,还不错。

猜你喜欢

转载自blog.csdn.net/it_rod/article/details/79605781