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:


Run the .


Template-driven forms


Many of us will build forms by writing templates in the Angular template syntax with the form-specific directives and techniques described in this guide.

通常,使用 Angular 模板语法编写模板,结合本章所描述的表单专用指令和技术来构建表单。

That's not the only way to create a form but it's the way we'll cover in this guide.


We can build almost any form we need with an Angular template — login forms, contact forms, pretty much any business form. 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, boilerplate tasks we'd otherwise wrestle with ourselves.

它用起来很简单,这是因为 Angular 处理了大多数重复、单调的任务,这让我们可以不必亲自操刀、身陷其中。

We'll discuss and learn to build a template-driven form that looks like this:


Clean Form

Here at the Hero Employment Agency we use this form to maintain personal information about heroes. 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:



Note that the submit button is disabled, and the "required" bar to the left of the input control changed from green to red.


We'll customize the colors and location of the "required" bar with standard CSS.

稍后,会使用标准 CSS 来定制“必填”条的颜色和位置。

We'll build this form in small steps:


  1. Create the Hero model class.


  2. Create the component that controls the form.


  3. Create a template with the initial form layout.


  4. Bind data properties to each form control using the ngModel two-way data binding syntax.


  5. Add a name attribute to each form input control.

    往每个表单输入控件上添加name属性 (attribute)。

  6. Add custom CSS to provide visual feedback.

    添加自定义 CSS 来提供视觉反馈。

  7. Show and hide validation error messages.


  8. Handle form submission with ngSubmit.

    使用 ngSubmit 处理表单提交。

  9. Disable the form’s submit button until the form is valid.




Follow the setup instructions for creating a new project named angular-forms.


Create the Hero model class

创建 Hero 模型类

As users enter form data, we'll capture their changes and update an instance of a model. We can't lay out 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).

最简单的模型是个“属性包”,用来存放应用中一件事物的事实。 这里使用三个必备字段 (idnamepower),和一个可选字段 (alterEgo,译注:中文含义是第二人格,例如 X 战警中的 Jean / 黑凤凰)。

In the app directory, create the following file with the given content:



export class Hero { constructor( public id: number, public name: string, public power: string, public alterEgo?: string ) { } }

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, so the constructor lets us omit it; note the (?) in alterEgo?.

alterEgo是可选的,调用构造函数时可省略,注意alterEgo?中的问号 (?)。

We can create a new hero like this:


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"

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 the following file with the given content:


src/app/hero-form.component.ts (v1)

import { Component } from '@angular/core'; import { Hero } from './hero'; @Component({ moduleId: module.id, selector: 'hero-form', templateUrl: './hero-form.component.html' }) export class HeroFormComponent { powers = ['Really Smart', 'Super Flexible', 'Super Hot', 'Weather Changer']; model = new Hero(18, 'Dr IQ', this.powers[0], 'Chuck Overstreet'); submitted = false; onSubmit() { this.submitted = true; } // TODO: Remove this when we're done get diagnostic() { return JSON.stringify(this.model); } }

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 covered in previous guides.


  1. The code imports the Angular core library, and the Hero model we just created.


  2. The @Component selector value of "hero-form" means we can drop this form in a parent template with a <hero-form> tag.


  3. The moduleId: module.id property sets the base for module-relative loading of the templateUrl.

    moduleId: module.id属性设置了基地址,用于从相对模块路径加载templateUrl

  4. The templateUrl property points to a separate file for the template HTML.

    templateUrl属性指向一个独立的 HTML 模板文件。

  5. 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 for binding to a parent component. None of this concerns us now and these future changes won't affect our form.

    modelpowers定义了供演示用的假数据。 将来,可以注入服务来获取和保存真实数据, 或者暴露这些属性为输入与输出属性,绑定到父组件上。 先不关心这些,因为这些未来的变化不会影响到表单。

  6. We threw in a diagnostic property 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.tsapp.component.ts,让它们使用新的HeroFormComponent

Revise 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.


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.


Replace the contents of the "QuickStart" version with the following:



import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { HeroFormComponent } from './hero-form.component'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent, HeroFormComponent ], bootstrap: [ AppComponent ] }) export class AppModule { }

There are three changes:


  1. We import FormsModule and our new HeroFormComponent.


  2. 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.


  3. We add the HeroFormComponent to the list of declarations defined in the ngModule decorator. This makes the HeroFormComponent component visible throughout this module.


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数组中。

