表单及客户端验证

要在 AppComponent 和 HeroDetailComponent 的模板中使用 Bootstrap 中的 CSS 类。请把 bootstrap 的CSS 样式表文件添加到 style.css 的头部:

1
      @import url('https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css');

一、表单

1.1 模板驱动表单

创建初始 HTML 表单模板

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
<div class="container">
    <h1>Hero Form</h1>
    <form>
    <div class="form-group">
        <label for="name">Name</label>
        <input type="text" class="form-control" id="name" required>
    </div>

    <div class="form-group">
        <label for="alterEgo">Alter Ego</label>
        <input type="text" class="form-control" id="alterEgo">
    </div>

    <button type="submit" class="btn btn-success">Submit</button>
    </form>
</div>

使用 ngModel 进行双向数据绑定

input 内容变动时,所绑定的对象属性也随之变动

1
2
3
4
<input type="text" class="form-control" id="name"
        required
        [(ngModel)]="model.name" name="name">
TODO: remove this: {{model.name}}

NgForm 指令

1
      <form #heroForm="ngForm">

heroForm 变量是一个到 NgForm 指令的引用,它代表该表单的整体;
NgForm 指令为 form 增补了一些额外特性。 它会控制那些带有 ngModel 指令和 name 属性的元素,监听他们的属性(包括其有效性)。 它还有自己的 valid 属性,这个属性只有在它包含的每个控件都有效时才是真。

通过 ngModel 跟踪修改状态与有效性验证

NgModel 指令不仅仅跟踪状态。它还使用特定的 Angular CSS 类来更新控件,以反映当前状态。 可以利用这些 CSS 类来修改控件的外观,显示或隐藏消息。 添加样式

1
2
3
4
5
6
7
.ng-valid[required], .ng-valid.required  {
border-left: 5px solid #42A948; /* green */
}

.ng-invalid:not(form)  {
border-left: 5px solid #a94442; /* red */
}

修改页面

1
2
3
4
5
6
7
8
9
<label for="name">Name</label>
<input type="text" class="form-control" id="name"
        required
        [(ngModel)]="model.name" name="name"
        #name="ngModel">
<div [hidden]="name.valid || name.pristine"
    class="alert alert-danger">
Name is required
</div>

当控件是有效的 (valid) 或全新的 (pristine) 时,隐藏消息。

使用 ngSubmit 提交该表单

在填表完成之后,用户还应该能提交这个表单。 “Submit(提交)”按钮位于表单的底部,它自己不做任何事,要让它有用,就要把该表单的 ngSubmit 事件属性绑定到英雄表单组件的 onSubmit() 方法上:

1
2
3
<form (ngSubmit)="onSubmit()" #heroForm="ngForm">

<button type="submit" class="btn btn-success" [disabled]="!heroForm.form.valid">Submit</button>

就会违反“必填”规则。同时,Submit 按钮也被禁用了。

1.2响应式表单

这种编程风格更倾向于在非 UI 的数据模型(通常接收自服务器)之间显式的管理数据流, 并且用一个 UI 导向的表单模型来保存屏幕上 HTML 控件的状态和值。
响应式表单可以让使用响应式编程模式、测试和校验变得更容易。在响应式编程范式中,组件会负责维护数据模型的不可变性,把模型当做纯粹的原始数据源。 组件不会直接更新数据模型,而是把用户的修改提取出来,把它们转发给外部的组件或服务,外部程序才会使用这些进行处理(比如保存它们), 并且给组件返回一个新的数据模型,以反映模型状态的变化。

1.2.1简单的表单类

CSS 类 说明
AbstractControl AbstractControl是这三个具体表单类的抽象基类。 并为它们提供了一些共同的行为和属性。
FormControl FormControl 用于跟踪一个单独的表单控件的值和有效性状态。它对应于一个 HTML 表单控件,比如 <input><select>
FormGroup FormGroup用于 跟踪一组AbstractControl 的实例的值和有效性状态。 该组的属性中包含了它的子控件。 组件中的顶级表单就是一个 FormGroup。
FormArray FormArray用于跟踪 AbstractControl 实例组成的有序数组的值和有效性状态。

①FormControl 它允许你直接创建并管理一个 FormControl 实例

1
import { FormControl } from '@angular/forms';

component类(创建了一个名叫 name 的 FormControl。 它将会绑定到模板中的一个 元素):

1
2
3
export class HeroDetailComponent1 {
    name = new FormControl();
}

模板文件(就要在模板中的 <input> 上加一句 [formControl]="name"):

1
2
3
4
5
<h2>Hero Detail</h2>
<h3><i>Just a FormControl</i></h3>
<label class="center-block">Name:
    <input class="form-control" [formControl]="name">
</label>

②FormGroup 如果有多个 FormControl,你要把它们都注册进一个父 FormGroup 中

1
import { FormControl, FormGroup } from '@angular/forms';

component类(把 FormControl 包裹进了一个名叫 heroForm 的 FormGroup 中):

1
2
3
4
5
export class HeroDetailComponent2 {
    heroForm = new FormGroup ({
        name: new FormControl()
    });
}

