360SDN.COM

Angular响应式表单

来源:  2018-04-20 15:13:49    评论:0点击:

响应式表单

响应式表单是 Angular 中用响应式风格创建表单的技术。 本章会在构建“英雄详情编辑器”的过程中,逐步讲解响应式表单的概念。

试试响应式表单的在线例子 / 下载范例

你还可以运行响应式表单的演示程序 / 下载范例,并从顶部选取一个中间步骤。

响应式表单简介

Angular 提供了两种构建表单的技术:响应式表单和模板驱动表单。 这两项技术都属于 @angular/forms 库,并且共享一组公共的表单控件类。

但是它们在设计哲学、编程风格和具体技术上有显著区别。 所以,它们都有自己的模块:ReactiveFormsModuleFormsModule

响应式表单

Angular 的响应式表单能让实现响应式编程风格更容易,这种编程风格更倾向于在非 UI 的数据模型(通常接收自服务器)之间显式的管理数据流, 并且用一个 UI 导向的表单模型来保存屏幕上 HTML 控件的状态和值。 响应式表单可以让使用响应式编程模式、测试和校验变得更容易。

使用响应式表单,你可以在组件中创建表单控件的对象树,并使用本章中传授的技巧把它们绑定到组件模板中的原生表单控件元素上。

你可以在组件类中直接创建和维护表单控件对象。由于组件类可以同时访问数据模型和表单控件结构, 因此你可以把表单模型值的变化推送到表单控件中,并把变化后的值拉取回来。 组件可以监听表单控件状态的变化,并对此做出响应。

直接使用表单控件对象的优点之一是值和有效性状态的更新总是同步的,并且在你的控制之下。 你不会遇到时序问题,这个问题有时在模板驱动表单中会成为灾难。而且响应式表单更容易进行单元测试。

在响应式编程范式中,组件会负责维护数据模型的不可变性,把模型当做纯粹的原始数据源。 组件不会直接更新数据模型,而是把用户的修改提取出来,把它们转发给外部的组件或服务,外部程序才会使用这些进行处理(比如保存它们), 并且给组件返回一个新的数据模型,以反映模型状态的变化。

使用响应式表单的指令,并不要求你遵循所有的响应式编程原则,但它能让你更容易使用响应式编程方法,从而更愿意使用它。

模板驱动表单

模板一章中介绍过的模板驱动表单,是一种完全不同的方式。

你把 HTML 表单控件(比如 <input><select>)放进组件模板中,并用 ngModel 等指令把它们绑定到组件中数据模型的属性上。

你不用自己创建 Angular 表单控件对象。Angular 指令会使用数据绑定中的信息创建它们。 你不用自己推送和拉取数据。Angular 使用 ngModel 来替你管理它们。 当用户做出修改时,Angular 会据此更新可变的数据模型

因此,ngModel 并不是 ReactiveFormsModule 模块的一部分。

虽然这意味着组件中的代码更少,但是模板驱动表单是异步工作的,这可能在更高级的场景中让开发复杂化。

异步 vs. 同步

响应式表单是同步的而模板驱动表单是异步的。

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

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

比如,如果你用 @ViewChild(NgForm) 查询来注入表单控件,并在生命周期钩子 ngAfterViewInit中检查它,就会发现它没有子控件。 你必须使用 setTimeout 等待一个节拍才能从控件中提取值、测试有效性,或把它设置为新值。

模板驱动表单的异步性让单元测试也变得复杂化了。 你必须把测试代码包裹在 async()fakeAsync() 中来解决要查阅的值尚不存在的情况。 使用响应式表单,在所期望的时机一切都是可用的。

选择响应式表单还是模板驱动表单?

响应式表单和模板驱动表单是两种架构范式,各有优缺点。 请自行选择更合适的方法,甚至可以在同一个应用中同时使用它们。

本章其余的部分只专注于响应式范式以及响应式表单技术的详情。 要了解关于模板驱动表单的更多信息,参见表单一章。

在下一节,你要先准备一个响应式表单范例的项目,然后就可以开始学习Angular 表单类,并在响应式表单中使用它们了。

准备工作

创建一个名叫angular-reactive-forms的新项目:

      
ng new angular-reactive-forms
    

创建数据模型

本章的焦点是响应式表单组件以及编辑一个英雄。 你需要一个 Hero 类和一些英雄数据。

使用 CLI 创建一个名叫 data-model 的新类:

      
ng generate class data-model
    

并把下列内容复制到 data-model.ts 中:

src/app/data-model.ts

src/app/data-model.ts

      

export class Hero {
  id = 0;
  name = '';
  addresses: Address[];
}

export class Address {
  street = '';
  city   = '';
  state  = '';
  zip    = '';
}

export const heroes: Hero[] = [
  {
    id: 1,
    name: 'Whirlwind',
    addresses: [
      {street: '123 Main',  city: 'Anywhere', state: 'CA',  zip: '94801'},
      {street: '456 Maple', city: 'Somewhere', state: 'VA', zip: '23226'},
    ]
  },
  {
    id: 2,
    name: 'Bombastic',
    addresses: [
      {street: '789 Elm',  city: 'Smallville', state: 'OH',  zip: '04501'},
    ]
  },
  {
    id: 3,
    name: 'Magneta',
    addresses: [ ]
  },
];

export const states = ['CA', 'MD', 'OH', 'VA'];

这个文件导出两个类和两个常量。AddressHero 类定义应用的数据模型heroesstates 常量提供测试数据。

创建响应式表单组件

生成一个名叫 HeroDetail 的新组件:

      
ng generate component HeroDetail
    

并导入:

src/app/hero-detail/hero-detail.component.ts
      
import { FormControl } from '@angular/forms';
    

接下来,创建并导出一个带 FormControlHeroDetailComponent 类。 FormControl 是一个指令,它允许你直接创建并管理一个 FormControl 实例。

src/app/hero-detail/hero-detail.component.ts (excerpt)
      
export class HeroDetailComponent1 {
  name = new FormControl();
}
    

这里创建了一个名叫 nameFormControl。 它将会绑定到模板中的一个 <input> 元素,表示英雄的名字。

FormControl 构造函数接收三个可选参数: 初始值、验证器数组和异步验证器数组。

最简单的控件并不需要数据或验证器,但是在实际应用中,大部分表单控件都会同时具备它们。 要想深入了解 Validators,参见表单验证一章。

创建模板

现在,把组件的模板文件 src/app/hero-detail.component.html 修改为如下内容:

src/app/hero-detail/hero-detail.component.html

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