Revise app.component.ts

修改 app.component.ts

AppComponent is the application's root component. It will host our new HeroFormComponent.


Replace the contents of the "QuickStart" version with the following:



import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: '<hero-form></hero-form>' }) export class AppComponent { }

There are only two changes. 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. We've also dropped the name field from the class body.

这里只做了两处修改。 template中只剩下这个新的元素标签,即组件的selector属性。这样当应用组件被加载时,就会显示这个英雄表单。 另外,我们还从类中移除了name字段。

Create an initial HTML form template

创建初始 HTML 表单模板

Create the new template file with the following contents:



<div class="container"> <h1>Hero Form</h1> <form> <div class="form-group"> <label for="name">Name</label> <input type="text" class="form-control" id="name" required> </div> <div class="form-group"> <label for="alterEgo">Alter Ego</label> <input type="text" class="form-control" id="alterEgo"> </div> <button type="submit" class="btn btn-success">Submit</button> </form> </div>

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字段,namealterEgo,供用户输入。

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.


The container, form-group, form-control, and btn classes come from Twitter Bootstrap. Purely cosmetic. We're using Bootstrap to give the form a little style!

containerform-groupform-controlbtn类来自 Twitter Bootstrap。纯粹是装饰。 我们使用 Bootstrap 来美化表单。嘿,一点样式都没有的表单算个啥!

Angular forms do not require a style library
Angular 表单不需要任何样式库

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 不需要containerform-groupform-controlbtn类, 或者外部库的任何样式。Angular 应用可以使用任何 CSS 库…… ,或者啥都不用。

Let's add the stylesheet. Open index.html and add the following link to the <head>:


src/index.html (bootstrap)

<link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css">

Add powers with *ngFor

ngFor 添加超能力

Our hero must 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 guide.

在表单中添加select,用ngForpowers列表绑定到列表选项。 我们在之前的显示数据一章中见过ngFor

Add the following HTML immediately below the Alter Ego group:

Alter Ego 的紧下方添加如下 HTML:

src/app/hero-form.component.html (powers)

<div class="form-group"> <label for="power">Hero Power</label> <select class="form-control" id="power" required> <option *ngFor="let pow of powers" [value]="pow">{{pow}}</option> </select> </div>

This code repeats the <option> 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.

列表中的每一项超能力都会渲染成<option>标签。 模板输入变量p在每个迭代指向不同的超能力,使用双花括号插值表达式语法来显示它的名称。

Two-way data binding with ngModel

使用 ngModel 进行双向数据绑定

Running the app right now would be disappointing.



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 taught us property binding. User Input showed us how to listen for DOM events with an event binding and how to update a component property with the displayed value.

因为还没有绑定到某个英雄,所以看不到任何数据。 解决方案见前面的章节。 显示数据介绍了属性绑定。 用户输入介绍了如何通过事件绑定来监听 DOM 事件,以及如何用显示值更新组件的属性。

Now we need to display, listen, and extract at the same time.


We could use the techniques we already know, but instead we'll introduce something new: the [(ngModel)] syntax, which makes binding the form to the model super easy.

虽然可以在表单中再次使用这些技术。 但是,这里将介绍个新东西,[(ngModel)]语法,使表单绑定到模型的工作变得超级简单。

Find the <input> tag for Name and update it like this:

找到 Name 对应的<input>标签,并且像这样修改它:

src/app/hero-form.component.html (excerpt)

<input type="text" class="form-control" id="name" required [(ngModel)]="model.name" name="name"> TODO: remove this: {{model.name}}

We added 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 标签后添加用于诊断的插值表达式,以看清正在发生什么事。 给自己留个备注,提醒我们完成后移除它。

Focus on the binding syntax: [(ngModel)]="...".


If we run 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.

如果现在运行这个应用,开始在姓名输入框中键入,添加和删除字符,将看到它们从插值结果中显示和消失。 某一瞬间,它看起来可能是这样:


The diagnostic is evidence that values really are flowing from the input box to the model and back again.

That's two-way data binding! For more information about [(ngModel)] and two-way data bindings, see the Template Syntax page.


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属性。

Internally Angular creates FormControl instances 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.

在内部,Angular 创建了一些FormControl,并把它们注册到NgForm指令,再将该指令附加到<form>标签。 注册每个FormControl时,使用name属性值作为键值。本章后面会讨论NgForm

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 look like this:


src/app/hero-form.component.html (excerpt)

