5.1 表单——既重要,又复杂

在Web应用中,表单或许是最重要的部分。虽然我们常从点击链接或移动鼠标中得到事件通知,但大多数“富数据”都是通过表单从用户那里获得的。

从表面上看,表单似乎很简单:创建一个input标签,用户填入数据,然后再点击提交。这有什么难的?

但事实证明,表单最终可能是非常复杂的。原因如下:

●表单输入意味着需要在页面和服务器端同时修改这份数据;

●修改的内容通常要在页面的其他地方反映出来;

●用户的输入可能存在很多问题,所以需要验证输入的内容;

●用户界面需要清晰地显示出可能出现的预期结果和错误信息;

●字段之间的依赖可能存在复杂的业务逻辑;

●我们希望不依赖DOM选择器就能轻松测试表单。

值得庆幸的是,Angular已经给出了上述所有问题的解决方案。

表单控件(FormControl)封装了表单中的输入,并提供了一些可供操纵的对象。

验证器(validator)让我们能以自己喜欢的任何方式验证表单输入。

观察者(observer)让我们能够监听表单的变化,并作出相应的回应。

在本章中,我们将一步一步构建表单应用。先构建一些简单的表单,然后构建逻辑更复杂的表单。

5.2 FormControl和FormGroup

FormControl和FormGroup是Angular中两个最基础的表单对象。

5.2.1 FormControl

FormControl代表单一的输入字段,它是Angular表单中的最小单元。

FormControl封装了这些字段的值和状态,比如是否有效、是否脏(被修改过)或是否有错误等。

比如,下列代码演示了如何在TypeScript中使用FormControl:

// create a new FormControl with the value "Nate"