要让 Angular 知道你希望把这个输入框关联到类中的 FormControl 型属性 name,就要在模板中的 <input> 上加一句 [formControl]="name"

请忽略 CSS 类 form-control,它属于Bootstrap CSS library而不是 Angular。 它会为表单添加样式,但是对表单的逻辑毫无影响。

导入 ReactiveFormsModule

HeroDetailComponent 的模板中使用了来自 ReactiveFormsModuleformControlName

app.module.ts 中做了下面两件事:

  1. 使用 JavaScript 的 import 语句访问 ReactiveFormsModuleHeroDetailComponent

  2. ReactiveFormsModule 添加到 AppModuleimports 列表中。

src/app/app.module.ts (excerpt)
      
import { NgModule }            from '@angular/core';
import { BrowserModule }       from '@angular/platform-browser';
import { ReactiveFormsModule } from '@angular/forms';  // <-- #1 import module

import { AppComponent }        from './app.component';
import { HeroDetailComponent } from './hero-detail/hero-detail.component';

@NgModule({
  declarations: [
    AppComponent,
    HeroDetailComponent,
  ],
  imports: [
    BrowserModule,
    ReactiveFormsModule // <-- #2 add to @NgModule imports
  ],
  bootstrap: [ AppComponent ]
})
export class AppModule { }

显示 HeroDetailComponent

修改 AppComponent 的模板,以便显示 HeroDetailComponent

src/app/app.component.html
      
<div class="container">
  <h1>Reactive Forms</h1>
  <app-hero-detail></app-hero-detail>
</div>
    

基础的表单类

本文使用四个基础类来构建响应式表单:

CSS 类

说明

AbstractControl

AbstractControl是这三个具体表单类的抽象基类。 并为它们提供了一些共同的行为和属性。

FormControl

FormControl 用于跟踪一个单独的表单控件的值和有效性状态。它对应于一个 HTML 表单控件,比如 <input><select>

FormGroup

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

FormArray

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

为应用添加样式

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

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

这些做好之后,启动应用服务器:

      
ng serve
    

浏览器应该显示成这样:

Single FormControl

添加 FormGroup

通常,如果有多个 FormControl,你要把它们都注册进一个父 FormGroup 中。 只要把它添加到 hero-detail.component.tsimports 区就可以了。

src/app/hero-detail/hero-detail.component.ts
      
import { Component }              from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';


在这个类中,把 FormControl 包裹进了一个名叫 heroFormFormGroup 中,代码如下:

src/app/hero-detail/hero-detail.component.ts
      export class HeroDetailComponent2 {
  heroForm = new FormGroup ({
    name: new FormControl()
  });
}
    

现在你改完了这个类,该把它映射到模板中了。把 hero-detail.component.html 改成这样:

src/app/hero-detail/hero-detail.component.html
 <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>
    

注意,现在单行输入框位于一个 form 元素中。

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

由于现在有了一个 FormGroup,因此你必须修改模板语法来把这个 <input> 关联到组件类中对应的 FormControl 上。 以前没有父 FormGroup 的时候,[formControl]="name" 也能正常工作,因为该指令可以独立工作,也就是说,不在 FormGroup 中时它也能用。 有了 FormGroupname 这个 <input> 就需要再添加一个语法 formControlName=name,以便让它关联到类中正确的 FormControl 上。 这个语法告诉 Angular,查阅父 FormGroup(这里是 heroForm),然后在这个 FormGroup 中查阅一个名叫 nameFormControl

表单模型概览

当用户在 <input> 中输入数据时,它的值就会进入这个表单模型。 要想知道表单模型是什么样的,请在 hero-detail.component.html<form> 标签紧后面添加如下代码:

src/app/hero-detail/hero-detail.component.html
      
<p>Form value: {{ heroForm.value | json }}</p>
    

heroForm.value 会返回表单模型。 用 JsonPipe 管道把这个模型以 JSON 格式渲染到浏览器中。

JSON output

最初的 name 属性是个空字符串,在 name <input> 中输入之后,可以看到这些按键出现在了 JSON 中。

在真实的应用中,表单很快就会变大。 FormBuilder 能让表单开发和维护变得更简单。

FormBuilder 简介

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

要使用 FormBuilder,就要先把它导入到 hero-detail.component.ts 中。你可以删除 FormControl

src/app/hero-detail/hero-detail.component.ts (excerpt)
      
import { Component }              from '@angular/core';
import { FormBuilder, FormGroup } from '@angular/forms';
    

遵循下列步骤来用 FormBuilderHeroDetailComponent 重构得更容易读写:

  • 明确把 heroForm 属性的类型声明为 FormGroup,稍后你会初始化它。

  • FormBuilder 注入到构造函数中。

  • 添加一个名叫 createForm() 的新方法,它会用 FormBuilder 来定义 heroForm

  • 在构造函数中调用 createForm()

修改过的 HeroDetailComponent 代码如下:

src/app/hero-detail/hero-detail.component.ts (excerpt)
    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"
    });
  }
}

FormBuilder.group 是一个用来创建 FormGroup 的工厂方法,它接受一个对象,对象的键和值分别是 FormControl 的名字和它的定义。 在这个例子中,name 控件的初始值是空字符串。

把一组控件定义在一个单一对象中,可以让你的代码更加紧凑、易读。 因为你不必写一系列重复的 new FormControl(...) 语句。

Validators.required

虽然本章不会深入讲解验证机制,但还是有一个例子来示范如何简单的在响应式表单中使用 Validators.required

首先,导入 Validators 符号。

src/app/hero-detail/hero-detail.component.ts (excerpt)
      
import { Component }                          from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
    

要想让 name 这个 FormControl 是必须的,请把 FormGroup 中的 name 属性改为一个数组。第一个条目是 name 的初始值,第二个是 required 验证器:Validators.required

src/app/hero-detail/hero-detail.component.ts (excerpt)
      
this.heroForm = this.fb.group({
  name: ['', Validators.required ],
});
    

响应式验证器是一些简单、可组合的函数。 在模板驱动表单中配置验证器有些困难,因为你必须把验证器包装进指令中。

修改模板底部的诊断信息,以显示表单的有效性状态。

src/app/hero-detail/hero-detail.component.html (excerpt)
      
<p>Form value: {{ heroForm.value | json }}</p>
<p>Form status: {{ heroForm.status | json }}</p>
    

浏览器会显示下列内容:

Single FormControl

Validators.required 生效了,但状态还是 INVALID,因为输入框中还没有值。 在输入框中输入,就会看到这个状态从 INVALID 变成了 VALID

在真实的应用中,你要把这些诊断信息替换成用户友好的信息。

