表单是最重要也是最复杂的
表单可能是WEB世界里最重要的,通过表单获取用户的输入.另一方面,表单看起来又是简单的,你放置一个input标签,一个submit按钮,然后点击按钮,提交,有什么难的呢?
实践证明,表单确实是很复杂的,理由如下:
- 表单输入意味着修改服务器与客户端的数据.
- 改变通常会需要去反映到其他的地方
- 用户会随意输入不同的值,所以需要校验
- 如果需要,需要清楚地标明错误
- 依赖字段具有复杂的逻辑
- 不依靠DOM选择器,我们不能测试表单
幸运的是,angular2提供了解决这些问题的工具:
- Controls封装了输入,并且提供一个与它们工作的对象
- Validators给了我们一个校验工具
- Observers让我们可以检测我们的表单,并且根据需要作出反应
在这一章,我们会深入学习表单.
Controls 和 Control Groups
ng2中,表单的两个基础设施就是Controls 和 Control Groups
Controls
Controls代表一个简单的输入域,它是ng2中表单的最小单元.
Controls封装了字段的值和有效,修改,错误的状态.
比如,我们会在Typescript中像下面一样使用Control
// create a new Control with the value "Nate"
let nameControl = new Control("Nate");
let name = nameControl.value; // -> Nate
// now we can query this control for certain values:
nameControl.errors // -> StringMap<string, any> of errors
nameControl.dirty // -> false
nameControl.valid // -> true
// etc.
为了构建表单,我们创建Control或者Control group,然后添加元数据和逻辑给它们.
像angular中的其他事情一样,我们有一个类(在本例子中是Control),我们会作为属性添加给DOM(本例子中是ngControl),如下:
<!-- part of some bigger form -->
<input type="text" ngControl="name" />
在我们的表单上下文中新建一个Control对象.
ControlGroup
许多表单都不止一个值,所以我们需要管理很多Controls,如果我们希望去校验我们的表单,去迭代每一个Control并校验是笨重的.为了解决这个问题,ControlGroup提供了一个封装.
下面我们创建一个ControlGroup.
let personInfo = new ControlGroup({
firstName: new Control("Nate"),
lastName: new Control("Murray"),
zip: new Control("90210")
})
ControlGroup和Control有一个公共的父类(AbstractControl).也就是说我们可以像一个简单Control一样检查personInfo的值和状态.
personInfo.value; // -> {
// firstName: "Nate",
// lastName: "Murray",
// zip: "90210"
//}
// now we can query this control group for certain values, which have sensible
// values depending on the children Control‘s values:
personInfo.errors // -> StringMap<string, any> of errors
personInfo.dirty // -> false
personInfo.valid // -> true
// etc.
注意,当我们获取ControlGroup的值时,我们会获取到一个key-value的对象.
第一个表单
这里有许多创建表单的重要方面我们没有提及,下面我们来逐一学习.
下面这个是我们将要创建的一个简单的表单:
在我们的理解中,我们创建一个电子商务型的网站,我们会列出我们出售的产品列表.在这个例子中,我们需要存储产品的SKU码,因此,我们创建一个获取SKU码的简单表单,它仅仅只有一个输入域.
我们的表单是非常简单的,它只有一个input和一个sumbit按钮.
让我们将这个表单组装成一个组件,如果你没有忘记,创建组件的三个部分为:
- 配置
- 创建模板
- 实现组件类
Simple SKU,配置选项
code/forms/app/forms/demo_form_sku.ts
import { Component } from ‘@angular/core‘;
import { FORM_DIRECTIVES } from ‘@angular/common‘;
@Component({
selector: ‘demo-form-sku‘,
directives: [FORM_DIRECTIVES],
注意,这里我们导入了FORM_DIRECTIVES,FORM_DIRECTIVES是一个指令组,它包含:
- ngControl
- ngControlGroup
- ngForm
- ngModel
- …
我们没有怎么使用这些指令,也没有说明它们能做什么.但是,现在,只需要知道我们需要使用这些指令就可以了.
Simple SKU模板
code/forms/app/ts/forms/demo_form_sku.ts
<div class="ui raised segment">
<h2 class="ui header">Demo Form: Sku</h2>
<form #f="ngForm" (ngSubmit)="onSubmit(f.value)" class="ui form">
<div class="field">
Forms in Angular 2 127
<label for="skuInput">SKU</label>
<input type="text" id="skuInput" placeholder="SKU" ngControl="sku">
<button type="submit" class="ui button">Submit</button>
</div>
</form>
</div>
form 和 NgForm
注意,我们导入了FORM_DIRECTIVES,所以我们可以使用ngForm添加到form上面.
NgForm做的事情是有好多好处的,而且也不明显.
导入FORM_DIRECTIVES后,ngForm会自动隐士地添加到任何的form标签上面.
ngForm给了我们两个东西:
- 一个命名为ngForm的ControlGroup
- 一个ngSubmit的输出
你可以在我们的代码中看到,我们使用了这两个东西:
<form #f="ngForm" (ngSubmit)="onSubmit(f.value)" class="ui form">
</form>
首先我们使用#f=”ngForm”创建了名为ngForm的表单,#v=thing说明,我们希望使用本地变量v代表这个view.
这里,我们创建了ngForm的标明,并绑定到变量f上.
我们的ngForm指令从哪里来? 它来自于NgForm指令.
ngForm的类型是什么?它是ControlGroup.这意味着我们可以将我们的视图作为ControlGroup使用.(ngSubmit)代表的是我们提交的时候需要做的事情.
所有添加起来的理解就是,当我们提交表单的时候,传递ControlGroup的value作为参数,调用我们组建的onSubmit方法.
input和ngControl
在我们讨论ngControl前,input标签有一些事情是我们感兴趣的.
code/forms/app/ts/forms/demo_form_sku.ts
<form #f="ngForm" (ngSubmit)="onSubmit(f.value)" class="ui form">
<div class="field">
Forms in Angular 2 127
<label for="skuInput">SKU</label>
<input type="text" id="skuInput" placeholder="SKU" ngControl="sku">
<button type="submit" class="ui button">Submit</button>
</div>
</form>
- class=”ui form”是可选的
- for属性与input的id是对应的
- placeholder是当用户没有输入的时候用来提示用的.
NgControl指令是用来标识ngControl这个选择器.这就意味着我们的input标签添加了这个属性:ngControl=”whatever”,这个例子中是sku.
NgControl会自动创建一个Control添加给父组件,这个例子中是ControlGroup.然后绑定这个DOM元素给Control.也就是说,我们通过名字sku将input与Control进行关联.
Simple SKU Form:组件定义
code/forms/app/ts/forms/demo_form_sku.ts
export class DemoFormSku { onSubmit(form: any): void {
console.log(‘you submitted value:‘, form);
}
}
试试
将所有代码合起来,像下面这样:
code/forms/app/ts/forms/demo_form_sku.ts
import { Component } from ‘@angular/core‘;
import { FORM_DIRECTIVES } from ‘@angular/common‘;
@Component({
selector: ‘demo-form-sku‘,
directives: [FORM_DIRECTIVES],
template: `
<div class="ui raised segment">
<h2 class="ui header">Demo Form: Sku</h2>
<form #f="ngForm"
(ngSubmit)="onSubmit(f.value)"
class="ui form">
<div class="field">
<label for="skuInput">SKU</label>
<input type="text"
id="skuInput"
placeholder="SKU"
ngControl="sku">
</div>
<button type="submit" class="ui button">Submit</button>
</form>
</div>
`
})
export class DemoFormSku {
onSubmit(form: any): void {
console.log(‘you submitted value:‘, form);
}
}
如果你运行这个程序,浏览器中会显示下面这样:
使用FormBuilder
隐式使用ngForm和ngControl是方便的,但是它没有给予我们太多的可自定义选项,通常,我们会使用一个更加复杂的方式创建form,那就是FormBuilder.
FormBuilder是一个帮助类,帮助我们创建表单.你可以将其理解为工厂方法.
让我们将FormBuilder添加到我们前面的例子中,看下面:
- 在我们组件定义类中怎么使用FormBuilder
- 是view中怎么使用自定义的ControlGroup.
怎么使用FormBuilder
在我们的组件类构造器参数中注入一个参数.
code/forms/app/ts/forms/demo_form_sku_with_builder.ts
export class DemoFormSkuBuilder { myForm: ControlGroup;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
‘sku‘: [‘ABC123‘] });
}
onSubmit(value: string): void {
console.log(‘you submitted value: ‘, value);
}
注入之后,一个FormBuilder的实例将会被创建,并且我们将其分配给局部变量fb.
我们将会使用FormBuilder的两个主要方法:
- control:创建一个新的Control
- group: 创建一个新的ControlGroup
注意,我们在本例子中使用myForm局部变量代表我们的表单.
myForm的类型是ControlGroup.通过fb.group创建的.group的参数是一个key-value,它代表这个ControlGroup里面的Control.在这个例子中,我们设置了一个名字为sku的Control,它的值是ABC123.
现在,我们有一个myForm的ControlGroup,我们需要使用它(通过绑定它到我们的form元素).
在view中使用myForm
我们希望改变去使用我们的myForm.如果没有忘记,在上面,我们说,导入FORM_DIRECTIVES时,ngForm会自动应用到我们的form元素上.我们也注意到了,ngForm创建了它自己的ControlGroup.好了,在这个例子中,我们不希望使用外部的ControlGroup,我们希望使用我们自己定义的myForm.我们应该怎么做?
当我们需要使用我们自己的ControlGroup时,angular提供了另外一种方式:它叫着ngFormModel,并且我们可以像下面这样使用它:
code/forms/app/ts/forms/demo_form_sku_with_builder.ts
<form [ngFormModel]="myForm"
(ngSubmit)="onSubmit(myForm.value)"
这里,我们使用ngFormModel告诉angular,我们将会使用myForm到这个上.
我们需要使用myForm的onSubmit代替f的onSubmit,最后一件事情就是绑定我们的Control到input上,使用NgFormControl.
code/forms/app/ts/forms/demo_form_sku_with_builder.ts
<input
type="text"
id="skuInput"
placeholder="SKU"
[ngFormControl]="myForm.controls[‘sku‘]">
试试
所有代码:
code/forms/app/ts/forms/demo_form_sku_with_builder.ts
import { Component } from ‘@angular/core‘;
import {
FORM_DIRECTIVES,
FormBuilder,
ControlGroup
} from ‘@angular/common‘;
@Component({
selector: ‘demo-form-sku-builder‘,
directives: [FORM_DIRECTIVES],
template: `
<div class="ui raised segment">
<h2 class="ui header">Demo Form: Sku with Builder</h2>
<form [ngFormModel]="myForm"
(ngSubmit)="onSubmit(myForm.value)"
class="ui form">
<div class="field">
<label for="skuInput">SKU</label>
<input type="text"
id="skuInput"
placeholder="SKU"
[ngFormControl]="myForm.controls[‘sku‘]">
</div>
<button type="submit" class="ui button">Submit</button>
</form>
</div>
`
})
export class DemoFormSkuBuilder {
myForm: ControlGroup;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
‘sku‘: [‘ABC123‘]
});
}
onSubmit(value: string): void {
console.log(‘you submitted value: ‘, value);
}
}
记住:
隐式创建ControlGroup和Control使用:
- ngForm
- ngControl
绑定存在的ControlGroup和Control使用:
- ngFormModel
- ngFormControl
添加校验器
用户经常不会按正确的模式输入正确的数据,如果用户没有按正确的格式输入数据,我们可以给用户一个反馈,并且让其不能提交,这种情况下,我们使用校验器(Validators).
Validators被Validators模块提供,而且,最简单的Validators是Validators.required.
Validators.require标明该Control是必填字段.
为了使用Validators,我们必须做两件事情.
- 分配一个Validator给Control
- 检查Validator状态,并给出反馈
let control = new Control(‘sku‘, Validators.required);
在我们的例子中,由于使用了FormBuilder,所以可以像下面这样:
code/forms/app/ts/forms/demo_form_with_validations_explicit.ts
constructor(fb: FormBuilder) {
this.myForm = fb.group({
‘sku‘: [‘‘, Validators.required]
现在,我们需要使用Validators到我们的view上,在View上,有两种方式访问Validators.
- 我们可以明确分配一个Control sku到我们类的实例,这个是比较啰嗦的,但是更加灵活.
- 在view中,我们从myForm获取Control sku,这中方式,在Component类中的代码量少,但是在view中的代码多.
为了分析他们的区别,我们看下面的例子.
显示的将Control变量设置为实例变量
如下:
更加灵活的方式是将Controls分开成单独的Control放在组件类中,下面是我们的类代码:
code/forms/app/ts/forms/demo_form_with_validations_explicit.ts
export class DemoFormWithValidationsExplicit {
myForm: ControlGroup;
sku: AbstractControl;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
‘sku‘: [‘‘, Validators.required]
});
this.sku = this.myForm.controls[‘sku‘];
}
onSubmit(value: string): void {
console.log(‘you submitted value: ‘, value);
}
}
注意:
- 我们在类中定义sku: AbstractControl
- 当使用FormBuilder创建之后,我们分配给sku.
这是一个很好的注意,它表明我们可以在我们的view的任何地方使用sku.现在,我们的sku是可以被validated.我们希望找到不同的方式去使用它.
- 校验所有的form,提供一条信息
- 分开校验每一个Control,并提供一条信息
- 分开校验每一个Control,当不合法的时候使用红色标注
- 分开校验每一个Control,是否已经输入,如果没有输入,显示一条信息
表单信息
我们可以通过检查myForm.valid来校验myForm.
code/forms/app/ts/forms/demo_form_with_validations_explicit.ts
<div *ngIf="!myForm.valid"
字段信息
我们也可以在字段不符合规定的时候显示一条信息
code/forms/app/ts/forms/demo_form_with_validations_explicit.ts
[ngFormControl]="sku">
<div *ngIf="!sku.valid"
字段颜色
我们也可以标注字段的颜色,当不合法的时候.
code/forms/app/ts/forms/demo_form_with_validations_explicit.ts
<div class="field"
[class.error]="!sku.valid && sku.touched">
指定特定的校验器
当需要依靠特定的校验器时,也可以制定.通过hasError()来实现.
code/forms/app/ts/forms/demo_form_with_validations_explicit.ts
class="ui error message">SKU is invalid</div>
<div *ngIf="sku.hasError(‘required‘)"
所有代码
code/forms/app/ts/forms/demo_form_with_validations_explicit.ts
/* tslint:disable:no-string-literal */
import { Component } from ‘@angular/core‘;
import {
CORE_DIRECTIVES,
FORM_DIRECTIVES,
FormBuilder,
ControlGroup,
Validators,
AbstractControl
} from ‘@angular/common‘;
@Component({
selector: ‘demo-form-with-validations-explicit‘,
directives: [CORE_DIRECTIVES, FORM_DIRECTIVES],
template: `
<div class="ui raised segment">
<h2 class="ui header">Demo Form: with validations (explicit)</h2>
<form [ngFormModel]="myForm"
(ngSubmit)="onSubmit(myForm.value)"
class="ui form">
<div class="field"
[class.error]="!sku.valid && sku.touched">
<label for="skuInput">SKU</label>
<input type="text"
id="skuInput"
placeholder="SKU"
[ngFormControl]="sku">
<div *ngIf="!sku.valid"
class="ui error message">SKU is invalid</div>
<div *ngIf="sku.hasError(‘required‘)"
class="ui error message">SKU is required</div>
</div>
<div *ngIf="!myForm.valid"
class="ui error message">Form is invalid</div>
<button type="submit" class="ui button">Submit</button>
</form>
</div>
`
})
export class DemoFormWithValidationsExplicit {
myForm: ControlGroup;
sku: AbstractControl;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
‘sku‘: [‘‘, Validators.required]
});
this.sku = this.myForm.controls[‘sku‘];
}
onSubmit(value: string): void {
console.log(‘you submitted value: ‘, value);
}
}
显示设置sku Controls作为组件类的实例变量
像我们上面一样,我们在组件类中创建了一个实例变量存储每一个input标签.
但是我们能不能不创建实例变量而直接浏览Control呢?答案是肯定的.我们会学习浏览Form的其他方式.
让我们看看另外的例子.
code/forms/app/ts/forms/demo_form_with_validations_shorthand.ts
export class DemoFormWithValidationsShorthand {
myForm: ControlGroup;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
‘sku‘: [‘‘, Validators.required] });
}
onSubmit(value: any): void {
console.log(‘you submitted value:‘, value.sku);
}
}
这两个例子的代码有点像,但是仔细看,你会发现,sku: AbstractControl已经不在这里面了.
让我们看看这三个字段级别的校验器,跟上面的作为对比.
声明一个本地sku引用
因为我们没有直接定义一个本地变量保存sku引用,所以我们需要一种获得它引用的方式,这里有两种方式:
- 通过myForm.find
- 通过ngFormControl
myForm.find
myForm具有一个find函数,它可以通过路径找到它的子元素,然后校验他们.
code/forms/app/ts/forms/demo_form_with_validations_shorthand.ts
<div class="field"
[class.error]="!myForm.find(‘sku‘).valid && myForm.find(‘sku‘).touched">
这比前面的代码还多,不可取.
通过NgFormControl导出
这里有另外一种方式获得引用,**通过ngForm导出ngFormControl.这是前面章节没有讲过的内容.
Component可以导出他们自己的引用,让你可以在视图中引用它们
我们在接下来的章节中讲解怎么使用exportAs导出,但是现在只需要知道,很多内建组件都可以像这样做.
在这个例子中,NgFormControl导出它自己作为ngForm.你可以使用#reference来引用他们.
如下:
code/forms/app/ts/forms/demo_form_with_validations_shorthand.ts
<input
type="text"
id="skuInput"
placeholder="SKU"
#sku="ngForm"
[ngFormControl]="myForm.controls[‘sku‘]">
注意,上面的就是使用ngFormControl导出它自己引入变量sku,但是要注意,sku是一个指令,不是Control.为了访问sku Control,需要使用sku.control.
现在,sku对于我们来说是可以利用的.我们可以像下面这样使用它.
code/forms/app/ts/forms/demo_form_with_validations_shorthand.ts
<div *ngIf="!sku.control.valid"
class="ui error message">SKU is invalid</div>
<div *ngIf="sku.control.hasError(‘required‘)" class="ui error message">SKU is required</div>
sku变量的范围
当我们创建一个引用的时候,它是可以在其兄弟节点或者子节点使用,但是不能在其父节点使用.
比如,不能像下面这样:
// this won‘t work
<div class="field"
[class.error]="!sku.control.valid && sku.control.touched">
因为div是form的父元素.
全部代码
code/forms/app/ts/forms/demo_form_with_validations_shorthand.ts
import { Component } from ‘@angular/core‘;
import {
CORE_DIRECTIVES,
FORM_DIRECTIVES,
FormBuilder,
ControlGroup,
Validators
} from ‘@angular/common‘;
@Component({
selector: ‘demo-form-with-validations-shorthand‘,
directives: [CORE_DIRECTIVES, FORM_DIRECTIVES],
template: `
<div class="ui raised segment">
<h2 class="ui header">Demo Form: with validations (shorthand)</h2>
<form [ngFormModel]="myForm"
(ngSubmit)="onSubmit(myForm.value)"
class="ui form">
<div class="field"
[class.error]="!myForm.find(‘sku‘).valid && myForm.find(‘sku‘).touched">
<label for="skuInput">SKU</label>
<input type="text"
id="skuInput"
placeholder="SKU"
#sku="ngForm"
[ngFormControl]="myForm.controls[‘sku‘]">
<div *ngIf="!sku.control.valid"
class="ui error message">SKU is invalid</div>
<div *ngIf="sku.control.hasError(‘required‘)"
class="ui error message">SKU is required</div>
</div>
<div *ngIf="!myForm.valid"
class="ui error message">Form is invalid</div>
<button type="submit" class="ui button">Submit</button>
</form>
</div>
`
})
export class DemoFormWithValidationsShorthand {
myForm: ControlGroup;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
‘sku‘: [‘‘, Validators.required]
});
}
onSubmit(value: any): void {
console.log(‘you submitted value:‘, value.sku);
}
}
自定义校验器
我们通常需要编写自己的校验器,怎么做?
为了弄清楚怎么实现自定义校验器,让我们看看内建的Validators.required.
export class Validators {
static required(c: Control): StringMap<string, boolean> {
return isBlank(c.value) || c.value == "" ? {"required": true} : null;
}
输入为一个Control,输出为一个
编写一个校验器
比如我们希望标志我们的sku,输入的必须是以123开头的.
我可以像下面这样写:
code/forms/app/ts/forms/demo_form_with_custom_validations.ts
function skuValidator(control: Control): { [s: string]: boolean } { if (!control.value.match(/^123/)) {
return {invalidSku: true};
}
}
当输入的不是以123开头时,这个校验器会返回invalidSku的错误代码.
分配校验器给Control
现在我们需要将skuValidator添加到Control上面去.但是这里有一个问题,我们的sku上面已经有一个校验器了,我们怎么添加多个校验器到同一个Control呢?
答案是我们使用Validators.compose.如下:
code/forms/app/ts/forms/demo_form_with_custom_validations.ts
this.myForm = fb.group({
‘sku‘: [‘‘, Validators.compose([
Validators.required, skuValidator])]
});
Validators.compose包装多个校验器给Control,当所有的校验器都是有效的时候,Control才是有效的.否则校验不通过.
现在我们可以在视图中使用这个新的校验器.
code/forms/app/ts/forms/demo_form_with_custom_validations.ts
<div *ngIf="sku.hasError(‘invalidSku‘)"
class="ui error message">SKU must begin with <tt>123</tt></div>
监听变化
到现在为止,当表单提交的时候,我们只是从我们的表单中读取数据.但是通常,我们希望监听每一个Control的变化.
ControlGroup与Control都有一个EventEmitter,它可以用来监听数据的变化.
为了监听变化,我们需要做下面的两个操作:
- 通过调用control.valueChanges获取
- 使用EventEmitter.observer方法添加一个观察者
下面是一个例子:
code/forms/app/ts/forms/demo_form_with_events.ts
export class DemoFormWithEvents {
myForm: ControlGroup;
sku: AbstractControl;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
‘sku‘: [‘‘, Validators.required]
});
this.sku = this.myForm.controls[‘sku‘];
this.sku.valueChanges.subscribe(
(value: string) => {
console.log(‘sku changed to:‘, value);
}
);
this.myForm.valueChanges.subscribe(
(form: any) => {
console.log(‘form changed to:‘, form);
}
);
}
onSubmit(form: any): void {
console.log(‘you submitted value:‘, form.sku);
}
}
这里我们监听两个事件:sku变化事件和整个form的变化事件.
我们传递进去的是一个简单的key.接下来是一个函数,当值变哈的时候会被调用.
ngModel
ngModel是一个特殊的指令,它绑定一个Model给form.ngModel是一个实现了双向数据绑定的特殊指令.双向数据绑定是比较复杂的.angular2默认使用的是单项数据流,但是在Form中,由于需要跟踪用户改变,所以使用双向数据绑定.
看下面的例子:
code/forms/app/ts/forms/demo_form_ng_model.ts
export class DemoFormNgModel {
myForm: ControlGroup; productName: string;
constructor(fb: FormBuilder) {
this.myForm = fb.group({
‘productName‘: [‘‘, Validators.required] });
}
onSubmit(value: string): void {
console.log(‘you submitted value: ‘, value);
}
}
注意,我们在勒种定义了productName: string 实例变量.然后让我们看看在input标签中怎么使用ngModel:
code/forms/app/ts/forms/demo_form_ng_model.ts
<input
type="text"
id="productNameInput"
placeholder="Product Name"
[ngFormControl]="myForm.find(‘productName‘)"
[(ngModel)]="productName">
ngModel的语法是很奇特的,它使用[]和()合起来,由前面的可知,[]是输入,()是输出,也就是说双向就是输入和输出都绑定了.
然后,我们将productName显示出来.
code/forms/app/ts/forms/demo_form_ng_model.ts
<div class="ui info message">
The product name is: {{productName}}
</div>
看起来像下面这样:
可以看到,是实时同步的.
总结
Form有很多的复杂功能,但是angular2提供了一个清晰明了的方式,一旦你学会使用了ControlGroups, Controls, 和 Validations,使用表单就会变得很简单了.