let nameControl = new FormControl("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.

为了构建表单,我们会创建几组FormControl对象,然后为它们附加元数据和逻辑。

在Angular中,我们经常将一个类(本例中为FormControl)以属性形式(本例中为formControl)附加在DOM上。比如下面这个表单:

<!—— part of some bigger form ——>

<input type="text" [formControl]="name" />

这会在此form的上下文中创建一个新的FormControl对象。稍后我们会进一步讨论它的工作原理。

5.2.2 FormGroup

大多数表单都拥有不止一个字段,因此我们需要某种方式来管理多个FormControl。假设我们要检查表单的有效性。如果要遍历这个FormControl数组并检查每一个FormControl是否有效,必然相当繁琐;而FormGroup则可以为一组FormControl提供总包接口(wrapper interface),来解决这种问题。

下面是FormGroup的创建方式:

let personInfo = new FormGroup({

  firstName:new FormControl("Nate"),

  lastName:new FormControl("Murray"),

  zip:new FormControl("90210")

})

FormGroup和FormControl都继承自同一个祖先AbstractControl。这意味检查personInfo的状态或值就像检查单个FormControl那么容易:

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 FormControl's values:

personInfo.errors // -> StringMap<string, any> of errors

personInfo.dirty // -> false

personInfo.valid // -> true

// etc.

注意,当我们试图从FormGroup中获取value时,会收到一个“键值对”结构的对象。它能让我们从表单中一次性获取全部的值而无需逐一遍历FormControl,使用起来相当顺手。

5.3 我们的第一个表单

创建表单的方式很多,而且好几种重要的方式我们还没有讨论到。先来看一个完整的例子,稍后再一一解释。

 本节的所有示例代码都可以在forms/目录下找到。

我们要构建的第一个表单,效果如图5-1所示。

图5-1 带SKU的表单演示:简易版

假设我们要构建一个电子商务网站来展示并销售一些产品。在此应用中需要存储产品的SKU,因此先来创建一个只有SKU输入框的简易表单。

 SKU是库存单位(stockkeeping unit)的缩写。它是用来跟踪产品库存的唯一编号。当我们提到SKU时,指的是人类可读的产品编码。

这个表单超级简单:只有一个sku(带label)输入框和一个提交按钮。

我们先把表单变为组件。你应该还记得,定义组件需要包含以下三个部分:

●配置@Component()注解;

●创建模板;

●在组件定义类中实现自定义功能。

下面来依次实现它们。

5.3.1 加载FormsModule

为了使用这个新的表单库,先要确保我们的NgModule中导入了这个表单库。

Angular中有两种使用表单的方式,我们在本章中都会展开讨论:使用FormsModule以及使用ReactiveFormsModule。既然都要用到,那么这个模块就同时导入它们。因此需要在引用启动程序app.ts中这样写:

import {

 FormsModule,

 ReactiveFormsModule

} from '@angular/forms';

// farther down……

@NgModule({

 declarations:[

  FormsDemoApp,

  DemoFormSku,

  // …… our declarations here

 ],

 imports:[

  BrowserModule,

  FormsModule,     // <—— add this

  ReactiveFormsModule // <—— and this

 ],

 bootstrap:[ FormsDemoApp ]

})

class FormsDemoAppModule {}

这确保了我们能在视图中使用Angular表单指令。先简要介绍一下,FormsModule为我们提供了一些模板驱动的指令,例如:

●ngModel

●NgForm

ReactiveFormsModule则提供了下列指令:

●formControl

●ngFormGroup

此外,还有很多指令。我们还没有讨论过如何使用这些指令以及它们是做什么的,但很快就要讲到了。现在只需要知道把FormsModule和ReactiveFormsModule导入到我们的NgModule中就行了。这表示我们能在视图中使用上述所有指令,并能在组件中注入相应的服务

5.3.2 简易SKU表单:@Component注解

现在我们就可以开始创建组件了。

code/forms/app/forms/demo_form_sku.ts

import { Component } from '@angular/core';

@Component({

 selector:'demo-form-sku',

这里定义了一个demo-form-sku的选择器(selector)。还记得吧?selector会告诉Angular,组件将绑定到哪些元素上。这里我们可以通过demo-form-sku标签来使用这个组件:

<demo-form-sku></demo-form-sku>

5.3.3 简易SKU表单:template

我们来看看template。

code/forms/app/ts/forms/demo_form_sku.ts

 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"

        name="sku" ngModel>

   </div>

   <button type="submit" class="ui button">Submit</button>

  </form>

 </div>

 `

form和NgForm

现在事情开始变得有趣了:我们导入了FormsModule,因此可以在视图中使用NgForm了。记住,当这些指令在视图中可用时,它就会被附加到任何能匹配其selector的节点上。

NgForm做了一件便利但隐晦的工作:它的选择器包含form 标签(而不用显式添加ngForm属性)。这意味着当我们导入FormsModule时候,NgForm就会被自动附加到视图中所有的<form>标签上。这确实非常有用,但由于它发生在幕后,也许会让很多人感到困惑。

NgForm给我们提供了两个重要的功能:

(1)一个名叫ngForm的FormGroup对象;

(2)一个输出事件(ngSubmit)。

你可以看到我们在视图的<form>标签中同时用到了它们两个。

code/forms/app/ts/forms/demo_form_sku.ts

  <form #f="ngForm"

     (ngSubmit)="onSubmit(f.value)"

首先,我们使用了#f="ngForm"。#v=thing语法的意思是,我们希望在当前视图中创建一个局部变量。

这里我们为视图中的ngForm创建了一个别名,并绑定到变量#f。这个ngForm来自哪里呢?它是由NgForm指令导出的。

ngForm是什么类型的对象呢?它是FormGroup类型的。这意味着我们可以在视图中把变量f当作FormGroup使用,而这也正是我们在输出事件(ngSubmit)中的使用方法。

细心的读者可能会注意到,上面提到NgForm会自动附加到<form>标签上(因为NgForm指令的选择器中默认包含了form),这意味着我们不必添加ngForm属性就能使用NgForm指令。但是这里我们也将ngForm添加到了属性的值上。这是笔误吗?

不,这不是笔误。如果ngForm是属性的键,那就是在告诉Angular:我们要根据这个属性使用NgForm指令。但在这里,我们要对一个引用赋值,而把ngForm用作属性值。这表示把ngForm这个表达式的执行结果赋值给局部模板变量f。

既然ngForm在这个节点上,你应该可以推断出我们正在导出的这个f变量是FormGroup类型的,接下来就可以在视图中的任何地方引用它了。

我们在表单中绑定ngSubmit事件的语法是:(ngSubmit)="onSubmit(f.value)"。

●(ngSubmit):来自NgForm指令。

●onSubmit():将会在我们的组件类中进行定义(稍后)。

●f.value:f就是我们前面提到的FormGroup,而.value会以键值对的形式返回FormGroup中所有控件的值。

总结起来,这行代码的意思是:“当我提交表单时,将会以该表单的值作为参数,调用组件实例上的onSubmit方法。”

input和NgModel

在讨论NgModel之前,关于input标签还有几点需要说明。

code/forms/app/ts/forms/demo_form_sku.ts

  <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"

        name="sku" ngModel>

   </div>

●class="ui form"和class="field"是两个可选的类。它们来自CSS框架Semantic UI。它们并不属于Angular的范畴,在这里加上它们只是为了让本例子好看一些。

●label标签的for属性和input标签的id属性是一致的,这依据的是W3C标准

●我们设置SKU控件的placeholder属性,将其作为input值为空时给用户的提示。

NgModel指令指定的selector是ngModel。这意味着我们可以通过添加这个属性把它附加到input标签上:ngModel="whatever"。在这里我们指定了一个不带属性值的ngModel。

有两种不同的方法能在模板中指定ngModel,这里是第一种。当使用不带属性值的ngModel时,我们是要指定:

(1)单向数据绑定;

(2)希望在表单中创建一个名为sku的FormControl(这个sku来自input标签上的name属性)。

NgModel会创建一个新的FormControl对象,把它自动添加到父FormGroup上(这里也就是form表单对象),并把这个FormControl对象绑定到一个DOM上。也就是说,它会在视图中的input标签和FormControl对象之间建立关联。这种关联是通过name属性建立的,在本例中是"sku"。

NgModel与ngModel有什么不同呢? 通常,我们使用Pascal命名法(如NgModel)时,指的是和供代码中引用的对象。首字母小写的驼峰命名法(如ngModel)来自指令的选择器 selector,并且只会被用在DOM/模板中。

需要指出的是,NgModel和FormControl并不是同一个。NgModel是用在视图中的指令,而FormControl则用来表示表单中的数据和验证规则。

有时,我们希望用ngModel来实现AngularJS那样的双向绑定。在本章的最后,我们会看到如何进行实现。

5.3.4 简易SKU表单:组件定义类

现在来看看组件类的定义。

code/forms/app/ts/forms/demo_form_sku.ts

export class DemoFormSku {

 onSubmit(form:any):void {

  console.log('you submitted value:', form);

 }

}

在这里,我们的类定义了一个名为onSubmit的方法,该方法会在表单提交时调用。目前我们只用console.log打印出传进去的值。

5.3.5 试试看

全部代码如下所示。

code/forms/app/ts/forms/demo_form_sku.ts

import { Component } from '@angular/core';

@Component({

 selector:'demo-form-sku',

 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"

        name="sku" ngModel>

   </div>

   <button type="submit" class="ui button">Submit</button>

  </form>

 </div>

 `

})

export class DemoFormSku {

 onSubmit(form:any):void {

  console.log('you submitted value:', form);

 }

}

如果打开浏览器运行代码,结果如图5-2所示。

图5-2 带SKU的表单演示:简易版,已提交

5.4 使用FormBuilder

使用ngForm和ngControl隐式构建FormControl和FormGroup确实很方便,但无法为我们提供更多定制化选项。使用FormBuilder构建表单则是一种更为灵活和通用的方式。

FormBuilder是一个名副其实的表单构建助手。你应该还记得,表单是由FormControl和FormGroup构成的,而FormBuilder则可以帮助我们创建它们(你可以把它看作一个“工厂”对象)。

让我们在先前的例子中添加一个FormBuilder,看看:

●如何在组件定义类中使用FormGroup;

●如何在视图表单中使用自定义的FormGroup。

5.5 响应式表单FormBuilder

我们将使用formGroup和formControl指令来构建这个组件,这意味着我们需要导入相应的类。导入的代码如下所示。

code/forms/app/ts/forms/demo_form_sku_with_builder.ts

import { Component } from '@angular/core';

import {

 FormBuilder,

 FormGroup

} from '@angular/forms';

@Component({

 selector:'demo-form-sku-builder',

5.5.1 使用FormBuilder

通过在组件类上声明带参数的constructor,我们注入了一个FormBuilder。

code/forms/app/ts/forms/demo_form_sku_with_builder.ts

export class DemoFormSkuBuilder {

 myForm:FormGroup;

 constructor(fb:FormBuilder){

  this.myForm = fb.group({

   'sku':['ABC123']

  });

 }

 onSubmit(value:string):void {

  console.log('you submitted value:', value);

 }

}

注入意味着什么? 我们还未曾深入讨论过依赖注入(dependency injection,DI)以及DI是如何关联到继承树的,因此你可能看不太懂最后这句话。我们在第8章中讨论了很多关于依赖注入的知识,如果你希望深入学习,请移步那里。

大体来说,依赖注入就是用来告诉Angular,为了让组件正常运行需要给它哪些依赖。

在这期间,Angular将会注入一个从FormBuilder类创建的对象实例,并把它赋值给fb变量(来自构造函数)。

我们将会使用FormBuilder中的两个主要函数:

●control,用于创建一个新的FormControl;

●group,用于创建一个新的FormGroup。

注意,我们在类中创建了一个名叫myForm的实例变量。(简单起见,确实也可以把它称作form,但这里是为了区分FormGroup和之前的form表单。)

myForm是FormGroup类型。我们通过调用fb.group()来创建FormGroup。.group方法的参数是代表组内各个FormControl的键值对。

在这里,我们设置了一个名为sku的控件,其值为["ABC123"]——意思是控件的默认值为"ABC123"。(你可能注意到了这里用的是数组。这是因为我们稍后还会添加更多配置项。)

现在我们就能在视图中使用myForm了。(也就是说,我们需要将它绑定到表单元素上。)

5.5.2 在视图中使用myForm

我们希望修改<form>标签,让它使用myForm变量。回忆一下,在上一节中我们提到过,当导入FormsModule时,ngForm就会自动起作用。还提到过ngForm会自动创建它自己的FormGroup。但在这里我们不希望使用外部的FormGroup,而是使用FormBuilder创建的这个myForm实例变量。那该怎么做呢?

Angular提供了另一个指令,能让我们使用现有的FormGroup。它叫作formGroup,可以这样使用。

code/forms/app/ts/forms/demo_form_sku_with_builder.ts

  <h2 class="ui header">Demo Form:Sku with Builder</h2>

  <form [formGroup]="myForm"

这里我们告诉Angular,想用myForm作为这个表单的FormGroup。

我们说过,当使用FormsModule时,NgForm会自动应用于<form>元素上。但其实有一个例外:NgForm不会应用到带formGroup属性的<form>节点上。

你也许不明白原因,这是因为NgForm的selector是:

form:not([ngNoForm]):not([formGroup]),ngForm,[ngForm]

这意味着你还可以使用ngNoForm属性产生一个不带NgForm的<form>表单。

我们还需要把onSubmit中的f替换为myForm,因为现在的myForm变量中保存着表单的配置和值。

想让程序运行起来,还要做最后一件事:将我们的FormControl绑定到input标签上。记住,ngControl会创建一个新的FormControl对象,并附加到父FormGroup中。但在这个例子中,我们已经用FormBuilder创建了自己的FormControl。

要将现有的FormControl绑定到input上,可以用formControl。

code/forms/app/ts/forms/demo_form_sku_with_builder.ts

     <label for="skuInput">SKU</label>

     <input type="text"

         id="skuInput"

         placeholder="SKU"

         [formControl]="myForm.controls['sku']">

在这里,我们将input标签上的formControl指令指向了myForm.controls上现有的FormControl控件sku。

5.5.3 试试看

将上面的所有代码整合在一起。

code/forms/app/ts/forms/demo_form_sku_with_builder.ts

import { Component } from '@angular/core';

import {

 FormBuilder,

 FormGroup

} from '@angular/forms';

@Component({

 selector:'demo-form-sku-builder',

 template:`

 <div class="ui raised segment">

  <h2 class="ui header">Demo Form:Sku with Builder</h2>

  <form [formGroup]="myForm"

     (ngSubmit)="onSubmit(myForm.value)"

     class="ui form">

   <div class="field">

    <label for="skuInput">SKU</label>

    <input type="text"

        id="skuInput"

        placeholder="SKU"

        [formControl]="myForm.controls['sku']">

   </div>

  <button type="submit" class="ui button">Submit</button>

  </form>

 </div>

 `

})

export class DemoFormSkuBuilder {

 myForm:FormGroup;

 constructor(fb:FormBuilder){

  this.myForm = fb.group({

   'sku':['ABC123']

  });

 }

 onSubmit(value:string):void {

  console.log('you submitted value:', value);

 }

}

你需要记住以下两点。

如果想隐式创建新的FormGroup和FormControl,使用:

●ngForm

●ngModel

如果要绑定一个现有的FormGroup和FormControl,使用:

●formGroup

●formControl

5.6 添加验证

用户输入的数据格式并不总是正确的。如果有人输入了错误的数据格式,我们希望给他反馈,并阻止他提交表单。因此,我们要用到验证器

验证器由Validators模块提供。Validators.required是最简单的验证,表明指定的字段是必填项,否则就认为这个FormControl是无效的。

想使用验证器,我们得做两件事:

(1)为FormControl对象指定一个验证器;

(2)在视图中检查验证器的状态,并据此采取行动。

要为FormControl对象分配一个验证器,我们可以直接把它作为第二个参数传给FormControl的构造函数。

let control = new FormControl('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]

  });

  this.sku = this.myForm.controls['sku'];

 }

现在要在视图中使用验证了。在视图中访问验证的值有以下两种方法。

(1)我们可以显式地把sku这个FormControl赋值给类的实例变量。这有点啰嗦,但便于我们在视图中访问这个FormControl。

(2)我们也可以在myForm中查找sku这个FormControl。这样能简化组件类中的工作,但在视图中会稍微麻烦些。

为了说明两者之间的差异,我们来看两个例子。

5.6.1 显式地把sku设置为实例变量

图5-3展示了这个带验证功能的表单应该是什么样子的。

图5-3 带验证器的演示表单

在视图中,处理单个FormControls的最灵活的方式是将每个FormControl都定义在组件类上。把sku定义在类上的代码如下所示。

code/forms/app/ts/forms/demo_form_with_validations_explicit.ts

export class DemoFormWithValidationsExplicit {

 myForm:FormGroup;

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

 }

}