在本章的其余部分,Validators.required 是可有可无的,但在每个与此范例配置相同的范例中都会保留它。

要了解 Angular 表单验证器的更多知识,参见表单验证器一章。

更多的 FormControl

本节要添加一些 FormControl,用来表示住址、一项超能力,和一个副手。

另外,住址中有一个所在州属性,用户将会从 <select> 框中选择一个州,你会用 <option> 元素渲染各个州。从 data-model.ts 中导入 states(州列表)。

src/app/hero-detail/hero-detail.component.ts (excerpt)
     import { Component }                          from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';

import { states } from '../data-model';

声明 states 属性并往 heroForm 中添加一些表示住址的 FormControl,代码如下:

src/app/hero-detail/hero-detail.component.ts (excerpt)
    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: ''
    });
  }
}

然后把下列代码添加到 hero-detail.component.html 文件中。

src/app/hero-detail/hero-detail.component.html
  <h2>Hero Detail</h2>
<h3><i>A FormGroup with multiple FormControls</i></h3>
<form [formGroup]="heroForm">
  <div class="form-group">
    <label class="center-block">Name:
      <input class="form-control" formControlName="name">
    </label>
  </div>
  <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 class="form-group radio">
    <h4>Super power:</h4>
    <label class="center-block"><input type="radio" formControlName="power" value="flight">Flight</label>
    <label class="center-block"><input type="radio" formControlName="power" value="x-ray vision">X-ray vision</label>
    <label class="center-block"><input type="radio" formControlName="power" value="strength">Strength</label>
  </div>
  <div class="checkbox">
    <label class="center-block">
      <input type="checkbox" formControlName="sidekick">I have a sidekick.
    </label>
  </div>
</form>


<p>Form value: {{ heroForm.value | json }}</p>

注意:不用管这些脚本中提到的 form-groupform-controlcenter-blockcheckbox 等。 它们是来自 Bootstrap 的 CSS 类,Angular 本身不会管它们。 注意 formGroupNameformControlName 属性。 他们是 Angular 指令,用于把相应的 HTML 控件绑定到组件中的 FormGroupFormControl 类型的属性上。

修改过的模板包含更多文本输入框,一个 state 选择框,power(超能力)的单选按钮和一个 sidekick 检查框。

你要用 [value]="state" 来绑定 <option>value 属性。 如果不绑定这个值,这个选择框就会显示来自数据模型中的第一个选项。

组件定义了控件属性而不用管它们在模板中的表现形式。 你可以像定义 name 控件一样定义 statepowersidekick 控件,并用 formControlName 指令来指定 FormControl 的名字。

参见 API 参考手册中的radio buttonsselectscheckboxes

多级 FormGroup

要想更有效的管理这个表单的大小,你可以把一些相关的 FormControl 组织到多级 FormGroup 中。 比如,streetcitystatezip 就可以作为一个名叫 addressFormGroup 中的理想属性。 用这种方式,多级表单组和控件可以让你轻松地映射多层结构的数据模型,以帮你跟踪这组相关控件的有效性和状态。

你用 FormBuilder 在这个名叫 heroForm 的组件中创建一个 FormGroup,并把它用作父 FormGroup。 再次使用 FormBuilder 创建一个子级 FormGroup,其中包括这些住址控件。把结果赋值给父 FormGroup 中新的 address 属性。

src/app/hero-detail/hero-detail.component.ts (excerpt)
      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: ''
    });
  }
}
    

当你修改组件类中表单控件的结构时,还必须对组件模板进行相应的调整。

hero-detail.component.html 中,把与住址有关的 FormControl 包裹进一个 div 中。 往这个 <div> 上添加一个 formGroupName 指令,并且把它绑定到 "address" 上。 这个 address 属性是一个 FormGroup,它的父 FormGroup 就是 heroForm。 把这个 name <input> 留在此 <div> 中。

要让这个变化更加明显,在文本的顶部加入一个 <h4> 头:Secret Lair。 新的住址组的 HTML 如下:

src/app/hero-detail/hero-detail.component.html (excerpt)
  <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>
    

做完这些之后,浏览器中的 JSON 输出就变成了带有多级 FormGroup 的表单模型。

JSON output

这时模板和表单模型在彼此通讯了。

查看 FormControl 的属性

你可以使用 .get() 方法来提取表单中一个单独 FormControl 的状态。 你可以在组件类中这么做,或者通过往模板中添加下列代码来把它显示在页面中,就添加在 {{form.value | json}} 插值表达式的紧后面:

src/app/hero-detail/hero-detail.component.html
      
<p>Name value: {{ heroForm.get('name').value }}</p>
    

要点取得 FormGroup 中的 FormControl 的状态,使用点语法来指定到控件的路径。

src/app/hero-detail/hero-detail.component.html
      
<p>Street value: {{ heroForm.get('address.street').value}}</p>
    

注意:如果你正在边看边跟着写代码,当你到达 FormArray 那节时,别忘了移除到 address.street 的引用。那一节中,你要在组件类中修改这个地址的名字,如果你把它留在模板中,就会抛出一个错误。

你可以使用此技术来显示 FromControl 的任意属性,代码如下:

属性

说明

myControl.value

FormControl 的值。

myControl.status

FormControl 的有效性。可能的值有 VALIDINVALIDPENDINGDISABLED

myControl.pristine

如果用户尚未改变过这个控件的值,则为 true。它总是与 myControl.dirty 相反。

myControl.untouched

如果用户尚未进入这个 HTML 控件,也没有触发过它的 blur(失去焦点)事件,则为 true。 它是 myControl.touched 的反义词。

要了解 FormControl 的更多属性,参见 API 参考手册的AbstractControl部分。

检查 FormControl 属性的另一个原因是确保用户输入了有效的值。 要了解更多关于 Angular 表单验证的知识,参见表单验证一章。

数据模型与表单模型

此刻,表单显示的是空值。 HeroDetailComponent 应该显示一个英雄的值,这个值可能接收自远端服务器。

在这个应用中,HeroDetailComponent 从它的父组件 HeroListComponent 中取得一个英雄。

来自服务器的 hero 就是数据模型,而 FormControl 的结构就是表单模型

组件必须把数据模型中的英雄值复制到表单模型中。这里隐含着两个非常重要的点。

  1. 开发人员必须理解数据模型是如何映射到表单模型中的属性的。

  2. 用户修改时的数据流是从 DOM 元素流向表单模型的,而不是数据模型。

表单控件永远不会修改数据模型

表单模型和数据模型的结构并不需要精确匹配。在一个特定的屏幕上,你通常只会展现数据模型的一个子集。 但是表单模型的形态越接近数据模型,事情就会越简单。