模板文件:

1
2
3
4
5
6
7
8
9
<h2>Hero Detail</h2>
<h3><i>FormControl in a FormGroup</i></h3>
<form [formGroup]="heroForm">
<div class="form-group">
    <label class="center-block">Name:
    <input class="form-control" formControlName="name">
    </label>
</div>
</form>

没有父 FormGroup 的时候,[formControl]="name" 也能正常工作
有了 FormGroup,name 这个 <input> 就需要再添加一个语法 formControlName=name,以便让它关联到类中正确的 FormControl 上。
这个语法告诉 Angular,查阅父 FormGroup(这里是 heroForm),然后在这个 FormGroup 中查阅一个名叫 name 的 FormControl。

1.2.2 FormBuilder

FormBuilder.group 是一个用来创建 FormGroup 的工厂方法,可以让你的代码更加紧凑、易读。 因为你不必写一系列重复的 new FormControl(...) 语句。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
export class HeroDetailComponent4 {
    heroForm: FormGroup;
    states = states;

    constructor(private fb: FormBuilder) {
        this.createForm();
    }

    createForm() {
        this.heroForm = this.fb.group({
        name: ['', Validators.required ],
        street: '',
        city: '',
        state: '',
        zip: '',
        power: '',
        sidekick: ''
        });
    }
}

1.2.3 多级 FormGroup

要想更有效的管理这个表单的大小,你可以把一些相关的 FormControl 组织到多级 FormGroup 中

componenet类(用 FormBuilder 在这个名叫 heroForm 的组件中创建一个 FormGroup,并把它用作父 FormGroup。 再次使用 FormBuilder 创建一个子级 FormGroup):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
export class HeroDetailComponent5 {
    heroForm: FormGroup;
    states = states;

    constructor(private fb: FormBuilder) {
        this.createForm();
    }

    createForm() {
        this.heroForm = this.fb.group({ // <-- the parent FormGroup
        name: ['', Validators.required ],
        address: this.fb.group({ // <-- the child FormGroup
            street: '',
            city: '',
            state: '',
            zip: ''
        }),
        power: '',
        sidekick: ''
        });
    }
}

组件(添加一个 formGroupName 指令):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div formGroupName="address" class="well well-lg">
    <h4>Secret Lair</h4>
    <div class="form-group">
    <label class="center-block">Street:
        <input class="form-control" formControlName="street">
    </label>
    </div>
    <div class="form-group">
    <label class="center-block">City:
        <input class="form-control" formControlName="city">
    </label>
    </div>
    <div class="form-group">
    <label class="center-block">State:
        <select class="form-control" formControlName="state">
        <option *ngFor="let state of states" [value]="state">{{state}}</option>
        </select>
    </label>
    </div>
    <div class="form-group">
    <label class="center-block">Zip Code:
        <input class="form-control" formControlName="zip">
    </label>
    </div>
</div>

1.2.4 查看 FormControl 的属性

.get() 方法来提取表单中一个单独 FormControl 的状态

1
2
<p>Name value: {{ heroForm.get('name').value }}</p>
<p>Street value: {{ heroForm.get('address.street').value}}</p>

1.2.5 使用 FormArray 来表示 FormGroup 数组

FormGroup 是一个命名对象,它的属性值是 FormControl 和其它的 FormGroup。

要使用 FormArray,就要这么做

①在数组中定义条目 FormControl 或 FormGroup。 ②把这个数组初始化微一组从数据模型中的数据创建的条目。 ③根据用户的需求添加或移除这些条目。

使用FormArray

1
2
3
4
5
6
this.heroForm = this.fb.group({
    name: ['', Validators.required ],
    secretLairs: this.fb.array([]), // <-- secretLairs as an empty FormArray
    power: '',
    sidekick: ''
});

初始化FormArray(secretLairs):

1
2
3
4
5
6
7
setAddresses(addresses: Address[]) {
    const addressFGs = addresses.map(address => this.fb.group(address));
    const addressFormArray = this.fb.array(addressFGs);
    this.heroForm.setControl('secretLairs', addressFormArray);
}
//使用 FormGroup.setControl() 方法,而不是 setValue() 方法来替换前一个 FormArray。 你所要替换的是控件,而不是控件的值。
//secretLairs 数组中包含的是**FormGroup,而不是 Address

获取 FormArray

1
2
3
get secretLairs(): FormArray {
return this.heroForm.get('secretLairs') as FormArray;
};

显示 FormArray ①formArrayName 指令设为 "secretLairs"。 这一步为内部的表单控件建立了一个 FormArray 型的 secretLairs 作为上下文;
②这些重复条目的数据源是 FormArray.controls 而不是 FormArray 本身;
③每个被重复渲染的 FormGroup 都需要一个独一无二的 formGroupName,它必须是 FormGroup 在这个 FormArray 中的索引。 你将复用这个索引,以便为每个地址组合出一个独一无二的标签。

1
2
3
4
5
<div formArrayName="secretLairs" class="well well-lg">
<div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" >
    <!-- The repeated address template -->
</div>
</div>

添加到 FormArray 中