注意:

(1)我们在类的顶部设置sku:AbstractControl;

(2)我们把用FormBuilder创建的myForm赋值给this.sku变量。

非常好,这意味着我们可以在组件视图中到处引用sku了。不过这样做有一个缺点:我们不得不为表单中的每个字段定义一个实例变量。对大型表单而言,这会显得相当啰嗦。

现在我们的sku可以得到验证了。我们要以四种不同的方式把它用在视图中:

(1)检查整个表单的有效性并显示一条错误信息;

(2)检查单个字段的有效性并显示一条错误信息;

(3)检查单个字段的有效性,当字段无效时将字段显示为红色;

(4)检查单个字段在特定规则下的有效性并显示一条错误信息。

表单信息

我们可以通过myForm.valid来检查整个表单的有效性。

code/forms/app/ts/forms/demo_form_with_validations_explicit.ts

    <div *ngIf="!sku.valid"

     class="ui error message">SKU is invalid</div>

记住,myForm是一个FormGroup;只有当里面所有的FormControl都有效时,这个FormGroup才有效。

字段信息

当字段的FormControl无效时,我们也可以为该字段显示一条错误信息。

code/forms/app/ts/forms/demo_form_with_validations_explicit.ts

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

字段着色

这里用的是Semantic UI CSS框架的CSS类.error。当给<div class= "field">节点加上CSS类error时,这个输入框就会带有红色的边框。