HeroDetailComponent 中,这两个模型是非常接近的。

回忆一下 data-model.tsHeroAddress 的 定义:

src/app/data-model.ts (classes)
      export class Hero {
  id = 0;
  name = '';
  addresses: Address[];
}

export class Address {
  street = '';
  city   = '';
  state  = '';
  zip    = '';
}

这里又是组件的 FormGroup 定义。

src/app/hero-detail/hero-detail.component.ts (excerpt)
      
this.heroForm = this.fb.group({
  name: ['', Validators.required ],
  address: this.fb.group({
    street: '',
    city: '',
    state: '',
    zip: ''
  }),
  power: '',
  sidekick: ''
});

在这些模型中有两点显著的差异:

  1. Hero 有一个 id。表单模型中则没有,因为你通常不会把主键展示给用户。

  2. Hero 有一个住址数组。这个表单模型只表示了一个住址,稍后的 FormArray则可以表示多个。

保持这两个模型的形态尽可能接近,可以在下一节中轻松使用 patchValue()setValue() 方法把数据模型拷贝到表单模型中。

首先把 address 这个 FormGroup 的定义重构成这样:

src/app/hero-detail/hero-detail.component.ts
    this.heroForm = this.fb.group({
  name: ['', Validators.required ],
  address: this.fb.group(new Address()), // <-- a FormGroup with a new address
  power: '',
  sidekick: ''
});
    

为了确保从 data-model 中导入,你可以引用 HeroAddress 类:

src/app/hero-detail/hero-detail.component.ts
      
import { Address, Hero, states } from '../data-model';
    

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

注意:如果你正在跟着写代码,那么本节是可选的,因为剩下的步骤并不依赖它。

以前,你创建了控件,并同时初始化它的值。 你也可以稍后用 setValue()patchValue() 来初始化或重置这些值。

setValue()

借助setValue(),你可以设置每个表单控件的值,只要把与表单模型的属性精确匹配的数据模型传进去就可以了。

src/app/hero-detail/hero-detail.component.ts (excerpt)
         

this.heroForm.setValue({
   name:    this.hero.name,
   address: this.hero.addresses[0] || new Address()
});
   

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

它不会接受一个与 FormGroup 结构不同或缺少表单组中任何一个控件的数据对象。 这种方式下,如果你有什么拼写错误或控件嵌套的不正确,它就能返回一些有用的错误信息。 反之,patchValue() 会默默地失败。

注意,你几乎可以直接把这个 hero 用作 setValue() 的参数,因为它的形态与组件的 FormGroup 结构是非常像的。

你现在只能显示英雄的第一个住址,不过你还必须考虑 hero 完全没有住址的可能性。 就像这个在数据对象参数中对 address 属性进行有条件的设置:

src/app/hero-detail/hero-detail.component.ts
      
address: this.hero.addresses[0] || new Address()
    

patchValue()

借助patchValue(),你可以通过提供一个只包含要更新的控件的键值对象来把值赋给 FormGroup 中的指定控件。

这个例子只会设置表单的 name 控件。

src/app/hero-detail/hero-detail.component.ts (excerpt)
      
this.heroForm.patchValue({
  name: this.hero.name
});
    

借助patchValue(),你可以更灵活地解决数据模型和表单模型之间的差异。 但是和 setValue() 不同,patchValue() 不会检查缺失的控件值,并且不会抛出有用的错误信息。

创建 HeroListComponentHeroService

要更好地演示后面的响应式表单技巧,可以通过加入 HeroListComponentHeroService 来为这个范例添加更多功能。

HeroDetalComponent 是一个嵌套在 HeroListComponent主从视图中的子组件。如果把它们放在一起就是这样的:

HeroListComponent

首先使用下列命令添加一个 HeroListComponent

      
ng generate component HeroList
    

HeroListComponent 修改为如下内容:

hero-list.component.ts
  import { Component, OnInit } from '@angular/core';
import { Observable }        from 'rxjs';
import { finalize } from 'rxjs/operators';

import { Hero }        from '../data-model';
import { HeroService } from '../hero.service';

@Component({
  selector: 'app-hero-list',
  templateUrl: './hero-list.component.html',
  styleUrls: ['./hero-list.component.css']
})
export class HeroListComponent implements OnInit {
  heroes: Observable<Hero[]>;
  isLoading = false;
  selectedHero: Hero;

  constructor(private heroService: HeroService) { }

  ngOnInit() { this.getHeroes(); }

  getHeroes() {
    this.isLoading = true;
    this.heroes = this.heroService.getHeroes()
                      // Todo: error handling
                      .pipe(finalize(() => this.isLoading = false));
    this.selectedHero = undefined;
  }

  select(hero: Hero) { this.selectedHero = hero; }
}

接着使用下列命令添加 HeroService

      
ng generate service Hero
    

然后,把它的内容改为:

hero.service.ts
   import { Injectable } from '@angular/core';

import { Observable, of } from 'rxjs';
import { delay } from 'rxjs/operators';

import { Hero, heroes } from './data-model';

@Injectable()
export class HeroService {

  delayMs = 500;

  // Fake server get; assume nothing can go wrong
  getHeroes(): Observable<Hero[]> {
    return of(heroes).pipe(delay(this.delayMs)); // simulate latency with delay
  }

  // Fake server update; assume nothing can go wrong
  updateHero(hero: Hero): Observable<Hero>  {
    const oldHero = heroes.find(h => h.id === hero.id);
    const newHero = Object.assign(oldHero, hero); // Demo: mutate cached hero
    return of(newHero).pipe(delay(this.delayMs)); // simulate latency with delay
  }
}
    

HeroListComponent 使用一个注入进来的 HeroService 来从服务器获取英雄列表,然后用一系列按钮把这些英雄展示给用户。 HeroService 模拟了 HTTP 服务。 它返回一个英雄组成的 Observable 对象,并会在短暂的延迟之后被解析出来,这是为了模拟网络延迟,并展示应用在自然延迟下的异步效果。

当用户点击一个英雄时,组件设置它的 selectedHero 属性,它绑定到 HeroDetailComponent@Input() 属性 hero 上。 HeroDetailComponent 检测到英雄的变化,并使用当前英雄的值重置此表单。

"刷新"按钮会清除英雄列表和当前选中的英雄,然后重新获取英雄列表。

注意,hero-list.component.tsrxjs 中导入了 Observablefinally,而 hero.service.ts 导入了 Observableofdelay

