` with `*ngFor`, and
set its `formArrayName` directive to `"secretLairs"`.
This step establishes the _secretLairs_ `FormArray` as the context for form controls in the inner, repeated HTML template.
在`*ngFor`的`
`之外套上另一个包装`
`,并且把它的`formArrayName`指令设为`"secretLairs"`。
这一步为内部的表单控件建立了一个`FormArray`型的`secretLairs`作为上下文,以便重复渲染HTML模板。
1. The source of the repeated items is the `FormArray.controls`, not the `FormArray` itself.
Each control is an _address_ `FormGroup`, exactly what the previous (now repeated) template HTML expected.
这些重复条目的数据源是`FormArray.controls`而不是`FormArray`本身。
每个控件都是一个`FormGroup`型的地址对象,与以前的模板HTML所期望的格式完全一样。
1. Each repeated `FormGroup` needs a unique `formGroupName` which must be the index of the `FormGroup` in the `FormArray`.
You'll re-use that index to compose a unique label for each address.
每个被重复渲染的`FormGroup`都需要一个独一无二的`formGroupName`,它必须是`FormGroup`在这个`FormArray`中的索引。
我们将复用这个索引,以便为每个地址组合出一个独一无二的标签。
Here's the skeleton for the _secret lairs_ section of the HTML template:
下面是HTML模板中*秘密小屋*部分的代码骨架:
+makeExample('reactive-forms/ts/src/app/hero-detail-8.component.html', 'form-array-skeleton','src/app/hero-detail.component.html (*ngFor)')(format=".")
:marked
Here's the complete template for the _secret lairs_ section:
这里是*秘密小屋*部分的完整模板:
+makeExample('reactive-forms/ts/src/app/hero-detail-8.component.html', 'form-array','src/app/hero-detail.component.html (excerpt)')
:marked
### Add a new lair to the _FormArray_
### 把新的小屋添加到`FormArray`中
Add an `addLair` method that gets the _secretLairs_ `FormArray` and appends a new _address_ `FormGroup` to it.
添加一个`addLair`方法,它获取`secretLairs`数组,并把新的表示地址的`FormGroup`添加到其中。
+makeExample('reactive-forms/ts/src/app/hero-detail-8.component.ts', 'add-lair','src/app/hero-detail.component.ts (addLair method)')(format=".")
:marked
Place a button on the form so the user can add a new _secret lair_ and wire it to the component's `addLair` method.
把一个按钮放在表单中,以便用户可以添加新的*秘密小屋*,并把它传给组件的`addLair`方法。
+makeExample('reactive-forms/ts/src/app/hero-detail-8.component.html', 'add-lair','src/app/hero-detail.component.html (addLair button)')(format=".")
.alert.is-important
:marked
Be sure to **add the `type="button"` attribute**.
In fact, you should always specify a button's `type`.
Without an explict type, the button type defaults to "submit".
When you later add a _form submit_ action, every "submit" button triggers the submit action which
might do something like save the current changes.
You do not want to save changes when the user clicks the _Add a Secret Lair_ button.
务必确保**添加了`type="button"`属性**。
事实上,我们应该总是指定按钮的`type`。
如果不明确指定类型,按钮的默认类型就是"submit"(提交)。
当我们稍后添加了*表单提交*的动作时,每个"submit"按钮都是触发一次提交操作,而它将可能会做一些处理,比如保存当前的修改。
我们显然不会希望每当用户点击"Add a Secret Lair"按钮时就保存一次。
:marked
### Try it!
### 试试看!
Back in the browser, select the hero named "Magneta".
"Magneta" doesn't have an address, as you can see in the diagnostic JSON at the bottom of the form.
回到浏览器中,选择名叫"Magneta"的英雄。
"Magneta"没有地址,我们会在表单底部的诊断用JSON中看到这一点。
figure.image-display
img(src="/resources/images/devguide/reactive-forms/addresses-array.png" width="400px" alt="JSON output of addresses array")
:marked
Click the "_Add a Secret Lair_" button.
A new address section appears. Well done!
点击"Add a Secret Lair"按钮,一个新的地址区就出现了,干得好!
### Remove a lair
### 移除一个小屋
This example can _add_ addresses but it can't _remove_ them.
For extra credit, write a `removeLair` method and wire it to a button on the repeating address HTML.
这个例子可以*添加*地址,但是还不能*移除*它们。
作为练习,你可以自己写一个`removeLair`方法,并且把它关联到地址HTML模板的一个按钮上。
.l-main-section
a#observe-control
:marked
## Observe control changes
## 监视控件的变化
Angular calls `ngOnChanges` when the user picks a hero in the parent `HeroListComponent`.
Picking a hero changes the `HeroDetailComponent.hero` input property.
每当用户在父组件`HeroListComponent`中选取了一个英雄,Angular就会调用一次`ngOnChanges`。
选取英雄会修改输入属性`HeroDetailComponent.hero`。
Angular does _not_ call `ngOnChanges` when the user modifies the hero's _name_ or _secret lairs_.
Fortunately, you can learn about such changes by subscribing to one of the form control properties
that raises a change event.
当用户修改英雄的*名字*或*秘密小屋*时,Angular*并不会*调用`ngOnChanges`。
幸运的是,我们可以通过订阅表单控件的属性之一来了解这些变化,此属性会发出变更通知。
These are properties, such as `valueChanges`, that return an RxJS `Observable`.
You don't need to know much about RxJS `Observable` to monitor form control values.
有一些属性,比如`valueChanges`,可以返回一个RxJS的`Observable`对象。
要监听控件值的变化,我们并不需要对RxJS的`Observable`了解更多。
Add the following method to log changes to the value of the _name_ `FormControl`.
添加下列方法,以监听姓名这个`FormControl`中值的变化。
+makeExample('reactive-forms/ts/src/app/hero-detail.component.ts', 'log-name-change','src/app/hero-detail.component.ts (logNameChange)')(format=".")
:marked
Call it in the constructor, after creating the form.
在构造函数中调用它,就在创建表单的代码之后:
+makeExample('reactive-forms/ts/src/app/hero-detail-8.component.ts', 'ctor')(format=".")
:marked
The `logNameChange` method pushes name-change values into a `nameChangeLog` array.
Display that array at the bottom of the component template with this `*ngFor` binding:
`logNameChange`方法会把一条改名记录追加到`nameChangeLog`数组中。
用`*ngFor`绑定在组件模板的底部显示这个数组:
+makeExample('reactive-forms/ts/src/app/hero-detail.component.html', 'name-change-log','src/app/hero-detail.component.html (Name change log)')(format=".")
:marked
Return to the browser, select a hero (e.g, "Magneta"), and start typing in the _name_ input box.
You should see a new name in the log after each keystroke.
返回浏览器,选择一个英雄(比如"Magneta"),并开始在*姓名*输入框中键入。
我们会看到,每次按键都会记录一个新名字。
### When to use it
### 什么时候用它
An interpolation binding is the easier way to _display_ a name change.
Subscribing to an observable form control property is handy for triggering
application logic _within_ the component class.
插值表达式绑定时显示姓名变化比较简单的方式。
*在组件类中*订阅表单控件属性变化的可观察对象以触发应用逻辑则是比较难的方式。
.l-main-section
a#save
:marked
## Save form data
## 保存表单数据
The `HeroDetailComponent` captures user input but it doesn't do anything with it.
In a real app, you'd probably save those hero changes.
In a real app, you'd also be able to revert unsaved changes and resume editing.
After you implement both features in this section, the form will look like this:
`HeroDetailComponent`捕获了用户输入,但没有用它做任何事。
在真实的应用中,我们可能要保存这些英雄的变化。
在真实的应用中,我们还要能丢弃未保存的变更,然后继续编辑。
在实现完本节的这些特性之后,表单是这样的:
figure.image-display
img(src="/resources/images/devguide/reactive-forms/save-revert-buttons.png" width="389px" alt="Form with save & revert buttons")
:marked
### Save
### 保存
In this sample application, when the user submits the form,
the `HeroDetailComponent` will pass an instance of the hero _data model_
to a save method on the injected `HeroService`.
在这个范例应用中,当用户提交表单时,`HeroDetailComponent`会把英雄实例的*数据模型*传给所注入进来的`HeroService`的一个方法来进行保存。
+makeExample('reactive-forms/ts/src/app/hero-detail.component.ts', 'on-submit','src/app/hero-detail.component.ts (onSubmit)')(format=".")
:marked
This original `hero` had the pre-save values. The user's changes are still in the _form model_.
So you create a new `hero` from a combination of original hero values (the `hero.id`)
and deep copies of the changed form model values, using the `prepareSaveHero` helper.
原始的`hero`中有一些保存之前的值,用户的修改仍然是在*表单模型*中。
所以我们要根据原始英雄(根据`hero.id`找到它)的值组合出一个新的`hero`对象,并用`prepareSaveHero`助手来深层复制变化后的模型值。
+makeExample('reactive-forms/ts/src/app/hero-detail.component.ts', 'prepare-save-hero','src/app/hero-detail.component.ts (prepareSaveHero)')(format=".")
.l-sub-section
:marked
**Address deep copy**
**地址的深层复制**
Had you assigned the `formModel.secretLairs` to `saveHero.addresses` (see line commented out),
the addresses in `saveHero.addresses` array would be the same objects
as the lairs in the `formModel.secretLairs`.
A user's subsequent changes to a lair street would mutate an address street in the `saveHero`.
我们已经把`formModel.secretLairs`赋值给了`saveHero.addresses`(参见注释掉的部分),
`saveHero.addresses`数组中的地址和`formModel.secretLairs`中的会是同一个对象。
用户随后对小屋所在街道的修改将会改变`saveHero`中的街道地址。
The `prepareSaveHero` method makes copies of the form model's `secretLairs` objects so that can't happen.
但`prepareSaveHero`方法会制作表单模型中的`secretLairs`对象的复本,因此实际上并没有修改原有对象。
:marked
### Revert (cancel changes)
### 丢弃(撤销修改)
The user cancels changes and reverts the form to the original state by pressing the _Revert_ button.
用户可以撤销修改,并通过点击*Revert*按钮来把表单恢复到原始状态。
Reverting is easy. Simply re-execute the `ngOnChanges` method that built the _form model_ from the original, unchanged `hero` _data model_.
丢弃很容易。只要重新执行`ngOnChanges`方法就可以拆而,它会重新从原始的、未修改过的`hero`数据模型来构建出*表单模型*。
+makeExample('reactive-forms/ts/src/app/hero-detail.component.ts', 'revert','src/app/hero-detail.component.ts (revert)')(format=".")
:marked
### Buttons
### 按钮
Add the "Save" and "Revert" buttons near the top of the component's template:
把"Save"和"Revert"按钮添加到组件模板的顶部:
+makeExample('reactive-forms/ts/src/app/hero-detail.component.html', 'buttons','src/app/hero-detail.component.html (Save and Revert buttons)')(format=".")
:marked
The buttons are disabled until the user "dirties" the form by changing a value in any of its form controls (`heroForm.dirty`).
这些按钮默认是禁用的,直到用户通过修改任何一个表单控件的值"弄脏"了表单中的数据(即`heroForm.dirty`)。
Clicking a button of type `"submit"` triggers the `ngSubmit` event which calls the component's `onSubmit` method.
Clicking the revert button triggers a call to the component's `revert` method.
Users now can save or revert changes.
点击一个类型为`"submit"`的按钮会触发`ngSubmit`事件,而它会调用组件的`onSubmit`方法。
点击"Revert"按钮则会调用组件的`revert`方法。
现在,用户可以保存或放弃修改了。
This is the final step in the demo.
Try the .
这是本演示的最后一步。
去试试吧。
.l-main-section
:marked
## Conclusion
## 总结
This page covered:
本章包括:
* How to create a reactive form component and its corresponding template.
如何创建一个响应式表单控件及其对应的模板。
* How to use `FormBuilder` to simplify coding a reactive form.
如何使用`FormBuilder`来简化响应式表单的编码工作。
* Grouping `FormControls`.
分组`FormControl`。
* Inspecting `FormControl` properties.
审查`FormControl`的属性。
* Setting data with `patchValue` and `setValue`.
使用`patchValue`和`setValue`设置数据。
* Adding groups dynamically with `FormArray`.
使用`FormArray`动态添加控件组。
* Observing changes to the value of a `FormControl`.
监听`FormControl`中值的变化。
* Saving form changes.
保存表单中的更改。
a#source-code
:marked
The key files of the final version are as follows:
最终版中的核心文件如下:
+makeTabs(
`reactive-forms/ts/src/app/app.component.ts,
reactive-forms/ts/src/app/app.module.ts,
reactive-forms/ts/src/app/hero-detail.component.ts,
reactive-forms/ts/src/app/hero-detail.component.html,
reactive-forms/ts/src/app/hero-list.component.html,
reactive-forms/ts/src/app/hero-list.component.ts,
reactive-forms/ts/src/app/data-model.ts,
reactive-forms/ts/src/app/hero.service.ts
`,
'',
`src/app/app.component.ts,
src/app/app.module.ts,
src/app/hero-detail.component.ts,
src/app/hero-detail.component.html,
src/app/hero-list.component.html,
src/app/hero-list.component.ts,
src/app/data-model.ts,
src/app/hero.service.ts,
`)
:marked
You can download the complete source for all steps in this guide
from the Reactive Forms Demo live example.
你可以到响应式表单在线例子中下载本章所有步骤的完整代码。