{{diagnostic}} <div class="form-group"> <label for="name">Name</label> <input type="text" class="form-control" id="name" required [(ngModel)]="model.name" name="name"> </div> <div class="form-group"> <label for="alterEgo">Alter Ego</label> <input type="text" class="form-control" id="alterEgo" [(ngModel)]="model.alterEgo" name="alterEgo"> </div> <div class="form-group"> <label for="power">Hero Power</label> <select class="form-control" id="power" required [(ngModel)]="model.power" name="power"> <option *ngFor="let pow of powers" [value]="pow">{{pow}}</option> </select> </div>

If we run the app now and changed every hero model property, the form might display like this:

如果现在运行本应用,修改 Hero 模型的每个属性,表单看起来像这样:

ngModel in action

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.


Track control 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 in our form.


Using ngModel in a form gives us more than just a two way data binding. It also tells us if the user touched the control, if the value changed, or if the value became invalid.


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.

NgModel 指令不仅仅跟踪状态。它还使用特定的 Angular CSS 类来更新控件,以反映当前状态。 可以利用这些 CSS 类来修改控件的外观,显示或隐藏消息。



Class if true

为真时的 CSS 类

Class if false

为假时的 CSS 类

Control has been visited




Control's value has changed




Control's value is valid




Let's temporarily add a template reference variable named spy to the Name <input> tag and use it to display the input's CSS classes.

往姓名<input>标签上添加名叫 spy 的临时模板引用变量, 然后用这个 spy 来显示它上面的所有 CSS 类。

src/app/hero-form.component.html (excerpt)

<input type="text" class="form-control" id="name" required [(ngModel)]="model.name" name="name" #spy> <br>TODO: remove this: {{spy.className}}

Now run the app, and look at the Name input box. Follow the next four steps precisely:

现在,运行本应用,并让姓名输入框获得焦点。 然后严格按照下面四个步骤来做:

  1. Look but don't touch.


  2. Click inside the name box, then click outside it.


  3. Add slashes to the end of the name.


  4. Erase the name.


The actions and effects are as follows:



We should see the following transitions and class names:


Control state transitions

The ng-valid/ng-invalid pair is the most interesting to us, because we want to send a strong visual signal when the values are invalid. We also want to mark required fields. To create such visual feedback, let's add definitions for the ng-* CSS classes.

(ng-valid | ng-invalid)这一对是我们最感兴趣的。当数据变得无效时,我们希望发出强力的视觉信号, 还想要标记出必填字段。可以通过加入自定义 CSS 来提供视觉反馈。

Delete the #spy template reference variable and the TODO as they have served their purpose.


Add custom CSS for visual feedback

添加用于视觉反馈的自定义 CSS

We can mark required fields and invalid data at the same time with a colored bar on the left of the input box:



We achieve this effect by adding these class definitions to a new forms.css file that we add to our project as a sibling to index.html:



.ng-valid[required], .ng-valid.required { border-left: 5px solid #42A948; /* green */ } .ng-invalid:not(form) { border-left: 5px solid #a94442; /* red */ }

Update the <head> of index.html to include this style sheet:


src/index.html (styles)

<link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="forms.css">

Show and hide validation error messages


We can do better. The Name input box is requiredand 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 control's state to reveal a helpful message.

我们能做的更好。“Name” 输入框是必填的,清空它会让左侧的条变红。这表示某些东西是错的,但我们不知道错在哪里,或者如何纠正。 可以借助ng-invalid类来给出有用的提示。

Here's the way it should look when the user deletes the name:



To achieve this effect we extend the <input> tag with


  1. a template reference variable


  2. the "is required" message in a nearby <div> which we'll display only if the control is invalid.

    “is required”消息,放在邻近的<div>元素中,只有当控件无效时,才显示它。

Here's an example of adding an error message to the name input box:


src/app/hero-form.component.html (excerpt)

<label for="name">Name</label> <input type="text" class="form-control" id="name" required [(ngModel)]="model.name" name="name" #name="ngModel"> <div [hidden]="name.valid || name.pristine" class="alert alert-danger"> Name is required </div>

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"。

Why "ngModel"? A directive's exportAs 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 属性告诉 Angular 如何链接模板引用变量到指令。 这里把name设置为ngModel是因为ngModel指令的exportAs属性设置成了 “ngModel”。

We 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.valid || name.pristine" class="alert alert-danger">

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 to the form.

有些人会为这种行为感到不安。它们希望只有在用户做出无效的更改时才显示这个消息。 如果当控件是“全新”状态时也隐藏消息,就能达到这个目的。 在往表单中添加新英雄时,将看到这种选择的重要性。

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 values.

英雄的超能力选项是必填的。 只要愿意,可以往<select>上添加相同的错误处理。 但没有必要,这个选择框已经限制了“超能力”只能选有效值。

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组件。

src/app/hero-form.component.html (New Hero button)

<button type="button" class="btn btn-default" (click)="newHero()">New Hero</button>

src/app/hero-form.component.ts (New Hero method)

newHero() { this.model = new Hero(42, '', ''); }

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 按钮,表单被清空了。 输入框左侧的必填项竖条是红色的,表示namepower属性是无效的。 这可以理解,因为有一些必填字段。 错误信息是隐藏的,因为表单还是全新的,还没有修改任何东西。

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.


src/app/hero-form.component.html (Reset the form)

<button type="button" class="btn btn-default" (click)="newHero(); heroForm.reset()">New Hero</button>

Now clicking "New Hero" both resets the form and its control flags.

现在点击“New Hero”重设表单和它的控制标记。

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, bind the form's ngSubmit event property to the hero form component's onSubmit() method:

现在这样仅仅触发“表单提交”是没用的。 要让它有用,就要把该表单的ngSubmit事件属性绑定到英雄表单组件的onSubmit()方法上:

<form (ngSubmit)="onSubmit()" #heroForm="ngForm">

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.


The NgForm directive


What NgForm directive? We didn't add an NgForm directive!

什么NgForm指令?之前没有添加过 NgForm 指令啊!

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 an 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属性,只有当其中所有控件都有效时,它才有效。

We'll bind the form's overall validity via the heroForm variable to the button's disabled property using an event binding. Here's the code:


<button type="submit" class="btn btn-success" [disabled]="!heroForm.form.valid">Submit</button>

If we run the application now, we find that the button is enabled — although it doesn't do anything useful yet.


Now if we delete the Name, we violate the "required" rule, which is duly noted in the error message. 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. Refer to that variable in a button many lines away.


Toggle two form regions (extra credit)


Submitting the form isn't terribly dramatic at the moment.


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 aren't interested, go ahead and skip to this guide's conclusion.

对演示来说,这个收场很平淡的。老实说,即使让它更出彩,也无法教给我们任何关于表单的新知识。 但这是练习新学到的绑定技能的好机会。 如果你不感兴趣,可以跳到本章的总结部分。

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.


src/app/hero-form.component.html (excerpt)

<div [hidden]="submitted"> <h1>Hero Form</h1> <form (ngSubmit)="onSubmit()" #heroForm="ngForm"> <!-- ... all of the form ... --> </form> </div>

The main form is visible from the start because the submitted property is false until we submit the form, as this fragment from the HeroFormComponent shows:

主表单从一开始就是可见的,因为submitted属性是 false,直到提交了这个表单。 来自HeroFormComponent的代码片段告诉了我们这一点:

src/app/hero-form.component.ts (submitted)

submitted = false; onSubmit() { this.submitted = true; }

When we click the Submit button, the submitted flag becomes true and the form disappears as planned.

当点击 Submit 按钮时,submitted标志会变成 true,并且表单像预想中一样消失了。

Now the app needs to show something else while the form is in the submitted state. Add the following HTML below the <div> wrapper we just wrote:

现在,当表单处于已提交状态时,需要显示一些别的东西。 在刚刚写的<div>包装下方,添加下列 HTML 语句:

src/app/hero-form.component.html (excerpt)

<div [hidden]="!submitted"> <h2>You submitted the following:</h2> <div class="row"> <div class="col-xs-3">Name</div> <div class="col-xs-9 pull-left">{{ model.name }}</div> </div> <div class="row"> <div class="col-xs-3">Alter Ego</div> <div class="col-xs-9 pull-left">{{ model.alterEgo }}</div> </div> <div class="row"> <div class="col-xs-3">Power</div> <div class="col-xs-9 pull-left">{{ model.power }}</div> </div> <br> <button class="btn btn-primary" (click)="submitted=false">Edit</button> </div>

There's our hero again, displayed read-only with interpolation bindings. This <div> appears only while the component is in the submitted state.

英雄又出现了,它通过插值表达式绑定显示为只读内容。 这一小段 HTML 只在组件处于已提交状态时才会显示。

The HTML includes an Edit button whose click event is bound to an expression that clears the submitted flag.

这段HTML包含一个 “Edit(编辑)”按钮,将 click 事件绑定到表达式,用于清除submitted标志。

When we click the Edit button, this block disappears and the editable form reappears.


That's as much drama as we can muster for now.




The Angular form discussed in this guide takes advantage of the following framework features to provide support for data modification, validation, and more:

本章讨论的 Angular 表单技术利用了下列框架特性来支持数据修改、验证和更多操作:

Our final project folder structure should look like this:


node_modules ...

Here’s the code for the final version of the application:


import { Component } from '@angular/core'; import { Hero } from './hero'; @Component({ moduleId: module.id, selector: 'hero-form', templateUrl: './hero-form.component.html' }) export class HeroFormComponent { powers = ['Really Smart', 'Super Flexible', 'Super Hot', 'Weather Changer']; model = new Hero(18, 'Dr IQ', this.powers[0], 'Chuck Overstreet'); submitted = false; onSubmit() { this.submitted = true; } newHero() { this.model = new Hero(42, '', ''); } } <div class="container"> <div [hidden]="submitted"> <h1>Hero Form</h1> <form (ngSubmit)="onSubmit()" #heroForm="ngForm"> <div class="form-group"> <label for="name">Name</label> <input type="text" class="form-control" id="name" required [(ngModel)]="model.name" name="name" #name="ngModel"> <div [hidden]="name.valid || name.pristine" class="alert alert-danger"> Name is required </div> </div> <div class="form-group"> <label for="alterEgo">Alter Ego</label> <input type="text" class="form-control" id="alterEgo" [(ngModel)]="model.alterEgo" name="alterEgo"> </div> <div class="form-group"> <label for="power">Hero Power</label> <select class="form-control" id="power" required [(ngModel)]="model.power" name="power" #power="ngModel"> <option *ngFor="let pow of powers" [value]="pow">{{pow}}</option> </select> <div [hidden]="power.valid || power.pristine" class="alert alert-danger"> Power is required </div> </div> <button type="submit" class="btn btn-success" [disabled]="!heroForm.form.valid">Submit</button> <button type="button" class="btn btn-default" (click)="newHero(); heroForm.reset()">New Hero</button> </form> </div> <div [hidden]="!submitted"> <h2>You submitted the following:</h2> <div class="row"> <div class="col-xs-3">Name</div> <div class="col-xs-9 pull-left">{{ model.name }}</div> </div> <div class="row"> <div class="col-xs-3">Alter Ego</div> <div class="col-xs-9 pull-left">{{ model.alterEgo }}</div> </div> <div class="row"> <div class="col-xs-3">Power</div> <div class="col-xs-9 pull-left">{{ model.power }}</div> </div> <br> <button class="btn btn-primary" (click)="submitted=false">Edit</button> </div> </div> export class Hero { constructor( public id: number, public name: string, public power: string, public alterEgo?: string ) { } } import { NgModule } from '@angular/core'; import { BrowserModule } from '@angular/platform-browser'; import { FormsModule } from '@angular/forms'; import { AppComponent } from './app.component'; import { HeroFormComponent } from './hero-form.component'; @NgModule({ imports: [ BrowserModule, FormsModule ], declarations: [ AppComponent, HeroFormComponent ], bootstrap: [ AppComponent ] }) export class AppModule { } import { Component } from '@angular/core'; @Component({ selector: 'my-app', template: '<hero-form></hero-form>' }) export class AppComponent { } import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { AppModule } from './app/app.module'; platformBrowserDynamic().bootstrapModule(AppModule); <html> <head> <title>Hero Form</title> <base href="/"> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://unpkg.com/bootstrap@3.3.7/dist/css/bootstrap.min.css"> <link rel="stylesheet" href="styles.css"> <link rel="stylesheet" href="forms.css"> <!-- Polyfills --> <script src="node_modules/core-js/client/shim.min.js"></script> <script src="node_modules/zone.js/dist/zone.js"></script> <script src="node_modules/systemjs/dist/system.src.js"></script> <script src="systemjs.config.js"></script> <script> System.import('main.js').catch(function(err){ console.error(err); }); </script> </head> <body> <my-app>Loading...</my-app> </body> </html> .ng-valid[required], .ng-valid.required { border-left: 5px solid #42A948; /* green */ } .ng-invalid:not(form) { border-left: 5px solid #a94442; /* red */ }