` 上添加一个 `formGroupName` 指令,并且把它绑定到 `"address"` 上。
这个 `address` 属性是一个 `FormGroup`,它的父 `FormGroup` 就是 `heroForm`。
把这个 `name` `
` 中。
To make this change visually obvious, add an `
` header near the top with the text, _Secret Lair_.
The new address HTML looks like this:
要让这个变化更加明显,在文本的顶部加入一个 `` 头:*Secret Lair*。
新的住址组的 HTML 如下:
After these changes, the JSON output in the browser shows the revised form model
with the nested address `FormGroup`:
做完这些之后,浏览器中的 JSON 输出就变成了带有多级 `FormGroup` 的表单模型。
This shows that the template
and the form model are talking to one another.
这时模板和表单模型在彼此通讯了。
{@a properties}
## Inspect `FormControl` Properties
## 查看 `FormControl` 的属性
You can inspect an individual `FormControl` within a form by extracting it with the `get()` method.
You can do this within the component class or display it on the
page by adding the following to the template,
immediately after the `{{form.value | json}}` interpolation as follows:
你可以使用 `.get()` 方法来提取表单中一个单独 `FormControl` 的状态。
你可以在组件类中这么做,或者通过往模板中添加下列代码来把它显示在页面中,就添加在 `{{form.value | json}}` 插值表达式的紧后面:
To get the state of a `FormControl` that’s inside a `FormGroup`, use dot notation to traverse to the control.
要点取得 `FormGroup` 中的 `FormControl` 的状态,使用点语法来指定到控件的路径。
*Note*: If you're coding along, remember to remove this reference to `address.street` when you get to the section on `FormArray`. In that section, you change the name of address in the component class and it will throw an error if you leave it in the template.
*注意*:如果你正在边看边跟着写代码,当你到达 `FormArray` 那节时,别忘了移除到 `address.street` 的引用。那一节中,你要在组件类中修改这个地址的名字,如果你把它留在模板中,就会抛出一个错误。
You can use this technique to display any property of a `FormControl`
such as one of the following:
你可以使用此技术来显示 `FromControl` 的任意属性,代码如下:
Property
属性
Description
说明
myControl.value
the value of a `FormControl`.
`FormControl` 的值。
myControl.status
the validity of a `FormControl`. Possible values: `VALID`,
`INVALID`, `PENDING`, or `DISABLED`.
`FormControl` 的有效性。可能的值有 `VALID`、`INVALID`、`PENDING` 或 `DISABLED`。
myControl.pristine
`true` if the user has _not_ changed the value in the UI.
Its opposite is `myControl.dirty`.
如果用户*尚未*改变过这个控件的值,则为 `true`。它总是与 `myControl.dirty` 相反。
myControl.untouched
`true` if the control user has not yet entered the HTML control
and triggered its blur event. Its opposite is `myControl.touched`.
如果用户尚未进入这个 HTML 控件,也没有触发过它的 `blur`(失去焦点)事件,则为 `true`。
它是 `myControl.touched` 的反义词。
Read about other `FormControl` properties in the
[_AbstractControl_](api/forms/AbstractControl) API reference.
要了解 `FormControl` 的更多属性,参见 API 参考手册的[_AbstractControl_](api/forms/AbstractControl)部分。
One common reason for inspecting `FormControl` properties is to
make sure the user entered valid values.
Read more about validating Angular forms in the
[Form Validation](guide/form-validation) guide.
检查 `FormControl` 属性的另一个原因是确保用户输入了有效的值。
要了解更多关于 Angular 表单验证的知识,参见[表单验证](guide/form-validation)一章。
{@a data-model-form-model}
## The data model and the form model
## 数据模型与表单模型
At the moment, the form is displaying empty values.
The `HeroDetailComponent` should display values of a hero,
possibly a hero retrieved from a remote server.
此刻,表单显示的是空值。
`HeroDetailComponent` 应该显示一个英雄的值,这个值可能接收自远端服务器。
In this app, the `HeroDetailComponent` gets its hero from a parent `HeroListComponent`.
在这个应用中,`HeroDetailComponent` 从它的父组件 `HeroListComponent` 中取得一个英雄。
The `hero` from the server is the **_data model_**.
The `FormControl` structure is the **_form model_**.
来自服务器的 `hero` 就是**数据模型**,而 `FormControl` 的结构就是**表单模型**。
The component must copy the hero values in the data model into the form model.
There are two important implications:
组件必须把数据模型中的英雄值复制到表单模型中。这里隐含着两个非常重要的点。
1. The developer must understand how the properties of the data model
map to the properties of the form model.
开发人员必须理解数据模型是如何映射到表单模型中的属性的。
2. User changes flow from the DOM elements to the form model, not to the data model.
用户修改时的数据流是从 DOM 元素流向表单模型的,而不是数据模型。
The form controls never update the _data model_.
表单控件永远不会修改*数据模型*。
The form and data model structures don't need to match exactly.
You often present a subset of the data model on a particular screen.
But it makes things easier if the shape of the form model is close to the shape of the data model.
表单模型和数据模型的结构并不需要精确匹配。在一个特定的屏幕上,你通常只会展现数据模型的一个子集。
但是表单模型的形态越接近数据模型,事情就会越简单。
In this `HeroDetailComponent`, the two models are quite close.
在 `HeroDetailComponent` 中,这两个模型是非常接近的。
Here are the definitions of `Hero` and `Address` in `data-model.ts`:
回忆一下 `data-model.ts` 中 `Hero` 和 `Address` 的 定义:
Here, again, is the component's `FormGroup` definition.
这里又是组件的 `FormGroup` 定义。
There are two significant differences between these models:
在这些模型中有两点显著的差异:
1. The `Hero` has an `id`. The form model does not because you generally don't show primary keys to users.
`Hero` 有一个 `id`。表单模型中则没有,因为你通常不会把主键展示给用户。
1. The `Hero` has an array of addresses. This form model presents only one address,
which is covered in the section on [`FormArray`](guide/reactive-forms#form-array "Form arrays") below.
`Hero` 有一个住址数组。这个表单模型只表示了一个住址,稍后的 [`FormArray`] (guide/reactive-forms#form-array "Form arrays")则可以表示多个。
Keeping the two models close in shape facilitates copying the data model properties
to the form model with the `patchValue()` and `setValue()` methods in the next section.
保持这两个模型的形态尽可能接近,可以在下一节中轻松使用 `patchValue()` 和 `setValue()` 方法把数据模型拷贝到表单模型中。
First, refactor the `address` `FormGroup` definition as follows:
首先把 `address` 这个 `FormGroup` 的定义重构成这样:
Also be sure to update the `import` from `data-model` so you can reference the `Hero` and `Address` classes:
为了确保从 `data-model` 中导入,你可以引用 `Hero` 和 `Address` 类:
{@a set-data}
## Populate the form model with `setValue()` and `patchValue()`
## 使用 `setValue()` 和 `patchValue()` 来操纵表单模型
*Note*: If you're coding along, this section is optional as the rest of the steps do not rely on it.
*注意*:如果你正在跟着写代码,那么本节是可选的,因为剩下的步骤并不依赖它。
Previously, you created a control and initialized its value at the same time.
You can also initialize or reset the values later with the
`setValue()` and `patchValue()` methods.
以前,你创建了控件,并同时初始化它的值。
你也可以稍后用 `setValue()` 和 `patchValue()` 来初始化或重置这些值。
### `setValue()`
With `setValue()`, you assign every form control value at once
by passing in a data object whose properties exactly match the form model behind the `FormGroup`.
借助**`setValue()`**,你可以设置每个表单控件的值,只要把与表单模型的属性精确匹配的数据模型传进去就可以了。
The `setValue()` method checks the data object thoroughly before assigning any form control values.
`setValue()` 方法会在赋值给任何表单控件之前先检查数据对象的值。
It will not accept a data object that doesn't match the `FormGroup` structure or is
missing values for any control in the group. This way, it can return helpful
error messages if you have a typo or if you've nested controls incorrectly.
Conversely, `patchValue()` will fail silently.
它不会接受一个与 `FormGroup` 结构不同或缺少表单组中任何一个控件的数据对象。
这种方式下,如果你有什么拼写错误或控件嵌套的不正确,它就能返回一些有用的错误信息。
反之,`patchValue()` 会默默地失败。
Notice that you can almost use the entire `hero` as the argument to `setValue()`
because its shape is similar to the component's `FormGroup` structure.
注意,你几乎可以直接把这个 `hero` 用作 `setValue()` 的参数,因为它的形态与组件的 `FormGroup` 结构是非常像的。
You can only show the hero's first address and you must account for the possibility that the `hero` has no addresses at all, as in the conditional setting of the `address` property in the data object argument:
你现在只能显示英雄的第一个住址,不过你还必须考虑 `hero` 完全没有住址的可能性。
就像这个在数据对象参数中对 `address` 属性进行有条件的设置:
### `patchValue()`
With **`patchValue()`**, you can assign values to specific controls in a `FormGroup`
by supplying an object of key/value pairs for them.
借助**`patchValue()`**,你可以通过提供一个只包含要更新的控件的键值对象来把值赋给 `FormGroup` 中的指定控件。
This example sets only the form's `name` control.
这个例子只会设置表单的 `name` 控件。
With `patchValue()` you have more flexibility to cope with divergent data and form models.
But unlike `setValue()`, `patchValue()` cannot check for missing control
values and doesn't throw helpful errors.
借助**`patchValue()`**,你可以更灵活地解决数据模型和表单模型之间的差异。
但是和 `setValue()` 不同,`patchValue()` 不会检查缺失的控件值,并且不会抛出有用的错误信息。
{@a hero-list}
## Create the `HeroListComponent` and `HeroService`
## 创建 `HeroListComponent` 和 `HeroService`
To demonstrate further reactive forms techniques, it is helpful to add more functionality to the example by adding a `HeroListComponent` and a `HeroService`.
要更好地演示后面的响应式表单技巧,可以通过加入 `HeroListComponent` 和 `HeroService` 来为这个范例添加更多功能。
The `HeroDetailComponent` is a nested sub-component of the `HeroListComponent` in a _master/detail_ view. Together they look like this:
`HeroDetalComponent` 是一个嵌套在 `HeroListComponent` 的*主从*视图中的子组件。如果把它们放在一起就是这样的:
First, add a `HeroListComponent` with the following command:
首先使用下列命令添加一个 `HeroListComponent`:
ng generate component HeroList
Give the `HeroListComponent` the following contents:
把 `HeroListComponent` 修改为如下内容:
Next, add a `HeroService` using the following command:
接着使用下列命令添加 `HeroService`:
ng generate service Hero
Then, give it the following contents:
然后,把它的内容改为:
The `HeroListComponent` uses an injected `HeroService` to retrieve heroes from the server
and then presents those heroes to the user as a series of buttons.
The `HeroService` emulates an HTTP service.
It returns an `Observable` of heroes that resolves after a short delay,
both to simulate network latency and to indicate visually
the necessarily asynchronous nature of the application.
`HeroListComponent` 使用一个注入进来的 `HeroService` 来从服务器获取英雄列表,然后用一系列按钮把这些英雄展示给用户。
`HeroService` 模拟了 HTTP 服务。
它返回一个英雄组成的 `Observable` 对象,并会在短暂的延迟之后被解析出来,这是为了模拟网络延迟,并展示应用在自然延迟下的异步效果。
When the user clicks on a hero,
the component sets its `selectedHero` property which
is bound to the `hero` `@Input()` property of the `HeroDetailComponent`.
The `HeroDetailComponent` detects the changed hero and resets its form
with that hero's data values.
当用户点击一个英雄时,组件设置它的 `selectedHero` 属性,它绑定到 `HeroDetailComponent` 的 `@Input()` 属性 `hero` 上。
`HeroDetailComponent` 检测到英雄的变化,并使用当前英雄的值重置此表单。
A refresh button clears the hero list and the current selected hero before refetching the heroes.
"刷新"按钮会清除英雄列表和当前选中的英雄,然后重新获取英雄列表。
Notice that `hero-list.component.ts` imports `Observable` and `finally` while `hero.service.ts` imports `Observable`, `of`,
and `delay` from `rxjs`.
注意,`hero-list.component.ts` 从 `rxjs` 中导入了 `Observable` 和 `finally`,而 `hero.service.ts` 导入了 `Observable`、`of` 和 `delay`。
The remaining `HeroListComponent` and `HeroService` implementation details are beyond the scope of this tutorial.
However, the techniques involved are covered elsewhere in the documentation, including the _Tour of Heroes_
[here](tutorial/toh-pt3 "ToH: Multiple Components") and [here](tutorial/toh-pt4 "ToH: Services").
`HeroListComponent` 和 `HeroService` 的其它实现细节超出了本教程的范围。
不过,它所涉及的技术包含在文档的其它部分,包括《英雄指南》的 [这里](tutorial/toh-pt3 "ToH: Multiple Components") 和 [这里](tutorial/toh-pt4 "ToH: Services")。
To use the `HeroService`, import it into `AppModule` and add it to the `providers` array. To use the `HeroListComponent`, import it, declare it, and export it:
要使用 `HeroService`,就要把它导入到 `AppModule` 中,并添加到 `providers` 数组里。
要使用 `HeroListComponent`,就要导入它、声明它并导出它:
Next, update the `HeroListComponent` template with the following:
接下来,把 `HeroListComponent` 的模板升级为:
These changes need to be reflected in the `AppComponent` template. Replace the contents of `app.component.html` with updated markup to use the `HeroListComponent`, instead of the `HeroDetailComponent`:
这些修改需要反映到 `AppComponent` 模板中。把 `app.component.html` 替换为如下内容,以便把 `HeroDetailComponent` 替换为 `HeroListComponent`:
Finally, add an `@Input()` property to the `HeroDetailComponent`
so `HeroDetailComponent` can receive the data from `HeroListComponent`. Remember to add the `Input` symbol to the `@angular/core ` `import` statement in the list of JavaScript imports too.
最后,为 `HeroDetailComponent` 添加一个 `@Input()` 属性,让它能从 `HeroListComponent` 中接收数据。
别忘了也要把来自 `@angular/core` 的 `Input` 符号 `import` 进来。
Now you should be able to click on a button for a hero and a form renders.
你先,你就可以点击一个按钮并渲染一个表单了。
## When to set form model values (`ngOnChanges`)
## 何时设置表单模型的值(`ngOnChanges`)
When to set form model values depends upon when the component gets the data model values.
何时设置表单模型的值取决于组件何时获得数据模型的值。
The `HeroListComponent` displays hero names to the user.
When the user clicks on a hero, the `HeroListComponent` passes the selected hero into the `HeroDetailComponent`
by binding to its `hero` `@Input()` property.
`HeroListComponent` 会给用户显示英雄的名字。
当用户点击某个英雄时,`HeroListComponent` 会通过绑定到 `hero` 这个输入属性,把选中的英雄传给 `HeroDetailComponent`。
In this approach, the value of `hero` in the `HeroDetailComponent` changes
every time the user selects a new hero.
You can call `setValue()` using the [ngOnChanges](guide/lifecycle-hooks#onchanges)
lifecycle hook, which Angular calls whenever the `@Input()` `hero` property changes.
这种方式下,每当用户选择一个新英雄时,`HeroDetailComponent` 的 `hero` 值就会发生变化。
你可以通过 [ngOnChanges](guide/lifecycle-hooks#onchanges) 生命周期钩子来调用 `setValue()`。只要 `hero` 这个输入属性发生了变化,Angular 就会调用这个钩子。
### Reset the form
### 重置表单
First, import the `OnChanges` symbol in `hero-detail.component.ts`.
首先,在 `hero-detail.component.ts` 中导入 `OnChanges` 符号。
Next, let Angular know that the `HeroDetailComponent` implements `OnChanges`:
接着,让 Angular 知道 `HeroDetailComponent` 实现了 `OnChanges`:
Add the `ngOnChanges` method to the class as follows:
向该类中添加 `ngOnChanges` 方法,代码如下:
Notice that it calls `rebuildForm()`, which is a method where you
can set the values. You can name `rebuildForm()` anything that makes sense to you. It isn't built into Angular, but is a method you create to effectively leverage the `ngOnChanges` lifecycle hook.
注意,它调用了 `rebuildForm()`,该函数是一个方法,在这里你可以对值进行设置。
你可以把 `rebuildForm()` 命名为任何对你有意义的名字。
它不是 Angular 内置的,而是你自己创建的方法,用以更有效的利用 `ngOnChanges` 钩子。
The `rebuildForm()` method does two things; resets the hero's name and the address.
`rebuildForm()` 方法会做两件事:重置英雄的名字和地址。
{@a form-array}
## Use _FormArray_ to present an array of `FormGroups`
## 使用 `FormArray` 来表示 `FormGroup` 数组
A `FormGroup` is a named object whose property values are `FormControls` and other `FormGroups`.
`FormGroup` 是一个命名对象,它的属性值是 `FormControl` 和其它的 `FormGroup`。
Sometimes you need to present an arbitrary number of controls or groups.
For example, a hero may have zero, one, or any number of addresses.
有时你需要表示任意数量的控件或控件组。
比如,一个英雄可能拥有 0、1 或任意数量的住址。
The `Hero.addresses` property is an array of `Address` instances.
An `address` `FormGroup` can display one `Address`.
An Angular `FormArray` can display an array of `address` `FormGroups`.
`Hero.addresses` 属性就是一个 `Address` 实例的数组。
一个 `address` 的 `FormGroup` 可以显示一个 `Address` 对象。
而 `FormArray` 可以显示一个 `address` `FormGroup` 的数组。
To get access to the `FormArray` class, import it into `hero-detail.component.ts`:
要访问 `FormArray` 类,请先把它导入 `hero-detail.component.ts` 中:
To work with a `FormArray` do the following:
要使用 `FormArray`,就要这么做:
1. Define the items in the array; that is, `FormControls` or `FormGroups`.
在数组中定义条目 `FormControl` 或 `FormGroup`。
1. Initialize the array with items created from data in the data model.
把这个数组初始化微一组从*数据模型*中的数据创建的条目。
1. Add and remove items as the user requires.
根据用户的需求添加或移除这些条目。
Define a `FormArray` for `Hero.addresses` and
let the user add or modify addresses.
为 `Hero.addresses` 定义了一个 `FormArray`,并且让用户添加或修改这些住址。
You’ll need to redefine the form model in the `HeroDetailComponent` `createForm()` method,
which currently only displays the first hero address in an `address` `FormGroup`:
你需要在 `HeroDetailComponent` 的 `createForm()` 中重新定义表单模型,它现在只在 `address` `FormGroup` 中显示第一个英雄住址。
### From `address` to `secretLairs`
### 从 `address`(住址)到 *`secretLairs`(秘密小屋)
From the user's point of view, heroes don't have _addresses_.
Addresses are for mere mortals. Heroes have _secret lairs_!
Replace the address `FormGroup` definition with a `secretLairs` `FormArray` definition:
在用户看来,英雄们没有*住址*。
只有我们凡人才有*住址*,英雄们拥有的是*秘密小屋*!
把 `FormGroup` 型的住址替换为 `FormArray` 型的 `secretLairs` 定义:
In `hero-detail.component.html` change `formArrayName="address"` to `formArrayName="secretLairs"`.
在 `hero-detail.component.html` 中 把 `formArrayName="address"` 改为 `formArrayName="secretLairs"`。
Changing the form control name from `address` to `secretLairs` underscores an important point:
the _form model_ doesn't have to match the _data model_.
把表单的控件名从 `address` 改为 `secretLairs` 时导致了一个重要问题:*表单模型*与*数据模型*不再匹配了。
Obviously, there has to be a relationship between the two.
But it can be anything that makes sense within the application domain.
显然,必须在两者之间建立关联。但它在应用领域中的意义不限于此,它可以用于任何东西。
_Presentation_ requirements often differ from _data_ requirements.
The reactive forms approach both emphasizes and facilitates this distinction.
*展现*的需求经常会与*数据*的需求不同。
响应式表单的方法既强调这种差异,也能为这种差异提供了便利。
### Initialize the `secretLairs` _FormArray_
### 初始化 `FormArray` 型的 `secretLairs`
The default form displays a nameless hero with no addresses.
默认的表单显示一个无地址的无名英雄。
You need a method to populate (or repopulate) the `secretLairs` with actual hero addresses whenever
the parent `HeroListComponent` sets the `HeroDetailComponent.hero` `@Input()` property to a new `Hero`.
你需要一个方法来用实际英雄的地址填充(或重新填充)`secretLairs`,
而不用管父组件 `HeroListComponent` 何时把 `@Input()` 属性 `HeroDetailComponent.hero` 设置为一个新的 `Hero`。
The following `setAddresses()` method replaces the `secretLairs` `FormArray` with a new `FormArray`,
initialized by an array of hero address `FormGroups`. Add this to the `HeroDetailComponent` class:
下面的 `setAddresses()` 方法把 `secretLairs` 这个 `FormArray` 替换为一个新的 `FormArray`,使用一组表示英雄地址的 `FormGroup` 来进行初始化。在 `HeroDetailComponent` 类中添加下列内容:
Notice that you replace the previous `FormArray` with the
`FormGroup.setControl()` method, not with `setValue()`.
You're replacing a _control_, not the _value_ of a control.
注意,你使用 `FormGroup.setControl()` 方法,而不是 `setValue()` 方法来替换前一个 `FormArray`。
你所要替换的是*控件*,而不是控件的*值*。
Notice also that the `secretLairs` `FormArray` contains `FormGroups`, not `Addresses`.
还要注意,`secretLairs` 数组中包含的是**`FormGroup`,而不是 `Address`。
Next, call `setAddresses()` from within `rebuildForm()`:
接着,在 `rebuildForm()` 中调用 `setAddresses()`:
### Get the _FormArray_
### 获取 `FormArray`
The `HeroDetailComponent` should be able to display, add, and remove items from the `secretLairs` `FormArray`.
`HeroDetailComponent` 应该能从 `secretLairs` `FormArray` 中显示、添加和删除条目。
Use the `FormGroup.get()` method to acquire a reference to that `FormArray`.
Wrap the expression in a `secretLairs` convenience property for clarity and re-use. Add the following to `HeroDetailComponent`.
使用 `FormGroup.get()` 方法来获取到 `FormArray` 的引用。
把这个表达式包装进一个名叫 `secretLairs` 的便捷属性中来让它更清晰,并供复用。
在 `HeroDetailComponent` 中添加下列内容。
### Display the _FormArray_
### 显示 `FormArray`
The current HTML template displays a single `address` `FormGroup`.
Revise it to display zero, one, or more of the hero's `address` `FormGroups`.
当前 HTML 模板显示单个的地址 `FormGroup`。
要把它修改成能显示 0、1 或更多个表示英雄地址的 `FormGroup`。
This is mostly a matter of wrapping the previous template HTML for an address in a `
` and
repeating that `
` with `*ngFor`.
要改的部分主要是把以前表示地址的 HTML 模板包裹进一个 `
` 中,并且使用 `*ngFor` 来重复渲染这个 `
`。
There are three key points when writing the `*ngFor`:
写这个 `*ngFor` 有三个要点:
1. Add another wrapping `
`, around the `
` 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 模板中*秘密小屋*部分的代码骨架:
Here's the complete template for the secret lairs section. Add this to `HeroDetailComponent` template, replacing the `forGroupName=address` `
`:
这里是*秘密小屋*部分的完整模板:
### 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` 添加到其中。
Place a button on the form so the user can add a new _secret lair_ and wire it to the component's `addLair()` method. Put it just before the closing `
` of the `secretLairs` `FormArray`.
把一个按钮放在表单中,以便用户可以添加新的*秘密小屋*,并把它传给组件的 `addLair()` 方法。
Be sure to add the `type="button"` attribute
because without an explicit 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"` 属性。
因为如果不明确指定类型,按钮的默认类型就是“submit”(提交)。
当你稍后添加了提交表单的动作时,每个“submit”按钮都是触发一次提交操作,而它将可能会做一些处理,比如保存当前的修改。
你显然不会希望每当用户点击“Add a Secret Lair”按钮时就保存一次。
### 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 中看到这一点。
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 模板的一个按钮上。
{@a observe-control}
## 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 `secretLairs`.
Fortunately, you can learn about such changes by subscribing to one of the `FormControl` 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`.
添加下列方法,以监听 `name` 这个 `FormControl` 中值的变化。
Call it in the constructor, after `createForm()`.
在构造函数中调用它,就在 `createForm()` 之后。
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` 绑定在组件模板的底部显示这个数组:
Return to the browser, select a hero; for example, Magneta, and start typing in the `name` `
`.
You should see a new name in the log after each keystroke.
返回浏览器,选择一个英雄(比如“Magneta”),并开始在 `name` 输入框中键入。
你会看到,每次按键都会记录一个新名字。
### When to use it
### 什么时候用它
An interpolation binding is the easier way to display a name change.
Subscribing to an observable `FormControl` property is handy for triggering
application logic within the component class.
插值表达式绑定时显示姓名变化比较简单的方式。
在组件类中订阅表单控件属性变化的可观察对象以触发应用逻辑则是比较难的方式。
{@a save}
## 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, revert unsaved changes, and resume editing.
After you implement both features in this section, the form will look like this:
`HeroDetailComponent` 捕获了用户输入,但没有用它做任何事。
在真实的应用中,你可能要保存这些英雄的变化。
在真实的应用中,你还要能丢弃未保存的变更,然后继续编辑。
在实现完本节的这些特性之后,表单是这样的:
### Save
### 保存
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`. Add the following to `HeroDetailComponent`.
当用户提交表单时,`HeroDetailComponent` 会把英雄实例的*数据模型*传给所注入进来的 `HeroService` 的一个方法来进行保存。
在 `HeroDetailComponent` 中添加如下内容:
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()` 助手来深层复制变化后的模型值。
Make sure to import `HeroService` and add it to the constructor:
确保导入了 `HeroService` 并把它加入了构造函数中:
**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` 对象的复本,因此实际上并没有修改原有对象。
### 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 `rebuildForm()` method that built the form model from the original, unchanged `hero` data model.
丢弃很容易。只要重新执行 `rebuildForm()` 方法就可以根据原始的、未修改过的 `hero` 数据模型重新构建出表单模型。
### Buttons
### 按钮
Add the "Save" and "Revert" buttons near the top of the component's template:
把“Save”和“Revert”按钮添加到组件模板的顶部:
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` 方法。
现在,用户可以保存或放弃修改了。
Try the
.
试试
.
{@a source-code}
The key files of the final version are as follows:
最终版中的核心文件如下:
You can download the complete source for all steps in this guide
from the
Reactive Forms Demo live example.
你可以到
响应式表单在线例子 中下载本章所有步骤的完整代码。