详解Angular2响应式表单
作者:余磊
本文将半翻译半总结的讲讲ng2官网的另一个未翻译高级教程页面。
文章目的是使用ng2提供的响应式表单技术快速搭出功能完善丰富的界面表单组件。
响应式表单是一项响应式风格的ng2技术,本文将解释响应式表单并用来创建一个英雄详情编辑器。
包含内容:
- 响应式表单介绍
- 开始搭建
- 创建数据模型
- 创建响应式的表单组件
- 创建组建的模板文件
- 引入ReactiveFormsModule
- 显示HeroDetailComponent
- 添加一个FormGroup
- 看看表单模型
- 介绍FormBuilder
- 验证的需求
- 放置FormGroup
- 检查FormControl属性
- 使用setValue以及patchValue设置表单模型数据
- 使用FotmArray提供FormGroup的数组
- 观察控件的更改
响应式表单介绍
angular提供了两种表单搭建技术: 响应式表单和模板驱动式表单。都依赖于@angular/forms库,并共享了一些通用的表单控件集。
但是他们在原理、代码风格以及技术上存在区别。他们甚至有自己的模块:ReactiveFormsModule以及FormsModule。
响应式表单(ReactiveFormsModule):
anguar的响应式表单简化了管理数据时响应式风格的编码实现,使用了在无视图数据模型(从服务器获取)以及以视图为导向的模型用于保持屏幕上HTML控件显示的值与状态。响应式表单提供了响应式模式测试以及验证上的便利。
使用响应式表单,你将在组件类中创建一个anular的表单控件树对象,并在组件模板中使用提供的技术绑定到原生表单控件标签中。
你直接在组件类中创建并操作控件对象。因为组件类能直接访问到数据模型以及表单控件结构,你可以将数据模型值推送到表单控件以及将用户的更改响应到后边来。组件可以观察表单控件状态的更改并响应这些更改。
直接使用表单控件对象工作的一个好处是值以及验证的更新总能够同步完成并受你控制。你不会遇到有时候因为模板驱动表单造成的时间问题,并且响应式表单更易测试。
为了保持响应的一致性,组件会保存不一致的数据模型,将其视为纯粹的原始值。不会直接更新数据模型,组件会提取用户的更改并转发到外面的组件或服务中,(可能是用来保存他们的)并返回一个新数据模型到组件,用于响应模型状态的更新。
使用响应式表单指令不需要你依赖于全部响应式原理,但是这确实能促进响应式编程方法如果你选择了要使用这个方法的话。
模板驱动式表单(FormsModule):
模板驱动式的表单使用了完全不同的方式。
你在组件模板中放置HTML表单控件(input这些)并使用比如ngModel这些指令绑定到数据模型属性。
你不需要创建angular表单控件对象,因为angular会根据你的数据绑定信息自动帮你创建出来。你不是推送或者拉取数据值。angular在ngModel中帮你处理了。angular会更新那些被改变的数据值。
出于这个原因,ngModel不再是ReactiveFormsModule的一部分了。
这意味着可以在组件类中写更少的代码,不过模板驱动表单是异步工作的,这可能会在某些情况下复杂化开发。
同步vs异步
响应式表单是同步的。模板驱动表单是异步的,这是其区别的根源。
在响应式表单中,你在代码中创建一个完整的表单控件树。你可以从子表单或父表单中立即更新或取回一个值,因为所有的控件都可访问到。
模板驱动表单将他们的表单控件的创建委托给了指令。为了避免“检查后又更改”的错误,这些指令使用了不止一个循环来建立整个控件树。这意味着你必须在操作任何组件类中的空间表单时等那么一小会儿。
比如说,如果你使用@ViewChild(NgForm)查询注入到表单控件中并在ngAfterViewInit这个生命周期钩子中检查它,你将发现它没有子元素。你必须等一会,使用setTimeout来等待,然后你才能从这个空间中去除值并验证它或者将它设置为新的值。
模板驱动表单的异步性同时复杂化了单元测试。你必须使用async()或者fakeAsync()来包装你的测试块来避免找不到表单的值。而如果使用的是响应式表单,一切都如你所愿的存在着。
哪一个方式更好?
没有哪种是更好的。他们是两种不同的搭建方式,各自拥有长处和短处。使用最适合你的方式才是对的。在一个应用中你可能两种方式都要使用到。
本文仅仅会描述响应式的范例与精华所在。对于模板驱动式表单,可以前往表单介绍页。
接下来你将写出你自己的项目来演示响应式表单。然后你将学会关于angular表单类以及如何在响应式表单中使用它。
对上文的总结就是,相比ng1中数据的双向绑定,ng2保留了这个双向绑定能力(底层其实优化了很多),原先的ng-model指令升级成了ngModel,使用的功能保持不变。
同时尽管ng2版本的数据双向绑定得到了很大的优化,仍改变不了其数据异步绑定的方式,因为ng2不能确定数据何时绑定,我们也不能确定很多网络请求得到的数据到来的时间。
在ng1中其实这个机制会有一些尴尬的场景,至少笔者在一些情况下不得不在一些业务场景下使用setTimeout来保证数据已经成功绑定进入scope的watch循环,但这个异步绑定数据又是不可避免的,除非我们自己来适应实际项目改写angular代码了。
所以ng2就提供了让我们配合具体项目场景改写ngModel的能力,也就是原文介绍的响应式表单。
其跟ngModel的关系就是,ngModel是响应式表单的官方实现,其在我们绑定数据时自动为我们实现响应式表单中用到的几个机制,如果我们需要数据严格的实时同步绑定,就不必使用ngModel,可以亲自来编写响应式的表单,步骤覆盖了组件模板到数据模型类整条龙,而这么多事情在合适的场景下使用ngModel已经可以实现了,这两种表单绑定的方式各有其优势。
1. 使用响应式表单
响应式表单的能力封装在ReactiveFormsModule中,并且跟FormsModule同时包含在@angular/forms这个包中。
表单类的要点:
1.AbstractControl是FormControl、FormGroup、FormArray这三个实例表单类的抽象基类。它提供了他们的通用行为以及属性,其中就有observable。
2.FormControl在单个表单控件中检查值并验证状态。它负责将其通知给HTML表单控件(比如input这些)。
3.FormGroup负责AbstractControl实例的一个组的值与验证状态。组的属性包含了它们的子控件。你的组件的顶级表单就是一个FormGroup。
4.FormArray负责AbstractControl实例的数值索引数组的值与状态验证。
2. FormControl
最核心的指令就是FormControl,算是底层的ngModel,在模板标签中跟定义好的数据模型字段绑定起来,就像这样:
<h2>Hero Detail</h2> <h3><i>Just a FormControl</i></h3> <label class="center-block">Name: <input class="form-control" [formControl]="name"> </label>
同时在组件代码中需要将上例中这个name字段声明为FormControl类:
export class HeroDetailComponent1 { name = new FormControl(); }
3. FormGroup
将多个FormControl对象分组到FormGroup中,用来方便管理。定义方法如下:
import { Component } from '@angular/core'; import { FormControl, FormGroup } from '@angular/forms'; export class HeroDetailComponent2 { heroForm = new FormGroup ({ name: new FormControl() }); }
此时模板标签中也要分组来写:
<h2>Hero Detail</h2> <h3><i>FormControl in a FormGroup</i></h3> <form [formGroup]="heroForm" novalidate> <div class="form-group"> <label class="center-block">Name: <input class="form-control" formControlName="name"> </label> </div> </form>
现在的效果就是可以从heroForm中实时读取到其值和一些附加的状态的变化。
可以在模板中添加两个标签来展示数据的更改:
<p>Form value: {{ heroForm.value | json }}</p> <p>Form status: {{ heroForm.status | json }}</p>
4. FormBuilder
还有个新概念就是FormBuilder,是用来帮助创建表单类的:
1. 显示声明heroForm属性的类型为FormGroup,后面你将会初始化它
2. 注入FormBuilder到构造器中
3. 使用FormsBuilder添加新方法定义heroForm,叫做createForm。
4. 在构造器中执行createForm方法。
export class HeroDetailComponent3 { heroForm: FormGroup; // <--- heroForm is of type FormGroup constructor(private fb: FormBuilder) { // <--- inject FormBuilder this.createForm(); } createForm() { this.heroForm = this.fb.group({ name: '', // <--- the FormControl called "name" }); } }
上例中执行createForm方法即可动态快速的创建出表单类,其在一些表单类需要更改的场景下可以使用。
5. setValue和patchValue
这两个方法是真正给表单模型赋值用的。因为表单显示的数据与真实的底层数据肯定不能使同一个,否则表单输入数据一旦更改,源数据就被污染了,而这两个方法就是用来将源数据赋值到表单模型数据上的。
每当需要赋值时就可以调用,其中setValue必须准确赋值,并且会在数据不匹配时报告错误;而patchValue没有这么严格,但可以传一个对象,且不匹配时不会报告错误。
而我们要做的就是在ng2组件的ngOnChanges回调中手动执行setValue设置数据值。比如这样:
ngOnChanges() this.heroForm.setValue({ name: this.hero.name, address: this.hero.addresses[0] || new Address() }); }
同时ng2还提供了一个reset方法来重新调用setValue方法(setValue本身好像只是用来一次性赋值的)。
6. FormArray
FormArray是用来对付FormGroups列表的,比如一个英雄有可能有多个address字段,address字段本身就是个FormGroup,此时就要用到FormArray了:
this.heroForm = this.fb.group({ name: ['', Validators.required ], secretLairs: this.fb.array([]), // <-- secretLairs as an empty FormArray power: '', sidekick: '' });
获取FormArray要用到FormGroup提供的一个get方法:
get secretLairs(): FormArray { return this.heroForm.get('secretLairs') as FormArray; };
其显示的模板如下:
<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以后就可以使用ngFor把这一个表单列表遍历出来了(直接使用ngModel加ngFor能省不少事情...)。
总结:
笔者目前使用ng2还没涉及到比较复杂的表单,所以对原文提到的内容理解并不是很深,加上老外写的文章通常都过于完整,都喜欢用长篇大论来说明一个简单的知识点,所以本文后半段其实没多少原文的影子,纯属笔者自己拙劣的概括,并且没有做过太多实践。
回看ng2的响应式表单能力,提供的指令以及服务也就这么几个(FormControl、FormGroup、FormArray、FormBuilder以及几个功能性方法),巧妙在使用这些能力就能完成一个强大的表单界面,其编码体验绝对是远超传统jQuery强行从DOM读取节点值的方式的,并且提供了除了简单的ngModel能力之外的更具体更强大的数据绑定方案,还有本文未提及的表单验证这个大内容,在ng2提供的这个响应式表单方案下实现起来也是很得心应手的。
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持脚本之家。