我们可以使用这种“属性语法”来有条件地设置这个CSS类。

code/forms/app/ts/forms/demo_form_with_validations_explicit.ts

  <div class="field"

    [class.error]="!sku.valid && sku.touched">

注意,这里我们为.error类设置了两个条件:检查!sku.valid和sku.touched。这是因为我们希望只有当用户修改过表单后(touched)才显示错误状态。

试着在input标签中输入一些数据,然后删除这个字段的内容。

特定验证

可能有很多原因导致一个表单字段无效。对于失败的验证,我们通常希望根据不同的原因显示不同的消息。

我们可以用hasError方法来检查特定的验证失败。

code/forms/app/ts/forms/demo_form_with_validations_explicit.ts

    <div *ngIf="sku.hasError('required')"

     class="ui error message">SKU is required</div>

注意,FormControl和FormGroup都定义了hasError方法。这意味着我们可以给它传入第二个参数path来在FormGroup中查询特定的字段。比如可以这样写:

    <div *ngIf="myForm.hasError('required', 'sku')"

     class="error">SKU is required</div>

整合

下面是我们把FormControl用作实例变量来实现验证功能的完整代码。

code/forms/app/ts/forms/demo_form_with_validations_explicit.ts

/* tslint:disable:no-string-literal */

import { Component } from '@angular/core';