1
2
3
addLair() {
this.secretLairs.push(this.fb.group(new Address()));
}

二、客户端验证

2.1 模板驱动验证

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
<input id="name" name="name" class="form-control"
    required minlength="4" appForbiddenName="bob"
    [(ngModel)]="hero.name" #name="ngModel" >

<div *ngIf="name.invalid && (name.dirty || name.touched)"
    class="alert alert-danger">

<div *ngIf="name.errors.required">
    Name is required.
</div>
<div *ngIf="name.errors.minlength">
    Name must be at least 4 characters long.
</div>
<div *ngIf="name.errors.forbiddenName">
    Name cannot be Bob.
</div>

</div>

<input> 元素带有一些 HTML 验证属性:required 和 minlength。它还带有一个自定义的验证器指令 forbiddenName。 #name="ngModel" 把 NgModel 导出成了一个名叫 name 的局部变量。NgModel 把自己控制的 FormControl 实例的属性映射出去,让你能在模板中检查控件的状态,比如 valid 和 dirty。 <div> 元素的 *ngIf 揭露了一套嵌套消息 divs,但是只在有“name”错误和控制器为 dirty 或者 touched。 每个嵌套的 <div> 为其中一个可能出现的验证错误显示一条自定义消息。比如 required、minlength 和 forbiddenName。

2.2 响应式表单的验证

在响应式表单中,真正的源码都在组件类中。不应该通过模板上的属性来添加验证器,而应该在组件类中直接把验证器函数添加到表单控件模型上(FormControl)。然后,一旦控件发生了变化,Angular 就会调用这些函数。

2.3 验证器函数

有两种验证器函数:同步验证器和异步验证器。
①同步验证器函数接受一个控件实例,然后返回一组验证错误或 null。你可以在实例化一个 FormControl 时把它作为构造函数的第二个参数传进去。
②异步验证器函数接受一个控件实例,并返回一个承诺(Promise)或可观察对象(Observable),它们稍后会发出一组验证错误或者 null。你可以在实例化一个 FormControl 时把它作为构造函数的第三个参数传进去。

2.2.2内置验证器

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
ngOnInit(): void {
this.heroForm = new FormGroup({
    'name': new FormControl(this.hero.name, [
    Validators.required,
    Validators.minLength(4),
    forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
    ]),
    'alterEgo': new FormControl(this.hero.alterEgo),
    'power': new FormControl(this.hero.power, Validators.required)
});
}

get name() { return this.heroForm.get('name'); }
get power() { return this.heroForm.get('power'); }

name 控件设置了两个内置验证器:Validators.required 和 Validators.minLength(4);
由于这些验证器都是同步验证器,因此你要把它们作为第二个参数传进去;
可以通过把这些函数放进一个数组后传进去,可以支持多重验证器

2.2.3 自定义验证器

前面的例子中的 forbiddenNameValidator 函数:

1
2
3
4
5
6
export function forbiddenNameValidator(nameRe: RegExp): ValidatorFn {
return (control: AbstractControl): {[key: string]: any} => {
    const forbidden = nameRe.test(control.value);
    return forbidden ? {'forbiddenName': {value: control.value}} : null;
};
}

添加响应式表单

添加自定义验证器相当简单。你所要做的一切就是直接把这个函数传给 FormControl

1
2
3
4
5
6
7
8
9
this.heroForm = new FormGroup({
'name': new FormControl(this.hero.name, [
    Validators.required,
    Validators.minLength(4),
    forbiddenNameValidator(/bob/i) // <-- Here's how you pass in the custom validator.
]),
'alterEgo': new FormControl(this.hero.alterEgo),
'power': new FormControl(this.hero.power, Validators.required)
});

添加到模板驱动表单

Angular 在验证流程中的识别出指令的作用,是因为指令把自己注册到了 NG_VALIDATORS 提供商中,该提供商拥有一组可扩展的验证器

1
        providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]

然后该指令类实现了 Validator 接口,以便它能简单的与 Angular 表单集成在一起

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
@Directive({
    selector: '[appForbiddenName]',
    providers: [{provide: NG_VALIDATORS, useExisting: ForbiddenValidatorDirective, multi: true}]
})
export class ForbiddenValidatorDirective implements Validator {
    @Input('appForbiddenName') forbiddenName: string;

    validate(control: AbstractControl): {[key: string]: any} {
    return this.forbiddenName ? forbiddenNameValidator(new RegExp(this.forbiddenName, 'i'))(control)
                                : null;
    }
}

一旦 ForbiddenValidatorDirective 写好了,你只要把 forbiddenName 选择器添加到输入框上就可以激活这个验证器了

1
2
3
<input id="name" name="name" class="form-control"
        required minlength="4" appForbiddenName="bob"
        [(ngModel)]="hero.name" #name="ngModel" >

自定义验证器指令是用 useExisting 而不是 useClass 来实例化的。注册的验证器必须是这个 ForbiddenValidatorDirective 实例本身,也就是表单中 forbiddenName 属性被绑定到了"bob"的那个。如果用 useClass 来代替 useExisting,就会注册一个新的类实例,而它是没有 forbiddenName 的。