1277 lines
55 KiB
Plaintext
1277 lines
55 KiB
Plaintext
include ../_util-fns
|
||
|
||
:marked
|
||
We’ve all used a form to log in, submit a help request, place an order, book a flight,
|
||
schedule a meeting and perform countless other data entry tasks.
|
||
Forms are the mainstay of business applications.
|
||
|
||
表单是商业应用的支柱,我们用它来执行登录、求助、下单、预订机票、安排会议,以及不计其数的其它数据录入任务。
|
||
|
||
Any seasoned web developer can slap together an HTML form with all the right tags.
|
||
It's more challenging to create a cohesive data entry experience that guides the
|
||
user efficiently and effectively through the workflow behind the form.
|
||
|
||
任何经验丰富的 Web 开发人员都能使用适当的标签拼凑出 HTML 表单。
|
||
但是,要想做出具有贴心的数据输入体验的表单,
|
||
引导用户明晰、高效地完成表单背后的工作流程,挑战就大多了。
|
||
|
||
*That* takes design skills that are, to be frank, well out of scope for this guide.
|
||
|
||
坦白地讲,*这当中*所需要的设计技能超出了本章的范围。
|
||
|
||
It also takes framework support for
|
||
**two-way data binding, change tracking, validation, and error handling**
|
||
... which we shall cover in this guide on Angular forms.
|
||
|
||
**双向数据绑定、变更跟踪、有效性验证和错误处理**等功能离不开框架的支持。
|
||
本章将介绍 Angular 表单相关的内容。
|
||
|
||
We will build a simple form from scratch, one step at a time. Along the way we'll learn how to
|
||
|
||
下面,从零开始,一步一步构建出一个简单的表单。在这个过程中,我们将学会如何:
|
||
|
||
- build an Angular form with a component and template
|
||
|
||
使用组件和模板构建 Angular 表单
|
||
|
||
- two-way data bind with `[(ngModel)]` syntax for reading and writing values to input controls
|
||
|
||
`[(ngModel)]`语法实现双向数据绑定,用于读取和写入输入控件的值
|
||
|
||
- track the change state and validity of form controls using `ngModel` in combination with a form
|
||
|
||
结合表单来使用`ngModel`,可以跟踪表单控件的状态变化和有效性
|
||
|
||
- provide strong visual feedback using special CSS classes that track the state of the controls
|
||
|
||
使用特殊的 CSS 类来跟踪控件状态,并提供强烈的视觉反馈
|
||
|
||
- display validation errors to users and enable/disable form controls
|
||
|
||
向用户显示验证错误提示,以及启用/禁用表单控件
|
||
|
||
- use [template reference variables](./template-syntax.html#ref-vars) for sharing information among HTML elements
|
||
|
||
利用[模板引用变量](./template-syntax.html#ref-vars)在 HTML 元素之间共享信息
|
||
|
||
Run the <live-example></live-example>.
|
||
|
||
运行<live-example>在线例子</live-example>
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Template-Driven Forms
|
||
|
||
## 模板驱动的表单
|
||
|
||
Many of us will build forms by writing templates in the Angular [template syntax](./template-syntax.html) with
|
||
the form-specific directives and techniques described in this guide.
|
||
|
||
通常,使用 Angular [模板语法](./template-syntax.html)编写模板,结合本章所描述的表单专用指令和技术来构建表单。
|
||
.l-sub-section
|
||
:marked
|
||
That's not the only way to create a form but it's the way we'll cover in this guide.
|
||
|
||
这不是创建表单的唯一方式,本章中只介绍模板驱动的表单。
|
||
:marked
|
||
We can build almost any form we need with an Angular template — login forms, contact forms ... pretty much any business forms.
|
||
We can lay out the controls creatively, bind them to data, specify validation rules and display validation errors,
|
||
conditionally enable or disable specific controls, trigger built-in visual feedback, and much more.
|
||
|
||
利用 Angular 模板,可以构建几乎所有表单 — 登录表单、联系人表单…… 以及任何的商务表单。
|
||
可以创造性的摆放各种控件、把它们绑定到数据、指定校验规则、显示校验错误、有条件的禁用或
|
||
启用特定的控件、触发内置的视觉反馈等等,不胜枚举。
|
||
|
||
It will be pretty easy because Angular handles many of the repetitive, boiler plate tasks we'd
|
||
otherwise wrestle with ourselves.
|
||
|
||
它用起来很简单,这是因为 Angular 处理了大多数重复、单调的任务,这让我们可以不必亲自操刀、身陷其中。
|
||
|
||
We'll discuss and learn to build the following template-driven form:
|
||
|
||
我们将讨论和学习构建如下的“模板驱动”表单:
|
||
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/hero-form-1.png" width="400px" alt="Clean Form")
|
||
|
||
:marked
|
||
Here at the *Hero Employment Agency* we use this form to maintain personal information about the
|
||
heroes in our stable. Every hero needs a job. It's our company mission to match the right hero with the right crisis!
|
||
|
||
这里是*英雄职业介绍所*,使用这个表单来维护候选英雄们的个人信息。每个英雄都需要一份工作。
|
||
公司的任务就是让适当的英雄去解决它/她所擅长应对的危机!
|
||
|
||
Two of the three fields on this form are required. Required fields have a green bar on the left to make them easy to spot.
|
||
|
||
表单中的三个字段,其中两个是必填的。必填的字段在左侧有个绿色的竖条,方便用户分辨哪些是必填项。
|
||
|
||
If we delete the hero name, the form displays a validation error in an attention grabbing style:
|
||
|
||
如果删除了英雄的名字,表单就会用醒目的样式把验证错误显示出来。
|
||
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/hero-form-2.png" width="400px" alt="无效!名字是必填项")
|
||
|
||
:marked
|
||
Note that the submit button is disabled and the "required" bar to the left of the input control changed from green to red.
|
||
|
||
注意,提交按钮被禁用了,而且输入控件左侧的“必填”条从绿色变为了红色。
|
||
|
||
.l-sub-section
|
||
p We'll customize the colors and location of the "required" bar with standard CSS.
|
||
|
||
p 稍后,会使用标准 CSS 来定制“必填”条的颜色和位置。
|
||
|
||
:marked
|
||
We will build this form in the following sequence of small steps
|
||
|
||
按下面的步骤,一点一点构建此表单:
|
||
|
||
1. Create the `Hero` model class
|
||
|
||
创建`Hero`模型类
|
||
|
||
1. Create the component that controls the form
|
||
|
||
创建控制此表单的组件
|
||
|
||
1. Create a template with the initial form layout
|
||
|
||
创建具有初始表单布局的模板
|
||
|
||
1. Bind data properties to each form input control with the `ngModel` two-way data binding syntax
|
||
|
||
使用`ngModel`双向数据绑定语法把数据属性绑定到每个表单输入控件
|
||
|
||
1. Add the `name` attribute to each form input control
|
||
|
||
往每个表单输入控件上添加`name`属性 (attribute)
|
||
|
||
1. Add custom CSS to provide visual feedback
|
||
|
||
添加自定义 CSS 来提供视觉反馈
|
||
|
||
1. Show and hide validation error messages
|
||
|
||
显示和隐藏有效性验证的错误信息
|
||
|
||
1. Handle form submission with **ngSubmit**
|
||
|
||
使用 **ngSubmit** 处理表单提交
|
||
|
||
1. Disable the form’s submit button until the form is valid
|
||
|
||
禁用此表单的提交按钮,直到表单变为有效
|
||
|
||
:marked
|
||
## Setup
|
||
|
||
## 搭建
|
||
|
||
Follow the [setup](setup.html) instructions for creating a new project
|
||
named <span ngio-ex>angular-forms</span>.
|
||
|
||
按照[搭建本地开发环境](setup.html)的说明,创建一个名为<span ngio-ex>angular-forms</span>的新项目。
|
||
|
||
## Create the Hero Model Class
|
||
|
||
## 创建 Hero 模型类
|
||
|
||
As users enter form data, we capture their changes and update an instance of a model.
|
||
We can't layout the form until we know what the model looks like.
|
||
|
||
当用户输入表单数据时,需要捕获它们的变化,并更新到模型的实例中。
|
||
除非知道模型里有什么,否则无法设计表单的布局。
|
||
|
||
A model can be as simple as a "property bag" that holds facts about a thing of application importance.
|
||
That describes well our `Hero` class with its three required fields (`id`, `name`, `power`)
|
||
and one optional field (`alterEgo`).
|
||
|
||
最简单的模型是个“属性包”,用来存放应用中一件事物的事实。
|
||
这里使用三个必备字段 (`id`、`name`、`power`),和一个可选字段 (`alterEgo`,译注:中文含义是第二人格,例如 X 战警中的 Jean / 黑凤凰)。
|
||
|
||
Create a new file in the app folder called `hero.ts` and give it the following class definition:
|
||
|
||
在应用文件夹中创建`hero.ts`文件,并且写入下列类定义内容:
|
||
|
||
+makeExample('forms/ts/app/hero.ts', null, 'app/hero.ts')
|
||
|
||
:marked
|
||
It's an anemic model with few requirements and no behavior. Perfect for our demo.
|
||
|
||
这是一个少量需求和零行为的贫血模型。对演示来说很完美。
|
||
|
||
The TypeScript compiler generates a public field for each `public` constructor parameter and
|
||
assigns the parameter’s value to that field automatically when we create new heroes.
|
||
|
||
TypeScript 编译器为每个`public`构造函数参数生成一个公共字段,在创建新的英雄实例时,自动把参数值赋给这些公共字段。
|
||
|
||
The `alterEgo` is optional and the constructor lets us omit it; note the (?) in `alterEgo?`.
|
||
|
||
`alterEgo`是可选的,调用构造函数时可省略,注意`alterEgo?`中的问号 (?)。
|
||
|
||
We can create a new hero like this:
|
||
|
||
可以像这样创建新英雄:
|
||
code-example(format="").
|
||
let myHero = new Hero(42, 'SkyDog',
|
||
'Fetch any object at any distance',
|
||
'Leslie Rollover');
|
||
console.log('My hero is called ' + myHero.name); // "My hero is called SkyDog"
|
||
:marked
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Create a Form component
|
||
|
||
## 创建表单组件
|
||
|
||
An Angular form has two parts: an HTML-based _template_ and a component _class_
|
||
to handle data and user interactions programmatically.
|
||
|
||
Angular 表单分为两部分:基于 HTML 的*模板*和组件*类*,用来程序处理数据和用户交互。
|
||
|
||
We begin with the class because it states, in brief, what the hero editor can do.
|
||
|
||
先从组件类开始,是因为它可以简要说明英雄编辑器能做什么。
|
||
|
||
Create a new file called `hero-form.component.ts` and give it the following definition:
|
||
|
||
创建名叫`hero-form.component.ts`的文件,放进下列定义:
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.ts', 'first', 'app/hero-form.component.ts')
|
||
|
||
:marked
|
||
There’s nothing special about this component, nothing form-specific, nothing to distinguish it from any component we've written before.
|
||
|
||
这个组件没有什么特别的地方,没有表单相关的东西,与之前写过的组件没什么不同。
|
||
|
||
Understanding this component requires only the Angular concepts we’ve learned in previous guides
|
||
|
||
只需要前面章节中学过的概念,就可以完全理解这个组件:
|
||
|
||
1. We import the `Component` decorator from the Angular library as we usually do.
|
||
|
||
像往常一样,从 Angular 库中导入`Component`装饰器。
|
||
|
||
1. We import the `Hero` model we just created.
|
||
|
||
导入刚刚创建的`Hero`模型。
|
||
|
||
1. The `@Component` selector value of "hero-form" means we can drop this form in a parent template with a `<hero-form>` tag.
|
||
|
||
`@Component`选择器"hero-form"表示可以用`<hero-form>`标签把这个表单放进父模板。
|
||
|
||
1. The `moduleId: module.id` property sets the base for module-relative loading of the `templateUrl`.
|
||
|
||
`moduleId: module.id`属性设置了基地址,用于从相对模块路径加载`templateUrl`。
|
||
|
||
1. The `templateUrl` property points to a separate file for the template HTML called `hero-form.component.html`.
|
||
|
||
`templateUrl`属性指向一个独立的 HTML 模板文件,名叫`hero-form.component.html`。
|
||
|
||
1. We defined dummy data for `model` and `powers` as befits a demo.
|
||
Down the road, we can inject a data service to get and save real data
|
||
or perhaps expose these properties as [inputs and outputs](./template-syntax.html#inputs-outputs) for binding to a
|
||
parent component. None of this concerns us now and these future changes won't affect our form.
|
||
|
||
为`model`和`powers`定义了供演示用的假数据。
|
||
将来,可以注入服务来获取和保存真实数据,
|
||
或者暴露这些属性为[输入与输出属性](./template-syntax.html#inputs-outputs),绑定到父组件上。
|
||
先不关心这些,因为这些未来的变化不会影响到表单。
|
||
|
||
1. We threw in a `diagnostic` property at the end to return a JSON representation of our model.
|
||
It'll help us see what we're doing during our development; we've left ourselves a cleanup note to discard it later.
|
||
|
||
在最后增加`diagnostic`属性,它返回这个模型的 JSON 形式。
|
||
在开发过程中,它用于调试,最后清理时会丢弃它。
|
||
|
||
### Why the separate template file?
|
||
|
||
### 为何分离模板文件?
|
||
|
||
Why don't we write the template inline in the component file as we often do elsewhere?
|
||
|
||
为什么不与我们在其他地方常常做的那样,以内联的方式把模板写在组件文件中呢?
|
||
|
||
There is no “right” answer for all occasions. We like inline templates when they are short.
|
||
Most form templates won't be short. TypeScript and JavaScript files generally aren't the best place to
|
||
write (or read) large stretches of HTML and few editors are much help with files that have a mix of HTML and code.
|
||
We also like short files with a clear and obvious purpose like this one.
|
||
|
||
没有什么答案在所有场合都总是“正确”的。当模板足够短的时候,内联形式更招人喜欢。
|
||
但大多数的表单模板都不短。通常,TypeScript 和 JavaScript 文件不是写(读)大型 HTML 的好地方,
|
||
而且没有几个编辑器能对混写的 HTML 和代码提供足够的帮助。
|
||
我们还是喜欢内容清晰、目标明确的短文件,像这个一样。
|
||
|
||
Form templates tend to be quite large even when displaying a small number of fields
|
||
so it's usually best to put the HTML template in a separate file.
|
||
We'll write that template file in a moment. Before we do, we'll take a step back
|
||
and revise the `app.module.ts` and `app.component.ts` to make use of the new `HeroFormComponent`.
|
||
|
||
就算是在仅仅显示少数表单项目时,表单模板一般都比较庞大。所以通常最好的方式是将 HTML 模板放到单独的文件中。
|
||
一会儿将编写这个模板文件。在这之前,先退一步,再看看`app.module.ts`和`app.component.ts`,让它们使用新的`HeroFormComponent`。
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Revise the *app.module.ts*
|
||
|
||
## 修改 *app.module.ts*
|
||
|
||
`app.module.ts` defines the application's root module. In it we identify the external modules we'll use in our application
|
||
and declare the components that belong to this module, such as our `HeroFormComponent`.
|
||
|
||
`app.module.ts`定义了应用的根模块。其中标识即将用到的外部模块,以及声明属于本模块中的组件,例如`HeroFormComponent`。
|
||
|
||
Because template-driven forms are in their own module, we need to add the `FormsModule` to the array of
|
||
`imports` for our application module before we can use forms.
|
||
|
||
因为模板驱动的表单位于它们自己的模块,所以在使用表单之前,需要将`FormsModule`添加到应用模块的`imports`数组中。
|
||
|
||
Replace the contents of the "QuickStart" version with the following:
|
||
|
||
把“快速起步”版的文件替换为如下内容:
|
||
+makeExample('forms/ts/app/app.module.ts', null, 'app/app.module.ts')
|
||
|
||
:marked
|
||
.l-sub-section
|
||
:marked
|
||
There are three changes:
|
||
|
||
有三处更改:
|
||
|
||
1. We import `FormsModule` and our new `HeroFormComponent`.
|
||
|
||
导入`FormsModule`和新组件`HeroFormComponent`。
|
||
|
||
1. We add the `FormsModule` to the list of `imports` defined in the `ngModule` decorator. This gives our application
|
||
access to all of the template-driven forms features, including `ngModel`.
|
||
|
||
把`FormsModule`添加到`ngModule`装饰器的`imports`列表中,这样应用就能访问模板驱动表单的所有特性,包括`ngModel`。
|
||
|
||
1. We add the `HeroFormComponent` to the list of `declarations` defined in the `ngModule` decorator. This makes
|
||
the `HeroFormComponent` component visible throughout this module.
|
||
|
||
把`HeroFormComponent`添加到`ngModule`装饰器的`declarations`列表中,使`HeroFormComponent`组件在整个模块中可见。
|
||
|
||
.alert.is-important
|
||
:marked
|
||
If a component, directive, or pipe belongs to a module in the `imports` array, _DON'T_ re-declare it in the `declarations` array.
|
||
If you wrote it and it should belong to this module, _DO_ declare it in the `declarations` array.
|
||
|
||
如果组件、指令或管道出现在模块的`imports`数组中,_不要_把它声明在`declarations`数组中。
|
||
如果它是你自己写的,并且属于当前模块,_就要_把它声明在`declarations`数组中。
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Revise the *app.component.ts*
|
||
|
||
## 修改 *app.component.ts*
|
||
|
||
`app.component.ts` is the application's root component. It will host our new `HeroFormComponent`.
|
||
|
||
`app.component.ts`是应用的根组件,`HeroFormComponent`将被放在其中。
|
||
|
||
Replace the contents of the "QuickStart" version with the following:
|
||
|
||
把"快速起步"的版本内容替换成下列代码:
|
||
+makeExample('forms/ts/app/app.component.ts', null, 'app/app.component.ts')
|
||
|
||
:marked
|
||
.l-sub-section
|
||
:marked
|
||
There is only one change.
|
||
|
||
仅有的一处修改。
|
||
|
||
1. The `template` is simply the new element tag identified by the component's `selector` property.
|
||
This will display the hero form when the application component is loaded.
|
||
|
||
1. `template`中只有新元素标签,即组件的`selector`属性。当应用组件被加载时,将显示这个英雄表单。
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Create an initial HTML Form Template
|
||
|
||
## 创建初始 HTML 表单模板
|
||
|
||
Create a new template file called `hero-form.component.html` and give it the following definition:
|
||
|
||
新建模板文件,命名为`hero-form.component.html`,并且填写如下内容:
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'start', 'app/hero-form.component.html')
|
||
|
||
:marked
|
||
That is plain old HTML 5. We're presenting two of the `Hero` fields, `name` and `alterEgo`, and
|
||
opening them up for user input in input boxes.
|
||
|
||
这只是一段普通的旧式 HTML 5 代码。这里有两个`Hero`字段,`name`和`alterEgo`,供用户输入。
|
||
|
||
The *Name* `<input>` control has the HTML5 `required` attribute;
|
||
the *Alter Ego* `<input>` control does not because `alterEgo` is optional.
|
||
|
||
*Name* `<input>`控件具有 HTML5 的`required`属性;但 *Alter Ego* `<input>`控件没有,因为`alterEgo`字段是可选的。
|
||
|
||
We've got a *Submit* button at the bottom with some classes on it for styling.
|
||
|
||
底部有个 *Submit* 按钮,具有一些 CSS 样式类。
|
||
|
||
**We are not using Angular yet**. There are no bindings. No extra directives. Just layout.
|
||
|
||
**还没有真正用到Angular**。没有绑定,没有额外的指令,只有布局。
|
||
|
||
The `container`, `form-group`, `form-control`, and `btn` classes
|
||
come from [Twitter Bootstrap](http://getbootstrap.com/css/). Purely cosmetic.
|
||
We're using Bootstrap to give the form a little style!
|
||
|
||
`container`、`form-group`、`form-control`和`btn`类来自 [Twitter Bootstrap](http://getbootstrap.com/css/)。纯粹是装饰。
|
||
我们使用 Bootstrap 来美化表单。嘿,一点样式都没有的表单算个啥!
|
||
|
||
.callout.is-important
|
||
header Angular Forms Do Not Require A Style Library
|
||
|
||
header Angular 表单不需要任何样式库
|
||
|
||
:marked
|
||
Angular makes no use of the `container`, `form-group`, `form-control`, and `btn` classes or
|
||
the styles of any external library. Angular apps can use any CSS library
|
||
... or none at all.
|
||
|
||
Angular 不需要`container`、`form-group`、`form-control`和`btn`类,
|
||
或者外部库的任何样式。Angular 应用可以使用任何 CSS 库…… ,或者啥都不用。
|
||
|
||
:marked
|
||
Let's add the stylesheet.
|
||
|
||
添加样式表。
|
||
|
||
ol
|
||
li
|
||
p Open a terminal window in the application root folder and enter the command:
|
||
|
||
p 在应用的根目录下打开终端窗口,输入如下命令:
|
||
|
||
code-example(language="html" escape="html").
|
||
npm install bootstrap --save
|
||
|
||
li
|
||
p Open <code>index.html</code> and add the following link to the <code><head></code>.
|
||
|
||
p 打开<code>index.html</code>文件并且把下列链接添加到<code><head></code>中。
|
||
|
||
+makeExample('forms/ts/index.html', 'bootstrap')(format=".")
|
||
:marked
|
||
.l-main-section
|
||
:marked
|
||
## Add Powers with ***ngFor**
|
||
|
||
## 用 ***ngFor*** 添加超能力
|
||
|
||
Our hero may choose one super power from a fixed list of Agency-approved powers.
|
||
We maintain that list internally (in `HeroFormComponent`).
|
||
|
||
英雄可以从认证过的固定列表中选择一项超能力。
|
||
这个列表位于`HeroFormComponent`中。
|
||
|
||
We'll add a `select` to our
|
||
form and bind the options to the `powers` list using `ngFor`,
|
||
a technique seen previously in the [Displaying Data](./displaying-data.html) guide.
|
||
|
||
在表单中添加`select`,用`ngFor`把`powers`列表绑定到列表选项。
|
||
我们在之前的[显示数据](./displaying-data.html)一章中见过`ngFor`。
|
||
|
||
Add the following HTML *immediately below* the *Alter Ego* group.
|
||
|
||
在 *Alter Ego* 的紧下方添加如下 HTML:
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'powers', 'app/hero-form.component.html (excerpt)')(format=".")
|
||
|
||
:marked
|
||
We are repeating the `<options>` tag for each power in the list of Powers.
|
||
The `pow` template input variable is a different power in each iteration;
|
||
we display its name using the interpolation syntax with the double-curly-braces.
|
||
|
||
列表中的每一项超能力都会渲染成`<option>`标签。
|
||
模板输入变量`p`在每个迭代指向不同的超能力,使用双花括号插值表达式语法来显示它的名称。
|
||
|
||
<a id="ngModel"></a>
|
||
.l-main-section
|
||
:marked
|
||
## Two-way data binding with **_ngModel_**
|
||
|
||
## 使用 ***ngModel*** 进行双向数据绑定
|
||
Running the app right now would be disappointing.
|
||
|
||
如果立即运行此应用,你将会失望。
|
||
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/hero-form-3.png" width="400px" alt="没有数据绑定的早期表单")
|
||
:marked
|
||
We don't see hero data because we are not binding to the `Hero` yet.
|
||
We know how to do that from earlier guides.
|
||
[Displaying Data](./displaying-data.html) taught us Property Binding.
|
||
[User Input](./user-input.html) showed us how to listen for DOM events with an
|
||
Event Binding and how to update a component property with the displayed value.
|
||
|
||
因为还没有绑定到某个英雄,所以看不到任何数据。
|
||
解决方案见前面的章节。
|
||
[显示数据](./displaying-data.html)介绍了属性绑定。
|
||
[用户输入](./user-input.html)介绍了如何通过事件绑定来监听 DOM 事件,以及如何用显示值更新组件的属性。
|
||
|
||
Now we need to display, listen, and extract at the same time.
|
||
|
||
现在,需要同时进行显示、监听和提取。
|
||
|
||
We could use those techniques again in our form.
|
||
Instead we'll introduce something new, the `[(ngModel)]` syntax, that
|
||
makes binding our form to the model super-easy.
|
||
|
||
虽然可以在表单中再次使用这些技术。
|
||
但是,这里将介绍个新东西,`[(ngModel)]`语法,使表单绑定到模型的工作变得超级简单。
|
||
|
||
Find the `<input>` tag for the "Name" and update it like this
|
||
|
||
找到 “Name” 对应的`<input>`标签,并且像这样修改它:
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'ngModelName-1','app/hero-form.component.html (节选)')(format=".")
|
||
|
||
.l-sub-section
|
||
:marked
|
||
We appended a diagnostic interpolation after the input tag
|
||
so we can see what we're doing.
|
||
We left ourselves a note to throw it away when we're done.
|
||
|
||
在 input 标签后添加用于诊断的插值表达式,以看清正在发生什么事。
|
||
给自己留个备注,提醒我们完成后移除它。
|
||
|
||
:marked
|
||
Focus on the binding syntax: `[(ngModel)]="..."`.
|
||
|
||
聚焦到绑定语法`[(ngModel)]="..."`上。
|
||
|
||
If we ran the app right now and started typing in the *Name* input box,
|
||
adding and deleting characters, we'd see them appearing and disappearing
|
||
from the interpolated text.
|
||
At some point it might look like this.
|
||
|
||
如果现在运行这个应用,开始在*姓名*输入框中键入,添加和删除字符,将看到它们从插值结果中显示和消失。
|
||
某一瞬间,它看起来可能是这样:
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/ng-model-in-action.png" width="400px" alt="操作中的ngModel")
|
||
:marked
|
||
The diagnostic is evidence that we really are flowing values from the input box to the model and
|
||
back again. **That's two-way data binding!**
|
||
|
||
诊断信息可以证明,数据确实从输入框流动到模型,再反向流动回来。**这就是双向数据绑定!**
|
||
|
||
Notice that we also added a `name` attribute to our `<input>` tag and set it to "name"
|
||
which makes sense for the hero's name. Any unique value will do, but using a descriptive name is helpful.
|
||
Defining a `name` attribute is a requirement when using `[(ngModel)]` in combination with a form.
|
||
|
||
注意,`<input>`标签还添加了`name`属性 (attribute),并设置为 "name",表示英雄的名字。
|
||
使用任何唯一的值都可以,但使用具有描述性的名字会更有帮助。
|
||
当在表单中使用`[(ngModel)]`时,必须要定义`name`属性。
|
||
|
||
.l-sub-section
|
||
:marked
|
||
Internally Angular creates `FormControls` and registers them with an `NgForm` directive that Angular
|
||
attached to the `<form>` tag. Each `FormControl` is registered under the name we assigned to the `name` attribute.
|
||
We'll talk about `NgForm` [later in this guide](#ngForm).
|
||
|
||
在内部,Angular 创建了一些`FormControl`,并把它们注册到`NgForm`指令,再将该指令附加到`<form>`标签。
|
||
注册每个`FormControl`时,使用`name`属性值作为键值。[本章后面](#ngForm)会讨论`NgForm`。
|
||
|
||
:marked
|
||
Let's add similar `[(ngModel)]` bindings and `name` attributes to *Alter Ego* and *Hero Power*.
|
||
We'll ditch the input box binding message
|
||
and add a new binding at the top to the component's `diagnostic` property.
|
||
Then we can confirm that two-way data binding works *for the entire hero model*.
|
||
|
||
为*第二人格*和*超能力*属性添加类似的`[(ngModel)]`绑定和`name`属性。
|
||
抛弃输入框的绑定消息,在组件顶部添加到`diagnostic`属性的新绑定。
|
||
这样就能确认双向数据绑定*在整个 Hero 模型上*都能正常工作了。
|
||
|
||
After revision the core of our form should have three `[(ngModel)]` bindings and `name` attributes that
|
||
look much like this:
|
||
|
||
修改之后的表单,其核心是三个`[(ngModel)]`绑定,看起来像这样:
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'ngModel-2', 'app/hero-form.component.html (节选)')
|
||
|
||
.l-sub-section
|
||
:marked
|
||
- Each input element has an `id` property that is used by the `label` element's `for` attribute
|
||
to match the label to its input control.
|
||
|
||
每个 input 元素都有`id`属性,`label`元素的`for`属性用它来匹配到对应的输入控件。
|
||
|
||
- Each input element has a `name` property that is required by Angular Forms to register the control with the form.
|
||
|
||
每个 input 元素都有`name`属性,Angular 表单用它注册控件。
|
||
|
||
:marked
|
||
If we ran the app right now and changed every hero model property, the form might display like this:
|
||
|
||
如果现在运行本应用,修改 Hero 模型的每个属性,表单看起来像这样:
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/ng-model-in-action-2.png" width="400px" alt="ngModel in super action")
|
||
:marked
|
||
The diagnostic near the top of the form
|
||
confirms that all of our changes are reflected in the model.
|
||
|
||
表单顶部的诊断信息反映出所做的一切更改。
|
||
|
||
**Delete** the `{{diagnostic}}` binding at the top as it has served its purpose.
|
||
|
||
表单顶部的`{{diagnostic}}`绑定已经完成了它的使命,**删除**它。
|
||
|
||
.l-sub-section
|
||
:marked
|
||
### Inside _[(ngModel)]_
|
||
|
||
### [(ngModel)]内幕
|
||
|
||
*This section is an optional deep dive into [(ngModel)]. Not interested? Skip ahead!*
|
||
|
||
*本节是对[(ngModel)]的深入剖析,它是可选的。不感兴趣?跳过它!*
|
||
|
||
The punctuation in the binding syntax, <span style="font-family:courier"><b>[()]</b></span>, is a good clue to what's going on.
|
||
|
||
绑定语法中的<span style="font-family:courier"><b>[()]</b></span>是很好的线索。
|
||
|
||
In a Property Binding, a value flows from the model to a target property on screen.
|
||
We identify that target property by surrounding its name in brackets, <span style="font-family:courier"><b>[]</b></span>.
|
||
This is a one-way data binding **from the model to the view**.
|
||
|
||
在属性绑定中,值从模型中流动到屏幕上的目标属性 (property)。
|
||
通过把属性名括在方括号中来标记出目标属性,<span style="font-family:courier"><b>[]</b></span>。
|
||
这是**从模型到视图**的单向数据绑定。
|
||
|
||
In an Event Binding, we flow the value from the target property on screen to the model.
|
||
We identify that target property by surrounding its name in parentheses, <span style="font-family:courier"><b>()</b></span>.
|
||
This is a one-way data binding in the opposite direction **from the view to the model**.
|
||
|
||
在事件绑定中,值从屏幕上的目标属性流动到模型。
|
||
通过把属性名括在圆括号中来标记出目标属性,<span style="font-family:courier"><b>()</b></span>。
|
||
这是**从视图到模型**的反向单向数据绑定。
|
||
|
||
No wonder Angular chose to combine the punctuation as <span style="font-family:courier"><b>[()]</b></span>
|
||
to signify a two-way data binding and a **flow of data in both directions**.
|
||
|
||
不出所料,Angular 选择了组合标点 <span style="font-family:courier"><b>[()]</b></span> 来标记出双向数据绑定和**双向数据流**。
|
||
|
||
In fact, we can break the `NgModel` binding into its two separate modes
|
||
as we do in this re-write of the "Name" `<input>` binding:
|
||
|
||
事实上,可以把`NgModel`绑定拆成两个独立的绑定,就像下面重写的 “Name” `<input>`绑定一样:
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'ngModel-3','app/hero-form.component.html (excerpt)')(format=".")
|
||
|
||
:marked
|
||
<br>The Property Binding should feel familiar. The Event Binding might seem strange.
|
||
|
||
<br>这个属性绑定看起来很眼熟,但事件绑定看起来有点怪。
|
||
|
||
The `ngModelChange` is not an `<input>` element event.
|
||
It is actually an event property of the `NgModel` directive.
|
||
When Angular sees a binding target in the form <span style="font-family:courier">[(x)]</span>,
|
||
it expects the `x` directive to have an `x` input property and an `xChange` output property.
|
||
|
||
`ngModelChange`并不是`<input>`元素的事件。
|
||
它实际上是来自`NgModel`指令的事件属性。
|
||
当 Angular 在表单中看到<span style="font-family:courier">[(x)]</span>的绑定目标时,
|
||
它会期待这个`x`指令有一个名为`x`的输入属性,和一个名为`xChange`的输出属性。
|
||
|
||
The other oddity is the template expression, `model.name = $event`.
|
||
We're used to seeing an `$event` object coming from a DOM event.
|
||
The `ngModelChange` property doesn't produce a DOM event; it's an Angular `EventEmitter`
|
||
property that returns the input box value when it fires — which is precisely what
|
||
we should assign to the model's `name` property.
|
||
|
||
模板表达式中的另一个古怪之处是`model.name = $event`。
|
||
之前看到的`$event`对象来自 DOM 事件。
|
||
但`ngModelChange`属性不会生成 DOM 事件 —— 它是Angular `EventEmitter`类型的属性,当它触发时,
|
||
它返回的是输入框的值 —— 也正是希望赋给模型`name`属性的值。
|
||
|
||
Nice to know but is it practical? We almost always prefer `[(ngModel)]`.
|
||
We might split the binding if we had to do something special in
|
||
the event handling such as debounce or throttle the key strokes.
|
||
|
||
很高兴知道这些,但是这样现实吗?实践上中,几乎总是优先使用`[(ngModel)]`形式的双向绑定。
|
||
只有当需要在事件处理函数中做一些特别的事情(例如合并或限制按键频率)时,才会拆分出独立的事件处理函数。
|
||
|
||
Learn more about `NgModel` and other template syntax in the
|
||
[Template Syntax](./template-syntax.html) guide.
|
||
|
||
要学习`ngModel`和其它模板语法的更多知识,见[模板语法](./template-syntax.html)。
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Track change-state and validity with **_ngModel_**
|
||
|
||
## 通过 **ngModel** 跟踪修改状态与有效性验证
|
||
|
||
A form isn't just about data binding. We'd also like to know the state of the controls on our form.
|
||
|
||
表单不仅是关于数据绑定的。我们还想知道表单中各个控件的状态。
|
||
|
||
Using `ngModel` in a form gives us more than just two way data binding. It also tells us if the user touched the control, if the value changed, or if the value became invalid.
|
||
|
||
在表单中使用`ngModel`可以获得比仅使用双向数据绑定更多的控制权。它还会告诉我们很多信息:用户碰过此控件吗?它的值变化了吗?数据变得无效了吗?
|
||
|
||
The *NgModel* directive doesn't just track state; it updates the control with special Angular CSS classes that reflect the state.
|
||
We can leverage those class names to change the appearance of the
|
||
control and make messages appear or disappear.
|
||
|
||
*NgModel* 指令不仅仅跟踪状态。它还使用特定的 Angular CSS 类来更新控件,以反映当前状态。
|
||
可以利用这些 CSS 类来修改控件的外观,显示或隐藏消息。
|
||
|
||
table
|
||
tr
|
||
th
|
||
p State
|
||
p 状态
|
||
th
|
||
p Class if true
|
||
p 为真时的 CSS 类
|
||
th
|
||
p Class if false
|
||
p 为假时的 CSS 类
|
||
tr
|
||
td
|
||
p Control has been visited
|
||
p 控件已经被访问过
|
||
td
|
||
p <code>ng-touched</code>
|
||
p <code>ng-touched</code>
|
||
td
|
||
p <code>ng-untouched</code>
|
||
p <code>ng-untouched</code>
|
||
tr
|
||
td
|
||
p Control's value has changed
|
||
p 控件值已经变化
|
||
td
|
||
p <code>ng-dirty</code>
|
||
p <code>ng-dirty</code>
|
||
td
|
||
p <code>ng-pristine</code>
|
||
p <code>ng-pristine</code>
|
||
tr
|
||
td
|
||
p Control's value is valid
|
||
p 控件值是有效的
|
||
td
|
||
p <code>ng-valid</code>
|
||
p <code>ng-valid</code>
|
||
td
|
||
p <code>ng-invalid</code>
|
||
p <code>ng-invalid</code>
|
||
:marked
|
||
Let's add a temporary [template reference variable](./template-syntax.html#ref-vars) named **spy**
|
||
to the "Name" `<input>` tag and use the spy to display those classes.
|
||
|
||
往姓名`<input>`标签上添加名叫 **spy** 的临时[模板引用变量](./template-syntax.html#local-vars),
|
||
然后用这个 spy 来显示它上面的所有 CSS 类。
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'ngModelName-2','app/hero-form.component.html (excerpt)')(format=".")
|
||
|
||
:marked
|
||
Now run the app and focus on the *Name* input box.
|
||
Follow the next four steps *precisely*
|
||
|
||
现在,运行本应用,并让*姓名*输入框获得焦点。
|
||
然后严格按照下面四个步骤来做:
|
||
|
||
1. Look but don't touch
|
||
|
||
查看输入框,但别碰它
|
||
|
||
1. Click in the input box, then click outside the text input box
|
||
|
||
点击输入框,然后点击输入框外面
|
||
|
||
1. Add slashes to the end of the name
|
||
|
||
在名字的末尾添加些斜杠
|
||
|
||
1. Erase the name
|
||
|
||
删除名字
|
||
|
||
The actions and effects are as follows:
|
||
|
||
动作和它对应的效果如下:
|
||
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/control-state-transitions-anim.gif" alt="控件状态转换")
|
||
:marked
|
||
We should be able to see the following four sets of class names and their transitions:
|
||
|
||
应该能看到下列四组类名以及它们的变迁:
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/ng-control-class-changes.png" width="400px" alt="控件状态转换")
|
||
|
||
:marked
|
||
The (`ng-valid` | `ng-invalid`) pair are most interesting to us. We want to send a
|
||
strong visual signal when the data are invalid and we want to mark required fields.
|
||
So we add custom CSS for visual feedback.
|
||
|
||
(`ng-valid` | `ng-invalid`)这一对是我们最感兴趣的。当数据变得无效时,我们希望发出强力的视觉信号,
|
||
还想要标记出必填字段。可以通过加入自定义 CSS 来提供视觉反馈。
|
||
|
||
**Delete** the `#spy` template reference variable and `TODO` as they have served their purpose.
|
||
|
||
**删除**模板引用变量`#spy`和`TODO`,因为它们已经完成了使命。
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Add Custom CSS for Visual Feedback
|
||
|
||
## 添加用于视觉反馈的自定义 CSS
|
||
|
||
We realize we can mark required fields and invalid data at the same time with a colored bar
|
||
on the left of the input box:
|
||
|
||
可以在输入框的左侧添加带颜色的竖条,用于标记必填字段和无效输入:
|
||
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/validity-required-indicator.png" width="400px" alt="无效表单")
|
||
|
||
:marked
|
||
We achieve this effect by adding two styles to a new `forms.css` file
|
||
that we add to our project as a sibling to `index.html`.
|
||
|
||
在新建的`forms.css`文件中,添加两个样式来实现这一效果。把这个文件添加到项目中,与`index.html`相邻。
|
||
|
||
+makeExample('forms/ts/forms.css',null,'forms.css')(format=".")
|
||
:marked
|
||
These styles select for the two Angular validity classes and the HTML 5 "required" attribute.
|
||
|
||
这些样式的作用于两个 Angular 有效性类和 HTML 5 的 “required” 属性。
|
||
|
||
We update the `<head>` of the `index.html` to include this style sheet.
|
||
|
||
更新`index.html`中的`<head>`来包含这个样式表。
|
||
+makeExample('forms/ts/index.html', 'styles', 'index.html (节选)')(format=".")
|
||
:marked
|
||
## Show and Hide Validation Error messages
|
||
|
||
## 显示和隐藏验证错误信息
|
||
|
||
We can do better.
|
||
|
||
我们能做的更好。
|
||
|
||
The "Name" input box is required. Clearing it turns the bar red. That says *something* is wrong but we
|
||
don't know *what* is wrong or what to do about it.
|
||
We can leverage the `ng-invalid` class to reveal a helpful message.
|
||
|
||
“Name” 输入框是必填的,清空它会让左侧的条变红。这表示*某些东西*是错的,但我们不知道错在哪里,或者如何纠正。
|
||
可以借助`ng-invalid`类来给出有用的提示。
|
||
|
||
Here's the way it should look when the user deletes the name:
|
||
|
||
当用户删除姓名时,看起来应该是这样的:
|
||
|
||
figure.image-display
|
||
img(src="/resources/images/devguide/forms/name-required-error.png" width="400px" alt="必须填写姓名")
|
||
|
||
:marked
|
||
To achieve this effect we extend the `<input>` tag with
|
||
|
||
要达到这个效果,在`<input>`标签中添加:
|
||
|
||
1. a [template reference variable](./template-syntax.html#ref-vars)
|
||
|
||
[模板引用变量](./template-syntax.html#ref-vars)
|
||
|
||
1. the "*is required*" message in a nearby `<div>` which we'll display only if the control is invalid.
|
||
|
||
“is required”消息,放在邻近的`<div>`元素中,只有当控件无效时,才显示它。
|
||
|
||
Here's how we do it for the *name* input box:
|
||
|
||
对 *Name* 输入框做如下修改:
|
||
+makeExample('forms/ts/app/hero-form.component.html',
|
||
'name-with-error-msg',
|
||
'app/hero-form.component.html (节选)')(format=".")
|
||
:marked
|
||
We need a template reference variable to access the input box's Angular control from within the template.
|
||
Here we created a variable called `name` and gave it the value "ngModel".
|
||
|
||
模板引用变量可以访问模板中输入框的 Angular 控件。
|
||
这里,创建了名叫`name`的变量,并且赋值为 "ngModel"。
|
||
|
||
.l-sub-section
|
||
:marked
|
||
Why "ngModel"?
|
||
A directive's [exportAs](../api/core/index/Directive-decorator.html) property
|
||
tells Angular how to link the reference variable to the directive.
|
||
We set `name` to `ngModel` because the `ngModel` directive's `exportAs` property happens to be "ngModel".
|
||
|
||
为什么是 “ngModel”?
|
||
指令的 [exportAs](../api/core/index/DirectiveMetadata-class.html#!#exportAs) 属性告诉 Angular 如何链接模板引用变量到指令。
|
||
这里把`name`设置为`ngModel`是因为`ngModel`指令的`exportAs`属性设置成了 “ngModel”。
|
||
|
||
Now we can control visibility of the "name" error message by binding properties of the `name` control to the message `<div>` element's `hidden` property.
|
||
|
||
现在,把`div`元素的`hidden`属性绑定到`name`控件的属性,这样就可以控制“姓名”字段错误信息的可见性了。
|
||
+makeExample('forms/ts/app/hero-form.component.html',
|
||
'hidden-error-msg',
|
||
'app/hero-form.component.html (节选)')(format='.')
|
||
:marked
|
||
In this example, we hide the message when the control is valid or pristine;
|
||
pristine means the user hasn't changed the value since it was displayed in this form.
|
||
|
||
上例中,当控件是有效的 (valid) 或全新的 (pristine) 时,隐藏消息。
|
||
“全新的”意味着从它被显示在表单中开始,用户还从未修改过它的值。
|
||
|
||
This user experience is the developer's choice. Some folks want to see the message at all times.
|
||
If we ignore the `pristine` state, we would hide the message only when the value is valid.
|
||
If we arrive in this component with a new (blank) hero or an invalid hero,
|
||
we'll see the error message immediately, before we've done anything.
|
||
|
||
这种用户体验取决于开发人员的选择。有些人会希望任何时候都显示这条消息。
|
||
如果忽略了`pristine`状态,就会只在值有效时隐藏此消息。
|
||
如果往这个组件中传入全新(空)的英雄,或者无效的英雄,将立刻看到错误信息 —— 虽然我们还啥都没做。
|
||
|
||
Some folks find that behavior disconcerting. They only want to see the message when the user makes an invalid change.
|
||
Hiding the message while the control is "pristine" achieves that goal.
|
||
We'll see the significance of this choice when we [add a new hero](#new-hero) to the form.
|
||
|
||
有些人会为这种行为感到不安。它们希望只有在用户做出无效的更改时才显示这个消息。
|
||
如果当控件是“全新”状态时也隐藏消息,就能达到这个目的。
|
||
在往表单中[添加新英雄](#new-hero)时,将看到这种选择的重要性。
|
||
|
||
The hero *Alter Ego* is optional so we can leave that be.
|
||
|
||
英雄的*第二人格*是可选项,所以不用改它。
|
||
|
||
Hero *Power* selection is required.
|
||
We can add the same kind of error handling to the `<select>` if we want
|
||
but it's not imperative because the selection box already constrains the
|
||
power to valid value.
|
||
|
||
英雄的*超能力*选项是必填的。
|
||
只要愿意,可以往`<select>`上添加相同的错误处理。
|
||
但没有必要,这个选择框已经限制了“超能力”只能选有效值。
|
||
|
||
<a id="new-hero"></a>
|
||
<a id="reset"></a>
|
||
.l-main-section
|
||
:marked
|
||
## Add a hero and reset the form
|
||
|
||
## 添加英雄及重置表单
|
||
|
||
We'd like to add a new hero in this form.
|
||
We place a "New Hero" button at the bottom of the form and bind its click event to a `newHero` component method.
|
||
|
||
我们希望在这个表单中添加新的英雄。
|
||
在表单的底部放置“New Hero(新增英雄)”按钮,并把它的点击事件绑定到`newHero`组件。
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.html',
|
||
'new-hero-button-no-reset',
|
||
'app/hero-form.component.html (新增英雄按钮)')
|
||
:marked
|
||
+makeExample('forms/ts/app/hero-form.component.ts',
|
||
'new-hero',
|
||
'app/hero-form.component.ts (新英雄方法)')(format=".")
|
||
:marked
|
||
Run the application again, click the *New Hero* button, and the form clears.
|
||
The *required* bars to the left of the input box are red, indicating invalid `name` and `power` properties.
|
||
That's understandable as these are required fields.
|
||
The error messages are hidden because the form is pristine; we haven't changed anything yet.
|
||
|
||
再次运行应用,点击 *New Hero* 按钮,表单被清空了。
|
||
输入框左侧的*必填项*竖条是红色的,表示`name`和`power`属性是无效的。
|
||
这可以理解,因为有一些必填字段。
|
||
错误信息是隐藏的,因为表单还是全新的,还没有修改任何东西。
|
||
|
||
Enter a name and click *New Hero* again.
|
||
The app displays a **_Name is required_** error message!
|
||
We don't want error messages when we create a new (empty) hero.
|
||
Why are we getting one now?
|
||
|
||
输入名字,再次点击 *New Hero* 按钮。
|
||
这次,出现了错误信息!为什么?我们不希望显示新(空)的英雄时,出现错误信息。
|
||
|
||
Inspecting the element in the browser tools reveals that the *name* input box is _no longer pristine_.
|
||
The form remembers that we entered a name before clicking *New Hero*.
|
||
Replacing the hero *did not restore the pristine state* of the form controls.
|
||
|
||
使用浏览器工具审查这个元素就会发现,这个 *name* 输入框并不是全新的。
|
||
表单记得我们在点击 *New Hero* 前输入的名字。
|
||
更换了英雄*并不会重置控件的“全新”状态*。
|
||
|
||
We have to clear all of the flags imperatively which we can do
|
||
by calling the form's `reset()` method after calling the `newHero()` method.
|
||
|
||
我们必须清除所有标记,在调用`newHero()`方法后调用表单的`reset()`方法即可。
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.html',
|
||
'new-hero-button-form-reset',
|
||
'app/hero-form.component.html (Reset the form)')
|
||
|
||
:marked
|
||
Now clicking "New Hero" both resets the form and its control flags.
|
||
|
||
现在点击“New Hero”重设表单和它的控制标记。
|
||
:marked
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Submit the form with **_ngSubmit_**
|
||
|
||
## 使用 **ngSubmit** 提交表单
|
||
|
||
The user should be able to submit this form after filling it in.
|
||
The Submit button at the bottom of the form
|
||
does nothing on its own but it will
|
||
trigger a form submit because of its type (`type="submit"`).
|
||
|
||
在填表完成之后,用户还应该能提交这个表单。
|
||
“Submit(提交)”按钮位于表单的底部,它自己不做任何事,但因为有特殊的 type 值 (`type="submit"`),所以会触发表单提交。
|
||
|
||
A "form submit" is useless at the moment.
|
||
To make it useful, we'll update the `<form>` tag with another Angular directive, `NgSubmit`,
|
||
and bind it to the `HeroFormComponent.submit()` method with an event binding
|
||
|
||
仅仅触发“表单提交”在目前是没用的。
|
||
要让它有用,还要用另外的 Angular 指令更新`<form>`标签 —— `NgSubmit`,
|
||
并且通过事件绑定把它绑定到`HeroFormComponent.submit()`方法。
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'ngSubmit')(format=".")
|
||
|
||
:marked
|
||
We slipped in something extra there at the end! We defined a
|
||
template reference variable, **`#heroForm`**, and initialized it with the value, "ngForm".
|
||
|
||
上面代码的最后出现一些额外的东西!定义了模板引用变量**`#heroForm`**,并初始化为 "ngForm"。
|
||
|
||
The variable `heroForm` is now a reference to the `NgForm` directive that governs the form as a whole.
|
||
|
||
现在`heroForm`变量引用的是`NgForm`指令,它代表的是表单的整体。
|
||
<a id="ngForm"></a>
|
||
.l-sub-section
|
||
:marked
|
||
### The _NgForm_ directive
|
||
|
||
### NgForm指令
|
||
What `NgForm` directive? We didn't add an [NgForm](../api/forms/index/NgForm-directive.html) directive!
|
||
|
||
什么`NgForm`指令?之前没有添加过 [NgForm](../api/common/index/NgForm-directive.html) 指令啊!
|
||
|
||
Angular did. Angular creates and attaches an `NgForm` directive to the `<form>` tag automatically.
|
||
|
||
是 Angular 干的。Angular 自动创建了`NgForm`指令,并把它附加到`<form>`标签。
|
||
|
||
The `NgForm` directive supplements the `form` element with additional features.
|
||
It holds the controls we created for the elements with `ngModel` directive and `name` attribute
|
||
and monitors their properties including their validity.
|
||
It also has its own `valid` property which is true only *if every contained
|
||
control* is valid.
|
||
|
||
`NgForm`指令为`form`元素扩充了额外的特性。
|
||
它持有通过`ngModel`指令和`name`属性为各个元素创建的那些控件,并且监视它们的属性变化,包括有效性。
|
||
它还有自己的`valid`属性,只有当*其中所有控件*都有效时,它才有效。
|
||
|
||
:marked
|
||
Later in the template we bind the button's `disabled` property to the form's over-all validity via
|
||
the `heroForm` variable. Here's that bit of markup:
|
||
|
||
模板中稍后的部分,通过`heroForm`变量把按钮的`disabled`属性绑定到表单的整体有效性。这里是那点 HTML:
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'submit-button')
|
||
:marked
|
||
Re-run the application. The form opens in a valid state and the button is enabled.
|
||
|
||
重新运行应用。表单打开时,状态是有效的,按钮是可用的。
|
||
|
||
Now delete the *Name*. We violate the "name required" rule which
|
||
is duly noted in our error message as before. And now the Submit button is also disabled.
|
||
|
||
现在,删除*姓名*。我们违反了“必填姓名”规则,它还是像以前那样显示出错误信息。同时,Submit 按钮也被禁用了。
|
||
|
||
Not impressed? Think about it for a moment. What would we have to do to
|
||
wire the button's enable/disabled state to the form's validity without Angular's help?
|
||
|
||
没感动吗?再想一会儿。如果没有 Angular `NgForm`的帮助,又该怎么让按钮的禁用/启用状态和表单的有效性关联起来呢?
|
||
|
||
For us, it was as simple as
|
||
|
||
有了 Angular,它就是这么简单:
|
||
|
||
1. Define a template reference variable on the (enhanced) form element
|
||
|
||
定义模板引用变量,放在(强化过的)form 元素上
|
||
|
||
2. Reference that variable in a button some 50 lines away.
|
||
|
||
从50行之外的按钮上引用这个变量。
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Toggle two form regions (extra credit)
|
||
|
||
## 切换两个表单区域(额外的奖励)
|
||
|
||
Submitting the form isn't terribly dramatic at the moment.
|
||
|
||
提交表单还是不够激动人心。
|
||
|
||
.l-sub-section
|
||
:marked
|
||
An unsurprising observation for a demo. To be honest,
|
||
jazzing it up won't teach us anything new about forms.
|
||
But this is an opportunity to exercise some of our newly won
|
||
binding skills.
|
||
If you're not interested, you can skip to the guide's conclusion
|
||
and not miss a thing.
|
||
|
||
对演示来说,这个收场很平淡的。老实说,即使让它更出彩,也无法教给我们任何关于表单的新知识。
|
||
但这是练习新学到的绑定技能的好机会。
|
||
如果你不感兴趣,可以跳到本章的总结部分,不用担心错失任何东西。
|
||
|
||
:marked
|
||
Let's do something more strikingly visual.
|
||
Let's hide the data entry area and display something else.
|
||
|
||
来实现一些更炫的视觉效果吧。
|
||
隐藏掉数据输入框,显示一些其它东西。
|
||
|
||
Start by wrapping the form in a `<div>` and bind
|
||
its `hidden` property to the `HeroFormComponent.submitted` property.
|
||
|
||
先把表单包裹进`<div>`中,再把它的`hidden`属性绑定到`HeroFormComponent.submitted`属性。
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'edit-div', 'app/hero-form.component.html (节选)')(format=".")
|
||
|
||
:marked
|
||
The main form is visible from the start because the
|
||
the `submitted` property is false until we submit the form,
|
||
as this fragment from the `HeroFormComponent` reminds us:
|
||
|
||
主表单从一开始就是可见的,因为`submitted`属性是 false,直到提交了这个表单。
|
||
来自`HeroFormComponent`的代码片段告诉了我们这一点:
|
||
|
||
+makeExample('forms/ts/app/hero-form.component.ts', 'submitted')(format=".")
|
||
|
||
:marked
|
||
When we click the Submit button, the `submitted` flag becomes true and the form disappears
|
||
as planned.
|
||
|
||
当点击 Submit 按钮时,`submitted`标志会变成 true,并且表单像预想中一样消失了。
|
||
|
||
Now we need to show something else while the form is in the submitted state.
|
||
Add the following block of HTML below the `<div>` wrapper we just wrote:
|
||
|
||
现在,当表单处于已提交状态时,需要显示一些别的东西。
|
||
在刚刚写的`<div>`包装下方,添加下列 HTML 块:
|
||
+makeExample('forms/ts/app/hero-form.component.html', 'submitted', 'app/hero-form.component.html (节选)')
|
||
|
||
:marked
|
||
There's our hero again, displayed read-only with interpolation bindings.
|
||
This slug of HTML only appears while the component is in the submitted state.
|
||
|
||
英雄又出现了,它通过插值表达式绑定显示为只读内容。
|
||
这一小段 HTML 只在组件处于已提交状态时才会显示。
|
||
|
||
We added an Edit button whose click event is bound to an expression
|
||
that clears the `submitted` flag.
|
||
|
||
添加了 “Edit(编辑)”按钮,将 click 事件绑定到表达式,用于清除`submitted`标志。
|
||
|
||
When we click it, this block disappears and the editable form reappears.
|
||
|
||
当点它时,这个只读块消失了,可编辑的表单重新出现了。
|
||
|
||
That's as much drama as we can muster for now.
|
||
|
||
够炫吗?好吧,我们已经尽力了!
|
||
|
||
.l-main-section
|
||
:marked
|
||
## Conclusion
|
||
|
||
## 结论
|
||
|
||
The Angular form techniques discussed in this guide take
|
||
advantage of the following framework features to provide support for data modification, validation and more:
|
||
|
||
本章讨论的 Angular 表单技术利用了下列框架特性来支持数据修改、验证和更多操作:
|
||
|
||
- An Angular HTML form template.
|
||
|
||
Angular HTML 表单模板。
|
||
|
||
- A form component class with a `Component` decorator.
|
||
|
||
带有`Component`装饰器的表单组件类。
|
||
|
||
- The `ngSubmit` directive for handling the form submission.
|
||
|
||
用来处理表单提交的`ngSubmit`指令。
|
||
|
||
- Template reference variables such as `#heroForm`, `#name` and `#power`.
|
||
|
||
模板引用变量,例如`#heroForm`、`#name`和`#power`。
|
||
|
||
- The `[(ngModel)]` syntax and a `name` attribute for two-way data binding, validation and change tracking.
|
||
|
||
用于双向数据绑定、数据验证和变化追踪的`[(ngModel)]`语法和`name`属性。
|
||
|
||
- The reference variable’s `valid` property on input controls to check if a control is valid and show/hide error messages.
|
||
|
||
指向 input 控件的引用变量上的`valid`属性,可用于检查控件是否有效、是否显示/隐藏错误信息。
|
||
|
||
- Controlling the submit button's enabled state by binding to `NgForm` validity.
|
||
|
||
通过绑定到`NgForm`的有效性状态,控制提交按钮的禁用状态。
|
||
|
||
- Custom CSS classes that provide visual feedback to users about invalid controls.
|
||
|
||
定制 CSS 类来给用户提供无效控件的视觉反馈。
|
||
|
||
Our final project folder structure should look like this:
|
||
|
||
最终的项目目录结构看起来是这样:
|
||
|
||
.filetree
|
||
.file angular-forms
|
||
.children
|
||
.file app
|
||
.children
|
||
.file app.component.ts
|
||
.file app.module.ts
|
||
.file hero.ts
|
||
.file hero-form.component.html
|
||
.file hero-form.component.ts
|
||
.file main.ts
|
||
.file node_modules ...
|
||
.file index.html
|
||
.file package.json
|
||
.file tsconfig.json
|
||
:marked
|
||
Here’s the final version of the source:
|
||
|
||
这里是源码的最终版本:
|
||
|
||
+makeTabs(
|
||
`forms/ts/app/hero-form.component.ts,
|
||
forms/ts/app/hero-form.component.html,
|
||
forms/ts/app/hero.ts,
|
||
forms/ts/app/app.module.ts,
|
||
forms/ts/app/app.component.ts,
|
||
forms/ts/app/main.ts,
|
||
forms/ts/index.html,
|
||
forms/ts/forms.css`,
|
||
'final, final,,,,,',
|
||
`hero-form.component.ts,
|
||
hero-form.component.html,
|
||
hero.ts,
|
||
app.module.ts,
|
||
app.component.ts,
|
||
main.ts,
|
||
index.html,
|
||
forms.css`)
|
||
:marked
|