import {

 FormBuilder,

 FormGroup,

 Validators,

 AbstractControl

} from '@angular/forms';

@Component({

 selector:'demo-form-with-validations-explicit',

 template:`

 <div class="ui raised segment">

  <h2 class="ui header">Demo Form:with validations(explicit)</h2>

  <form [formGroup]="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"

        [formControl]="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:FormGroup;

 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实例变量

在上面的例子中,我们将sku:AbstractControl设置为一个实例变量。通常,我们不希望为每一个AbstractControl控件都创建一个实例变量。在没有实例变量的情况下,我们该如何在视图中引用FormControl呢?

我们可以改用myForm.controls属性。

code/forms/app/ts/forms/demo_form_with_validations_shorthand.ts

    <input type="text"

       id="skuInput"

       placeholder="SKU"

       [formControl]="myForm.controls['sku']">

    <div *ngIf="!myForm.controls['sku'].valid"

     class="ui error message">SKU is invalid</div>

    <div *ngIf="myForm.controls['sku'].hasError('required')"

     class="ui error message">SKU is required</div>

通过这种方式,我们就不用被迫在组件类中显式定义实例变量来访问sku控件了。

5.6.2 自定义验证器

我们经常要写一些自定义验证器,下面来看看如何实现。

要明白如何实现自己的验证器,不妨看看Angular源代码中是如何实现Validators.required的:

export class Validators {

 static required(c:FormControl):StringMap<string, boolean> {

  return isBlank(c.value)|| c.value == "" ? {"required":true}:null;

 }

一个验证器:

●接收一个FormControl作为输入;

●当验证失败时,会返回一个StringMap<string, boolean>对象,它的键是“错误代码”,值是true。

编写验证器

假设我们的sku有特殊的验证需求,比如sku必须以123作为开始。我们写的验证器是这样的:

code/forms/app/ts/forms/demo_form_with_custom_validations.ts

function skuValidator(control:FormControl):{ [s:string]:boolean } {

 if(!control.value.match(/^123/)){

  return validSku:true;

 }

}

当输入值(控件的值control.value)不是以123作为开始时,验证器会返回错误代码invalidSku。

给FormControl分配验证器

现在要为FormControl添加验证,但是有一个小问题:sku已经有一个验证器了,怎样才能在同一个字段上添加多个验证器呢?

我们可以用Validators.compose来实现。

code/forms/app/ts/forms/demo_form_with_custom_validations.ts

 constructor(fb:FormBuilder){

  this.myForm = fb.group({

   'sku':['', Validators.compose([

    Validators.required, skuValidator])]

  });

Validators.compose把两个验证器包装在一起,我们可以将其赋值给FormControl。只有当两个验证都合法时,FormControl才是合法的。

现在就能在视图中使用这个新的验证器了。

code/forms/app/ts/forms/demo_form_with_custom_validations.ts

    <div *ngIf="sku.hasError('invalidSku')"

     class="ui error message">SKU must begin with <span>123</span></div>

 注意,我们在本节中为每个FormControl都显式添加了实例变量。这意味着,在本节的视图中sku引用的是一个FormControl。

运行示例代码,你会注意到有一点很奇妙:当你在字段中输入一些内容时,满足了required验证,但违反了invalidSku验证。棒极了,这意味着我们可以对字段进行部分验证并显示相应的信息。

5.7 监听变化

到目前为止,我们只在提交表单时才调用onSubmit方法来获取表单的值。但我们也要经常监听控件的变化。

FormGroup和FormControl都带有EventEmitter(事件发射器),我们可以通过它来观察变化。

 EventEmitter 是一个可观察对象,符合“变化监听”规范。如果你对可观察对象的规范感兴趣,可以参见https://github.com/jhusain/observable-spec。

想监听控件的变化,我们要:

(1)通过调用control.valueChanges访问到这个EventEmitter;

(2)然后使用.subscribe方法添加一个监听器

下面是一个例子。

code/forms/app/ts/forms/demo_form_with_events.ts

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

   }

  );

 }

在这里我们监听了两个事件:sku字段的变化和整个表单的变化。

我们传递了一个带有next键的对象(也可以传递其他键,但目前还不用关心它们)。next就是我们希望当值发生变化时被调用的函数。

如果在输入框中输入kj,就会在控制台中看到:

sku changed to: k

form changed to: Object {sku:"k"}

sku changed to: kj

form changed to: Object {sku:"kj"}

如你所见,每一次按键都会触发控件的变化,我们的可观察对象也会被触发。监听单个FormControl时,我们会得到一个值(例如kj);而监听整个表单时,我们会得到一个包含键值对的对象(例如{sku:"kj"})。

5.8 ngModel

ngModel是一个特殊的指令,它将模型绑定到表单中。ngModel的特殊之处在于它实现了双向绑定。相对于单向绑定来说,双向绑定更加复杂和难以推断。Angular通常的数据流向是单向的:自顶向下。但对于表单来说,双向绑定有时会更容易。

 不要仅仅因为你以前在AngularJS中用过ng-model而急于使用ngModel,因为有很多避免使用双向绑定的理由。当然,ngModel确实用起来更方便,但要记住Angular已经不像AngularJS那样必须依赖双向绑定了。

下面对表单稍作修改:我们希望能输入产品名称productName。这次要用ngModel来保持组件实例和视图的同步。

首先,我们的组件定义类如下所示。

code/forms/app/ts/forms/demo_form_ng_model.ts

export class DemoFormNgModel {

 myForm:FormGroup;

 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

   <label for="productNameInput">Product Name</label>

   <input type="text"

       id="productNameInput"

       placeholder="Product Name"

       [formControl]="myForm.get('productName')"

       [(ngModel)]="productName">

注意,这里ngModel的语法很有意思:我们在ngModel属性上同时使用了()和[]。我们既使用了表示输入属性(@Input)的方括号[],又使用了表示输出属性(@Output)的圆括号(),这就是双向绑定的标志。

另外还需要注意的是:我们仍然用formControl指定此input应该绑定到表单上的FormControl。这是因为ngModel只负责将input绑定到对象实例上,但FormControl的功能是与此独立的。由于我们还需要对这个值加以验证并把它作为表单的一部分提交上去,仍要保留formControl指令。

最后,我们把产品名称productName值显示在视图中。

code/forms/app/ts/forms/demo_form_ng_model.ts

  <div class="ui info message">

   The product name is:{{productName}}

  </div>

运行效果图如图5-4所示。

图5-4 带ngModel的演示表单

很简单吧!

5.9 总结

表单有很多零碎的知识,但Angular让它变得非常简明。只要我们掌握了如何使用FormGroup、FormControl和Validation,它就变得非常容易了!

  1. https://github.com/angular/angular/blob/master/modules/angular2/src/common/forms/model.ts
  2. http://semantic-ui.com/
  3. http://www.w3.org/TR/WCAG20-TECHS/H44.html