HeroListComponentHeroService 的其它实现细节超出了本教程的范围。 不过,它所涉及的技术包含在文档的其它部分,包括《英雄指南》的 这里这里

要使用 HeroService,就要把它导入到 AppModule 中,并添加到 providers 数组里。 要使用 HeroListComponent,就要导入它、声明它并导出它:

app.module.ts (excerpts)
   // add JavaScript imports
import { HeroListComponent }   from './hero-list/hero-list.component';
import { HeroService }         from './hero.service';

@NgModule({
  declarations: [
    AppComponent,
    HeroDetailComponent,
    HeroListComponent // <--declare HeroListComponent
  ],
  // ...
  exports: [
    AppComponent,
    HeroDetailComponent,
    HeroListComponent // <-- export HeroListComponent
  ],
  providers: [ HeroService ], // <-- provide HeroService

接下来,把 HeroListComponent 的模板升级为:

hero-list.component.html
   <h3 *ngIf="isLoading"><i>Loading heroes ... </i></h3>
<h3 *ngIf="!isLoading">Select a hero:</h3>

<nav>
  <button (click)="getHeroes()" class="btn btn-primary">Refresh</button>
  <a *ngFor="let hero of heroes | async" (click)="select(hero)">{{hero.name}}</a>
</nav>

<div *ngIf="selectedHero">
  <hr>
  <h2>Hero Detail</h2>
  <h3>Editing: {{selectedHero.name}}</h3>
  <app-hero-detail [hero]="selectedHero"></app-hero-detail>
</div>

这些修改需要反映到 AppComponent 模板中。把 app.component.html 替换为如下内容,以便把 HeroDetailComponent 替换为 HeroListComponent

app.component.html
<div class="container">
  <h1>Reactive Forms</h1>
  <app-hero-list></app-hero-list>
</div>

最后,为 HeroDetailComponent 添加一个 @Input() 属性,让它能从 HeroListComponent 中接收数据。 别忘了也要把来自 @angular/coreInput 符号 import 进来。

hero-detail.component.ts (excerpt)
      
@Input() hero: Hero;
    

你先,你就可以点击一个按钮并渲染一个表单了。

何时设置表单模型的值(ngOnChanges

何时设置表单模型的值取决于组件何时获得数据模型的值。

HeroListComponent 会给用户显示英雄的名字。 当用户点击某个英雄时,HeroListComponent 会通过绑定到 hero 这个输入属性,把选中的英雄传给 HeroDetailComponent

hero-list.component.html (simplified)
   <nav>
  <a *ngFor="let hero of heroes | async" (click)="select(hero)">{{hero.name}}</a>
</nav>

<div *ngIf="selectedHero">
  <app-hero-detail [hero]="selectedHero"></app-hero-detail>
</div>

这种方式下,每当用户选择一个新英雄时,HeroDetailComponenthero 值就会发生变化。 你可以通过 ngOnChanges 生命周期钩子来调用 setValue()。只要 hero 这个输入属性发生了变化,Angular 就会调用这个钩子。

重置表单

首先,在 hero-detail.component.ts 中导入 OnChanges 符号。

src/app/hero-detail/hero-detail.component.ts (core imports)
      
import { Component, Input, OnChanges }             from '@angular/core';
    

接着,让 Angular 知道 HeroDetailComponent 实现了 OnChanges

src/app/hero-detail/hero-detail.component.ts (excerpt)
      
export class HeroDetailComponent implements OnChanges {
    

向该类中添加 ngOnChanges 方法,代码如下:

src/app/hero-detail/hero-detail.component.ts (ngOnchanges)
      
ngOnChanges() {
  this.rebuildForm();
}
    

注意,它调用了 rebuildForm(),该函数是一个方法,在这里你可以对值进行设置。 你可以把 rebuildForm() 命名为任何对你有意义的名字。 它不是 Angular 内置的,而是你自己创建的方法,用以更有效的利用 ngOnChanges 钩子。

src/app/hero-detail/hero-detail.component.ts
      
rebuildForm() {
  this.heroForm.reset({
    name: this.hero.name,
    address: this.hero.addresses[0] || new Address()
  });
}
    

rebuildForm() 方法会做两件事:重置英雄的名字和地址。

使用 FormArray 来表示 FormGroup 数组

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

有时你需要表示任意数量的控件或控件组。 比如,一个英雄可能拥有 0、1 或任意数量的住址。

Hero.addresses 属性就是一个 Address 实例的数组。 一个 addressFormGroup 可以显示一个 Address 对象。 而 FormArray 可以显示一个 address FormGroup 的数组。

要访问 FormArray 类,请先把它导入 hero-detail.component.ts 中:

src/app/hero-detail/hero-detail.component.ts (excerpt)
      
import { Component, Input, OnChanges }                   from '@angular/core';
import { FormArray, FormBuilder, FormGroup, Validators } from '@angular/forms';

import { Address, Hero, states } from '../data-model';
    

要使用 FormArray,就要这么做:

  1. 在数组中定义条目 FormControlFormGroup

  2. 把这个数组初始化微一组从数据模型中的数据创建的条目。

  3. 根据用户的需求添加或移除这些条目。

Hero.addresses 定义了一个 FormArray,并且让用户添加或修改这些住址。

你需要在 HeroDetailComponentcreateForm() 中重新定义表单模型,它现在只在 address FormGroup 中显示第一个英雄住址。

src/app/hero-detail/hero-detail.component.ts
      
this.heroForm = this.fb.group({
  name: ['', Validators.required ],
  address: this.fb.group(new Address()), // <-- a FormGroup with a new address
  power: '',
  sidekick: ''
});
    

address(住址)到 *secretLairs(秘密小屋)

在用户看来,英雄们没有住址。 只有我们凡人才有住址,英雄们拥有的是秘密小屋! 把 FormGroup 型的住址替换为 FormArray 型的 secretLairs 定义:

src/app/hero-detail/hero-detail-8.component.ts
      
this.heroForm = this.fb.group({
  name: ['', Validators.required ],
  secretLairs: this.fb.array([]), // <-- secretLairs as an empty FormArray
  power: '',
  sidekick: ''
});
    

hero-detail.component.html 中 把 formArrayName="address" 改为 formArrayName="secretLairs"

src/app/hero-detail/hero-detail.component.ts
      
<div formArrayName="secretLairs" class="well well-lg">
    

把表单的控件名从 address 改为 secretLairs 时导致了一个重要问题:表单模型数据模型不再匹配了。

显然,必须在两者之间建立关联。但它在应用领域中的意义不限于此,它可以用于任何东西。

展现的需求经常会与数据的需求不同。 响应式表单的方法既强调这种差异,也能为这种差异提供了便利。

初始化 FormArray 型的 secretLairs

默认的表单显示一个无地址的无名英雄。

你需要一个方法来用实际英雄的地址填充(或重新填充)secretLairs, 而不用管父组件 HeroListComponent 何时把 @Input() 属性 HeroDetailComponent.hero 设置为一个新的 Hero

下面的 setAddresses() 方法把 secretLairs 这个 FormArray 替换为一个新的 FormArray,使用一组表示英雄地址的 FormGroup 来进行初始化。在 HeroDetailComponent 类中添加下列内容:

src/app/hero-detail/hero-detail.component.ts
      
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

接着,在 rebuildForm() 中调用 setAddresses()

src/app/hero-detail/hero-detail.component.ts
      
rebuildForm() {
  this.heroForm.reset({
    name: this.hero.name
  });
  this.setAddresses(this.hero.addresses);
}
    

获取 FormArray

HeroDetailComponent 应该能从 secretLairs FormArray 中显示、添加和删除条目。

使用 FormGroup.get() 方法来获取到 FormArray 的引用。 把这个表达式包装进一个名叫 secretLairs 的便捷属性中来让它更清晰,并供复用。 在 HeroDetailComponent 中添加下列内容。

src/app/hero-detail/hero-detail.component.ts (secretLairs property)
      
get secretLairs(): FormArray {
  return this.heroForm.get('secretLairs') as FormArray;
};
    

显示 FormArray

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

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

写这个 *ngFor 有三个要点:

  1. *ngFor<div> 之外套上另一个包装 <div>,并且把它的 formArrayName 指令设为 "secretLairs"。 这一步为内部的表单控件建立了一个 FormArray 型的 secretLairs 作为上下文,以便重复渲染 HTML 模板。

  2. 这些重复条目的数据源是 FormArray.controls 而不是 FormArray 本身。 每个控件都是一个 FormGroup 型的地址对象,与以前的模板 HTML 所期望的格式完全一样。

  3. 每个被重复渲染的 FormGroup 都需要一个独一无二的 formGroupName,它必须是 FormGroup 在这个 FormArray 中的索引。 你将复用这个索引,以便为每个地址组合出一个独一无二的标签。

下面是 HTML 模板中秘密小屋部分的代码骨架:

src/app/hero-detail/hero-detail.component.html (*ngFor)
      
<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>
    

这里是秘密小屋部分的完整模板:

src/app/hero-detail/hero-detail.component.html (excerpt)
      <div formArrayName="secretLairs" class="well well-lg">
  <div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" >
    <!-- The repeated address template -->
    <h4>Address #{{i + 1}}</h4>
    <div style="margin-left: 1em;">
      <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>
    <br>
    <!-- End of the repeated address template -->
  </div>
</div>

把新的小屋添加到 FormArray

添加一个 addLair() 方法,它获取 secretLairs 数组,并把新的表示地址的 FormGroup 添加到其中。

src/app/hero-detail/hero-detail.component.ts (addLair method)
      
addLair() {
  this.secretLairs.push(this.fb.group(new Address()));
}
    

把一个按钮放在表单中,以便用户可以添加新的秘密小屋,并把它传给组件的 addLair() 方法。

src/app/hero-detail/hero-detail.component.html (addLair button)
      
<button (click)="addLair()" type="button">Add a Secret Lair</button>
    

务必确保添加了 type="button" 属性。 因为如果不明确指定类型,按钮的默认类型就是“submit”(提交)。 当你稍后添加了提交表单的动作时,每个“submit”按钮都是触发一次提交操作,而它将可能会做一些处理,比如保存当前的修改。 你显然不会希望每当用户点击“Add a Secret Lair”按钮时就保存一次。

试试看!

回到浏览器中,选择名叫“Magneta”的英雄。 "Magneta"没有地址,你会在表单底部的诊断用 JSON 中看到这一点。

JSON output of addresses array

点击“Add a Secret Lair”按钮,一个新的地址区就出现了,干得好!

移除一个小屋

这个例子可以添加地址,但是还不能移除它们。 作为练习,你可以自己写一个 removeLair 方法,并且把它关联到地址 HTML 模板的一个按钮上。

监视控件的变化

每当用户在父组件 HeroListComponent 中选取了一个英雄,Angular 就会调用一次 ngOnChanges。 选取英雄会修改输入属性 HeroDetailComponent.hero()

当用户修改英雄的名字秘密小屋时,Angular并不会调用 ngOnChanges()。 幸运的是,你可以通过订阅表单控件的属性之一来了解这些变化,此属性会发出变更通知。

有一些属性,比如 valueChanges,可以返回一个 RxJS 的 Observable 对象。 要监听控件值的变化,你并不需要对 RxJS 的 Observable 了解更多。

添加下列方法,以监听 name 这个 FormControl 中值的变化。

src/app/hero-detail/hero-detail.component.ts (logNameChange)
    nameChangeLog: string[] = [];
logNameChange() {
  const nameControl = this.heroForm.get('name');
  nameControl.valueChanges.forEach(
    (value: string) => this.nameChangeLog.push(value)
  );
}

在构造函数中调用它,就在 createForm() 之后。

src/app/hero-detail/hero-detail.component.ts
  constructor(private fb: FormBuilder) {
  this.createForm();
  this.logNameChange();
}

logNameChange() 方法会把一条改名记录追加到 nameChangeLog 数组中。 用 *ngFor 绑定在组件模板的底部显示这个数组:

src/app/hero-detail/hero-detail.component.html (Name change log)
<h4>Name change log</h4>
<div *ngFor="let name of nameChangeLog">{{name}}</div>

返回浏览器,选择一个英雄(比如“Magneta”),并开始在 name 输入框中键入。 你会看到,每次按键都会记录一个新名字。

什么时候用它

插值表达式绑定时显示姓名变化比较简单的方式。 在组件类中订阅表单控件属性变化的可观察对象以触发应用逻辑则是比较难的方式。

保存表单数据

HeroDetailComponent 捕获了用户输入,但没有用它做任何事。 在真实的应用中,你可能要保存这些英雄的变化。 在真实的应用中,你还要能丢弃未保存的变更,然后继续编辑。 在实现完本节的这些特性之后,表单是这样的:

Form with save & revert buttons

保存

当用户提交表单时,HeroDetailComponent 会把英雄实例的数据模型传给所注入进来的 HeroService 的一个方法来进行保存。 在 HeroDetailComponent 中添加如下内容:

src/app/hero-detail/hero-detail.component.ts (onSubmit)
   onSubmit() {
  this.hero = this.prepareSaveHero();
  this.heroService.updateHero(this.hero).subscribe(/* error handling */);
  this.rebuildForm();
}

原始的 hero 中有一些保存之前的值,用户的修改仍然是在表单模型中。 所以你要根据原始英雄(根据 hero.id 找到它)的值组合出一个新的 hero 对象,并用 prepareSaveHero() 助手来深层复制变化后的模型值。

src/app/hero-detail/hero-detail.component.ts (prepareSaveHero)
   prepareSaveHero(): Hero {
  const formModel = this.heroForm.value;

  // deep copy of form model lairs
  const secretLairsDeepCopy: Address[] = formModel.secretLairs.map(
    (address: Address) => Object.assign({}, address)
  );

  // return new `Hero` object containing a combination of original hero value(s)
  // and deep copies of changed form model values
  const saveHero: Hero = {
    id: this.hero.id,
    name: formModel.name as string,
    // addresses: formModel.secretLairs // <-- bad!
    addresses: secretLairsDeepCopy
  };
  return saveHero;
}
    

确保导入了 HeroService 并把它加入了构造函数中:

src/app/hero-detail/hero-detail.component.ts (prepareSaveHero)
      
import { HeroService }           from '../hero.service';
    
src/app/hero-detail/hero-detail.component.ts (prepareSaveHero)
constructor(
  private fb: FormBuilder,
  private heroService: HeroService) {

  this.createForm();
  this.logNameChange();
}

地址的深层复制

你已经把 formModel.secretLairs 赋值给了 saveHero.addresses(参见注释掉的部分), saveHero.addresses 数组中的地址和 formModel.secretLairs 中的会是同一个对象。 用户随后对小屋所在街道的修改将会改变 saveHero 中的街道地址。

prepareSaveHero 方法会制作表单模型中的 secretLairs 对象的复本,因此实际上并没有修改原有对象。

丢弃(撤销修改)

用户可以撤销修改,并通过点击 Revert 按钮来把表单恢复到原始状态。

丢弃很容易。只要重新执行 rebuildForm() 方法就可以根据原始的、未修改过的 hero 数据模型重新构建出表单模型。

src/app/hero-detail/hero-detail.component.ts (revert)
      
revert() { this.rebuildForm(); }
    

按钮

把“Save”和“Revert”按钮添加到组件模板的顶部:

src/app/hero-detail/hero-detail.component.html (Save and Revert buttons)
      <form [formGroup]="heroForm" (ngSubmit)="onSubmit()">
  <div style="margin-bottom: 1em">
    <button type="submit"
            [disabled]="heroForm.pristine" class="btn btn-success">Save</button> &nbsp;
    <button type="button" (click)="revert()"
            [disabled]="heroForm.pristine" class="btn btn-danger">Revert</button>
  </div>

  <!-- Hero Detail Controls -->
  <div class="form-group radio">
    <h4>Super power:</h4>
    <label class="center-block"><input type="radio" formControlName="power" value="flight">Flight</label>
    <label class="center-block"><input type="radio" formControlName="power" value="x-ray vision">X-ray vision</label>
    <label class="center-block"><input type="radio" formControlName="power" value="strength">Strength</label>
  </div>
  <div class="checkbox">
    <label class="center-block">
      <input type="checkbox" formControlName="sidekick">I have a sidekick.
    </label>
  </div>
</form>
    

这些按钮默认是禁用的,直到用户通过修改任何一个表单控件的值“弄脏”了表单中的数据(即 heroForm.dirty)。

点击一个类型为 "submit" 的按钮会触发 ngSubmit 事件,而它会调用组件的 onSubmit 方法。 点击“Revert”按钮则会调用组件的 revert 方法。 现在,用户可以保存或放弃修改了。

试试 Reactive Forms (final) in Stackblitz / 下载范例.

最终版中的核心文件如下:

src/app/app.component.html

      

<div class="container">
  <h1>Reactive Forms</h1>
  <app-hero-list></app-hero-list>
</div>
    



src/app/app.component.ts

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

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent { }




src/app/app.module.ts



    import { NgModule }            from '@angular/core';
    import { BrowserModule }       from '@angular/platform-browser';
    import { ReactiveFormsModule } from '@angular/forms';  // <-- #1 import module
     
    import { AppComponent }        from './app.component';
    import { HeroDetailComponent } from './hero-detail/hero-detail.component';
    // add JavaScript imports
    import { HeroListComponent }   from './hero-list/hero-list.component';
    import { HeroService }         from './hero.service';
     
    @NgModule({
      declarations: [
        AppComponent,
        HeroDetailComponent,
        HeroListComponent // <--declare HeroListComponent
      ],
      imports: [
        BrowserModule,
        ReactiveFormsModule // <-- #2 add to @NgModule imports
      ],
      // export for the DemoModule
      // ...
      exports: [
        AppComponent,
        HeroDetailComponent,
        HeroListComponent // <-- export HeroListComponent
      ],
      providers: [ HeroService ], // <-- provide HeroService
      bootstrap: [ AppComponent ]
    })
    export class AppModule { }




src/app/hero-detail/hero-detail.component.ts



    import { Component, Input, OnChanges }       from '@angular/core';
    import { FormArray, FormBuilder, FormGroup } from '@angular/forms';
     
    import { Address, Hero, states } from '../data-model';
    import { HeroService }           from '../hero.service';
     
    @Component({
      selector: 'app-hero-detail',
      templateUrl: './hero-detail.component.html',
      styleUrls: ['./hero-detail.component.css']
    })
     
    export class HeroDetailComponent implements OnChanges {
      @Input() hero: Hero;
     
      heroForm: FormGroup;
      nameChangeLog: string[] = [];
      states = states;
     
      constructor(
        private fb: FormBuilder,
        private heroService: HeroService) {
     
        this.createForm();
        this.logNameChange();
      }
     
      createForm() {
        this.heroForm = this.fb.group({
          name: '',
          secretLairs: this.fb.array([]),
          power: '',
          sidekick: ''
        });
      }
     
      ngOnChanges() {
        this.rebuildForm();
      }
     
      rebuildForm() {
        this.heroForm.reset({
          name: this.hero.name
        });
        this.setAddresses(this.hero.addresses);
      }
     
      get secretLairs(): FormArray {
        return this.heroForm.get('secretLairs') as FormArray;
      };
     
      setAddresses(addresses: Address[]) {
        const addressFGs = addresses.map(address => this.fb.group(address));
        const addressFormArray = this.fb.array(addressFGs);
        this.heroForm.setControl('secretLairs', addressFormArray);
      }
     
      addLair() {
        this.secretLairs.push(this.fb.group(new Address()));
      }
     
      onSubmit() {
        this.hero = this.prepareSaveHero();
        this.heroService.updateHero(this.hero).subscribe(/* error handling */);
        this.rebuildForm();
      }
     
      prepareSaveHero(): Hero {
        const formModel = this.heroForm.value;
     
        // deep copy of form model lairs
        const secretLairsDeepCopy: Address[] = formModel.secretLairs.map(
          (address: Address) => Object.assign({}, address)
        );
     
        // return new `Hero` object containing a combination of original hero value(s)
        // and deep copies of changed form model values
        const saveHero: Hero = {
          id: this.hero.id,
          name: formModel.name as string,
          // addresses: formModel.secretLairs // <-- bad!
          addresses: secretLairsDeepCopy
        };
        return saveHero;
      }
     
      revert() { this.rebuildForm(); }
     
      logNameChange() {
        const nameControl = this.heroForm.get('name');
        nameControl.valueChanges.forEach(
          (value: string) => this.nameChangeLog.push(value)
        );
      }
    }





src/app/hero-detail/hero-detail.component.html

    <form [formGroup]="heroForm" (ngSubmit)="onSubmit()">
      <div style="margin-bottom: 1em">
        <button type="submit"
                [disabled]="heroForm.pristine" class="btn btn-success">Save</button> &nbsp;
        <button type="button" (click)="revert()"
                [disabled]="heroForm.pristine" class="btn btn-danger">Revert</button>
      </div>
     
      <!-- Hero Detail Controls -->
      <div class="form-group">
          <label class="center-block">Name:
            <input class="form-control" formControlName="name">
          </label>
      </div>
     
      <div formArrayName="secretLairs" class="well well-lg">
        <div *ngFor="let address of secretLairs.controls; let i=index" [formGroupName]="i" >
          <!-- The repeated address template -->
          <h4>Address #{{i + 1}}</h4>
          <div style="margin-left: 1em;">
            <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>
          <br>
          <!-- End of the repeated address template -->
        </div>
        <button (click)="addLair()" type="button">Add a Secret Lair</button>
      </div>
      <div class="form-group radio">
        <h4>Super power:</h4>
        <label class="center-block"><input type="radio" formControlName="power" value="flight">Flight</label>
        <label class="center-block"><input type="radio" formControlName="power" value="x-ray vision">X-ray vision</label>
        <label class="center-block"><input type="radio" formControlName="power" value="strength">Strength</label>
      </div>
      <div class="checkbox">
        <label class="center-block">
          <input type="checkbox" formControlName="sidekick">I have a sidekick.
        </label>
      </div>
    </form>
     
    <p>heroForm value: {{ heroForm.value | json}}</p>
     
    <h4>Name change log</h4>
    <div *ngFor="let name of nameChangeLog">{{name}}</div>





src/app/hero-list/hero-list.component.html



    <h3 *ngIf="isLoading"><i>Loading heroes ... </i></h3>
    <h3 *ngIf="!isLoading">Select a hero:</h3>
     
    <nav>
      <button (click)="getHeroes()" class="btn btn-primary">Refresh</button>
      <a *ngFor="let hero of heroes | async" (click)="select(hero)">{{hero.name}}</a>
    </nav>
     
    <div *ngIf="selectedHero">
      <hr>
      <h2>Hero Detail</h2>
      <h3>Editing: {{selectedHero.name}}</h3>
      <app-hero-detail [hero]="selectedHero"></app-hero-detail>
    </div>




src/app/hero-list/hero-list.component.ts

    import { Component, OnInit } from '@angular/core';
    import { Observable }        from 'rxjs';
    import { finalize } from 'rxjs/operators';
     
    import { Hero }        from '../data-model';
    import { HeroService } from '../hero.service';
     
    @Component({
      selector: 'app-hero-list',
      templateUrl: './hero-list.component.html',
      styleUrls: ['./hero-list.component.css']
    })
    export class HeroListComponent implements OnInit {
      heroes: Observable<Hero[]>;
      isLoading = false;
      selectedHero: Hero;
     
      constructor(private heroService: HeroService) { }
     
      ngOnInit() { this.getHeroes(); }
     
      getHeroes() {
        this.isLoading = true;
        this.heroes = this.heroService.getHeroes()
                          // Todo: error handling
                          .pipe(finalize(() => this.isLoading = false));
        this.selectedHero = undefined;
      }
     
      select(hero: Hero) { this.selectedHero = hero; }
    }



src/app/data-model.ts


    export class Hero {
      id = 0;
      name = '';
      addresses: Address[];
    }
     
    export class Address {
      street = '';
      city   = '';
      state  = '';
      zip    = '';
    }
     
    export const heroes: Hero[] = [
      {
        id: 1,
        name: 'Whirlwind',
        addresses: [
          {street: '123 Main',  city: 'Anywhere', state: 'CA',  zip: '94801'},
          {street: '456 Maple', city: 'Somewhere', state: 'VA', zip: '23226'},
        ]
      },
      {
        id: 2,
        name: 'Bombastic',
        addresses: [
          {street: '789 Elm',  city: 'Smallville', state: 'OH',  zip: '04501'},
        ]
      },
      {
        id: 3,
        name: 'Magneta',
        addresses: [ ]
      },
    ];
     
    export const states = ['CA', 'MD', 'OH', 'VA'];


src/app/hero.service.ts

    import { Injectable } from '@angular/core';
     
    import { Observable, of } from 'rxjs';
    import { delay } from 'rxjs/operators';
     
    import { Hero, heroes } from './data-model';
     
    @Injectable()
    export class HeroService {
     
      delayMs = 500;
     
      // Fake server get; assume nothing can go wrong
      getHeroes(): Observable<Hero[]> {
        return of(heroes).pipe(delay(this.delayMs)); // simulate latency with delay
      }
     
      // Fake server update; assume nothing can go wrong
      updateHero(hero: Hero): Observable<Hero>  {
        const oldHero = heroes.find(h => h.id === hero.id);
        const newHero = Object.assign(oldHero, hero); // Demo: mutate cached hero
        return of(newHero).pipe(delay(this.delayMs)); // simulate latency with delay
      }
    }

你可以到响应式表单在线例子 / 下载范例中下载本章所有步骤的完整代码。


来源:https://www.angular.cn/guide/reactive-forms

为您推荐

友情链接 |九搜汽车网 |手机ok生活信息网|ok生活信息网|ok微生活
 Powered by www.360SDN.COM   京ICP备11022651号-4 © 2012-2016 版权