表单是商业应用程序的主流。您可使用表单登陆,提交帮助请求,下订单,预订航班,安排会议,并执行无数其余数据录入任务。css
在开发表单时,建立一个数据录入体验很是重要,该体验能够经过工做流高效地引导用户。html
开发表单须要设计技巧(超出本页面的范围),以及双向数据绑定,更改跟踪,验证和错误处理的框架支持,您将在本页面上了解这些信息。java
本页面向您展现了如何从头构建一个简单的表单。一路上你将学习如何:git
您能够在Plunker中运行实例(查看源代码)并从那里下载代码。github
您能够经过使用本页中描述的特定于表单的指令和技术在Angular模板语法中编写模板来构建表单。web
您也可使用响应式(或模型驱动)方法来构建表单。 可是,此页面重点介绍模板驱动的表单。编程
您可使用Angular模板 构建几乎任何表单- 登陆表单,联系表单和几乎任何业务表单。 您能够创造性地设计控件,将它们绑定到数据,指定验证规则和显示验证错误,有条件地启用或禁用特定控件,触发内置的视觉反馈等等。bootstrap
Angular经过许多重复的,模板化的任务使处理过程变得简单。api
您将学习如何构建一个模板驱动的表单,以下所示:浏览器
英雄就业机构使用这种形式来维护关于英雄的我的信息。 每一个英雄都须要一份工做。 让正确的英雄与正确的危机相匹配是公司的使命。
这个表格中的三个字段中的两个是必需的。 遵循材料设计准则,必填字段带有星号(*)。
若是您删除了英雄名称,表单将以吸引人注意的风格显示验证错误:
请注意提交按钮被禁用,而且输入控件从绿色变为红色。
您将以小步骤构建此表单:
按照设置说明建立一个名为表单的新项目。
Angular表单功能位于angular_forms库中,该库位于其本身的包中。 将该包添加到pubspec依赖项:
当用户输入表单数据时,您将捕获其更改并更新模型的实例。 直到你知道模型是什么样子,你才能布置表格。
一个模型能够像“钱包”同样简单,掌握关于应用程序重要事实的事实。 这很好地描述了英雄类与三个必填字段(id, name, power)和一个可选字段(alterEgo)。
在lib目录中,使用给定的内容建立如下文件:lib/src/hero.dart
class Hero { int id; String name, power, alterEgo; Hero(this.id, this.name, this.power, [this.alterEgo]); String toString() => '$id: $name ($alterEgo). Super power: $power'; }
这是一个缺少要求,没有行为的鸡肋模型,对于演示来讲足够了。
alterEgo是可选的,因此构造函数可让你忽略它。 请注意[this.alterEgo]中的括号。
你能够像这样建立一个新的英雄:
var myHero = new Hero( 42, 'SkyDog', 'Fetch any object at any distance', 'Leslie Rollover'); print('My hero is ${myHero.name}.'); // "My hero is SkyDog."
一个Angular表单有两个部分:一个基于HTML的模板和一个组件类,以编程方式处理数据和用户交互。 从课程开始,由于它简要地说明了英雄编辑能够作什么。
使用给定的内容建立如下文件:lib/src/hero_form_component.dart (v1)
import 'package:angular/angular.dart'; import 'package:angular_forms/angular_forms.dart'; import 'hero.dart'; const List<String> _powers = const [ 'Really Smart', 'Super Flexible', 'Super Hot', 'Weather Changer' ]; @Component( selector: 'hero-form', templateUrl: 'hero_form_component.html', directives: const [CORE_DIRECTIVES, formDirectives], ) class HeroFormComponent { Hero model = new Hero(18, 'Dr IQ', _powers[0], 'Chuck Overstreet'); bool submitted = false; List<String> get powers => _powers; void onSubmit() => submitted = true; }
这个组件没有什么特别之处,没有任何特定的形式,没有什么区别它与你以前写的任何组件。
理解这个组件只须要前面几页中介绍的Angular概念。
顺便说一句,您能够注入数据服务来获取和保存真实数据,或者将这些属性做为输入和输出(请参阅“模板语法”页面中的输入和输出属性)来绑定到父组件。 这不是如今的问题,这些将来的变化不会影响表单。
AppComponent是应用程序的根组件。 它将承载HeroFormComponent。
将初学者应用版本的内容替换为如下内容:lib/app_component.dart
import 'package:angular/angular.dart'; import 'src/hero_form_component.dart'; @Component( selector: 'my-app', template: '<hero-form></hero-form>', directives: const [HeroFormComponent], ) class AppComponent {}
使用如下内容建立模板文件:lib/src/hero_form_component.html (start)
<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> <div class="row"> <div class="col-auto"> <button type="submit" class="btn btn-primary">Submit</button> </div> <small class="col text-right">* Required</small> </div> </form> </div>
该语言只是HTML5。 您将展现两个Hero字段,name和alterEgo,并在输入框中将其打开以供用户输入。
Name <input>控件具备HTML5必需属性; Alter Ego <input>控件什么也不作,由于alterEgo是可选的。
您在底部添加了一个提交按钮,其中有一些类用于样式。
你尚未使用Angular。 没有绑定或额外的指令,只是布局。
在模板驱动的表单中,若是已经导入了angular_forms库,则没必要为了使用库功能而对<form>标记执行任何操做。 继续看看这是如何工做的。
刷新浏览器。 你会看到一个简单的,没有样式的表单。
通常的CSS类container和btn来自Bootstrap。 Bootstrap还具备form-specific的类,包括form-control和form-group。 一块儿,这些给表单了一些样式。
Angular可不使用Bootstrap类或任何外部库的样式。 Angular的应用程序可使用任何CSS库或不使用。
经过将如下连接插入到index.html的<head>中来添加Bootstrap样式:web/index.html (bootstrap)
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
刷新浏览器。 你会看到一个样式化的表单!
英雄必须从一个固定的机构批准的权力列表中选择一个超级大国。 您在内部维护该列表(在HeroFormComponent中)。
您将在表单中添加一个select,并使用ngFor(先前在“显示数据”页面中看到的一种技术)将选项绑定到powers列表。
在Alter Ego group下方添加如下HTML:lib/src/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 p of powers" [value]="p">{{p}}</option> </select> </div>
这段代码重复列表中每一个power 的<option>标签。 p模板输入变量在每次迭代中是不一样的power; 您使用插值语法显示其名称。
如今运行应用程序有点使人失望。
你没有看到英雄数据,由于你尚未绑定到英雄。 你知道如何从早期的页面作到这一点。 显示数据教导属性绑定。 用户输入显示如何使用事件绑定监听DOM事件以及如何使用显示的值更新组件属性。
如今您须要同时显示,聆听和提取。
你可使用你已经知道的技术,可是你会使用新的[(ngModel)]语法,这使得绑定到模型的表单变得容易。
找到Name的<input>标签,并像下面这样更新它:lib/src/hero_form_component.html (name)
<!-- TODO: remove the next diagnostic line --> <mark>{{model.name}}</mark><hr> <div class="form-group"> <label for="name">Name *</label> <input type="text" class="form-control" id="name" required [(ngModel)]="model.name" ngControl="name"> </div>
你在form-group以前添加了一个诊断插值,因此你能够看到你在作什么。 当你完成的时候,你留下一张纸条扔掉它。
关注绑定语法:[(ngModel)] =“...”。
如今运行应用程序并输入名称输入,添加和删除字符。 您会看到这些字符出如今诊断文本中并消失。 在某个时候,它可能看起来像这样:
诊断结果代表数值确实是从输入流向模型,再返回。
这是双向的数据绑定。 有关更多信息,请参见模板语法页面上的与NgModel的双向绑定。
请注意,您还为<input>标记添加了一个ngControl指令,并将其设置为“name”,这对于英雄的名字是有意义的。 任何惟一值将会这样作,但使用描述性名称是有帮助的。 将[(ngModel)]与表单结合使用时,定义ngControl指令是一项要求。
在内部,Angular建立NgFormControl实例,并使用Angular附加到<form>标签的NgForm指令注册它们。 每一个NgFormControl都是在您分配给ngControl指令的名称下注册的。 本指南稍后将详细介绍NgForm。
在Alter Ego和Hero Power上添加相似的[(ngModel)]绑定和ngControl指令。
用model替换诊断绑定表达式。 经过这种方式,您能够确认双向数据绑定适用于整个英雄模型。
修改后,表单的核心应该是这样的:lib/src/hero_form_component.html (controls)
<!-- TODO: remove the next diagnostic line --> <mark>{{model}}</mark><hr> <div class="form-group"> <label for="name">Name *</label> <input type="text" class="form-control" id="name" required [(ngModel)]="model.name" ngControl="name"> </div> <div class="form-group"> <label for="alterEgo">Alter Ego</label> <input type="text" class="form-control" id="alterEgo" [(ngModel)]="model.alterEgo" ngControl="alterEgo"> </div> <div class="form-group"> <label for="power">Hero Power *</label> <select class="form-control" id="power" required [(ngModel)]="model.power" ngControl="power"> <option *ngFor="let p of powers" [value]="p">{{p}}</option> </select> </div>
- 每一个input元素都有一个id属性,label元素的for属性使用它来匹配label和input控件。
- 每一个input元素都有一个ngControl指令,Angular表单须要用这个指令在表单上注册控件。
若是您如今运行应用程序并更改每一个英雄model属性,表单可能会显示以下:
靠近表单顶部的诊断确认全部的更改都反映在model中。
从模板中删除诊断绑定,由于它已经达到了目的。
使用CSS和类绑定,您能够更改表单控件的外观以反映其状态。
Angular表单控件能够告诉您用户是否触摸了该控件,值是否改变,或者该值是否失效。
每一个Angular控制(NgControl)都跟踪本身的状态,并经过如下字段成员使状态可供检查:
有效的控制属性是最有趣的,由于当一个控制值无效时,你想发送一个强烈的视觉信号。 要建立这样的视觉反馈,您将使用Bootstrap自定义表单类 is-valid和is-invalid。
将名为name的模板引用变量添加到Name <input>标记中。 使用name和类绑定来有条件地分配适当的表单有效性类。
临时将另外一个名为spy的模板引用变量添加到Name <input>标记,并使用它显示输入的CSS类。
lib/src/hero_form_component.html (name)
<input type="text" class="form-control" id="name" required [(ngModel)]="model.name" #name="ngForm" #spy [class.is-valid]="name.valid" [class.is-invalid]="!name.valid" ngControl="name"> <!-- TODO: remove the next diagnostic line --> {{spy.className}}
模板引用变量
spy模板引用变量绑定到<input> DOM元素,而name变量(经过#name =“ngForm”语法)绑定到与input元素关联的NgModel。
为何“ngForm”? 指令的exportAs属性告诉Angular如何将引用变量连接到指令。 您将name设置为“ngForm”,由于ngModel指令的exportAs属性是“ngForm”。
刷新浏览器,而后按照下列步骤操做:
1.看看名字输入。
2.经过添加一些字符来更改name。 类保持不变。
3.删除名称。
删除#spy模板引用变量和使用它的诊断。
做为类绑定的替代方法,可使用NgClass指令来设置控件的样式。 首先,添加如下方法来设置控件的依赖于状态的CSS类名称:
lib/src/hero_form_component.dart (setCssValidityClass)
Map<String, bool> setCssValidityClass(NgControl control) { final validityClass = control.valid == true ? 'is-valid' : 'is-invalid'; return {validityClass: true}; }
使用此方法返回的映射值绑定到NgClass指令 - 在模板语法页面中详细了解此指令及其替代方法。
lib/src/hero_form_component.html (power)
<select class="form-control" id="power" required [(ngModel)]="model.power" #power="ngForm" [ngClass]="setCssValidityClass(power)" ngControl="power"> <option *ngFor="let p of powers" [value]="p">{{p}}</option> </select>
你能够改善表格。 名称输入是必需的,清除它将框的轮廓变为红色。 这说明有些事情是错的,但用户不知道什么是错的,或者该怎么作。 利用控件的状态来显示有用的消息。
当用户删除名称时,表单应该以下所示:
为了达到这个效果,在Name <input>以后当即添加下面的<div>:
lib/src/hero_form_component.html (hidden error message)
<div [hidden]="name.valid || name.pristine" class="invalid-feedback"> Name is required </div>
刷新浏览器并删除Name 输入。 显示错误消息。
您能够经过根据名称控制的状态设置<div>的隐藏属性来控制错误消息的可见性。
在这个例子中,当控件是有效的或者原始的时候隐藏消息 - “pristine”意味着用户没有改变这个值,由于它是以这种形式显示的。
用户体验是开发者的选择
有些开发人员但愿消息始终显示。 若是您忽略原始状态,则只有在该值有效时才会隐藏该消息。 若是您使用新(空白)英雄或无效英雄到达此组件,则在您执行任何操做以前,您将当即看到错误消息。
有些开发人员但愿仅在用户进行无效更改时显示消息。 当控件是“原始的”时隐藏消息实现了这个目标。 当您向表单添加一个“清除”按钮时,您会看到此选项的重要性。
英雄Alter Ego是可选的,因此你能够不用关那个。
英雄power选择是必需的。 若是须要,能够将相同类型的错误消息添加到<select>中,但这不是必须的,由于选择框已经将权限限制为有效值。
将clear()方法添加到组件类中:lib/src/hero_form_component.dart (clear)
void clear() { model.name = ''; model.power = _powers[0]; model.alterEgo = ''; }
在提交按钮后面添加一个带有点击事件绑定的清除按钮:lib/src/hero_form_component.html (Clear button)
<button (click)="clear()" type="button" class="btn"> Clear </button>
刷新浏览器。 点击清除按钮。 文本字段变为空白,若是您更改了power,它将恢复为默认值。
用户应该可以在填写表单后提交这个表单。表单底部的Submit按钮自己不作任何事情,可是因为它的类型(type =“submit”),它会触发一个表单提交。
表单提交目前是无用的。 为了使它有用,将表单组件的onSubmit()方法分配给表单的ngSubmit事件绑定:
<form (ngSubmit)="onSubmit()" #heroForm="ngForm">
请注意模板引用变量#heroForm。 正如前面所解释的,变量heroForm被绑定到总体管理表单的NgForm指令。
NgForm指令
Angular自动建立并附加一个NgForm指令给<form>标签。
NgForm指令补充表单元素的附加功能。 它包含用ngModel和ngControl指令为元素建立的控件,并监视它们的属性,包括它们的有效性。
您将经过heroForm变量将表单的总体有效性绑定到按钮的disabled属性:
<button [disabled]="!heroForm.form.valid" type="submit" class="btn btn-primary"> Submit </button>
刷新浏览器。 你会发现这个按钮是启用的,尽管它没有作任何有用的事情。
如今,若是您删除Name,则违反了“必需的”规则,这在错误消息中正确记录。 提交按钮也被禁用。
没有留下深入印象? 想想。 若是没有Angular的帮助,你须要作什么才能将按钮的启用/禁用状态链接到表单的有效性?
对你来讲,这很简单:
提交表单目前没有视觉效果。
如预期的演示。 增长代码事后的demo不会教你任何关于表单的新东西。 可是这是一个锻炼一些新得到的绑定技巧的机会。 若是您不感兴趣,请跳至本页的摘要。
做为一种视觉效果,您能够隐藏数据输入区域并显示其余内容。
将表单封装在<div>中,并将其hidden属性绑定到HeroFormComponent.submitted属性。
lib/src/hero_form_component.html (excerpt)
<div [hidden]="submitted"> <h1>Hero Form</h1> <form (ngSubmit)="onSubmit()" #heroForm="ngForm"> </form> </div>
该表单从一开始就是可见的,由于在提交表单以前,提交的属性为false,由于HeroFormComponent中的片断显示为:lib/src/hero_form_component.dart (submitted)
bool submitted = false; void onSubmit() => submitted = true;
如今在刚刚写的<div>包装器下面添加下面的HTML:lib/src/hero_form_component.html (submitted)
<div [hidden]="!submitted"> <h1>Hero data</h1> <table class="table"> <tr> <th>Name</th> <td>{{model.name}}</td> </tr> <tr> <th>Alter Ego</th> <td>{{model.alterEgo}}</td> </tr> <tr> <th>Power</th> <td>{{model.power}}</td> </tr> </table> <button (click)="submitted=false" class="btn btn-primary">Edit</button> </div>
刷新浏览器并提交表单。 提交的标志变为真,表格消失。 您将看到表格中显示的英雄模型值(只读)。
该视图包含一个编辑按钮,其单击事件绑定将清除提交的标志。 当您单击编辑按钮时,该表消失,而且可编辑的表单从新出现。
Angular表单为数据修改,验证等提供支持。 在此页面中,您学习了如何使用如下功能:
最终的项目文件夹结构应该以下所示:
如下是应用程序最终版本的代码:
lib/app_component.dart
import 'package:angular/angular.dart'; import 'src/hero_form_component.dart'; @Component( selector: 'my-app', template: '<hero-form></hero-form>', directives: const [HeroFormComponent], ) class AppComponent {}
lib/src/hero.dart
class Hero { int id; String name, power, alterEgo; Hero(this.id, this.name, this.power, [this.alterEgo]); String toString() => '$id: $name ($alterEgo). Super power: $power'; }
lib/src/hero_form_component.dart
import 'package:angular/angular.dart'; import 'package:angular_forms/angular_forms.dart'; import 'hero.dart'; const List<String> _powers = const [ 'Really Smart', 'Super Flexible', 'Super Hot', 'Weather Changer' ]; @Component( selector: 'hero-form', templateUrl: 'hero_form_component.html', directives: const [CORE_DIRECTIVES, formDirectives], ) class HeroFormComponent { Hero model = new Hero(18, 'Dr IQ', _powers[0], 'Chuck Overstreet'); bool submitted = false; List<String> get powers => _powers; void onSubmit() => submitted = true; /// Returns a map of CSS class names representing the state of [control]. Map<String, bool> setCssValidityClass(NgControl control) { final validityClass = control.valid == true ? 'is-valid' : 'is-invalid'; return {validityClass: true}; } void clear() { model.name = ''; model.power = _powers[0]; model.alterEgo = ''; } }
lib/src/hero_form_component.html
<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="ngForm" [class.is-valid]="name.valid" [class.is-invalid]="!name.valid" ngControl="name"> <div [hidden]="name.valid || name.pristine" class="invalid-feedback"> 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" ngControl="alterEgo"> </div> <div class="form-group"> <label for="power">Hero Power *</label> <select class="form-control" id="power" required [(ngModel)]="model.power" #power="ngForm" [ngClass]="setCssValidityClass(power)" ngControl="power"> <option *ngFor="let p of powers" [value]="p">{{p}}</option> </select> </div> <div class="row"> <div class="col-auto"> <button [disabled]="!heroForm.form.valid" type="submit" class="btn btn-primary"> Submit </button> <button (click)="clear()" type="button" class="btn"> Clear </button> </div> <small class="col text-right">* Required</small> </div> </form> </div> <div [hidden]="!submitted"> <h1>Hero data</h1> <table class="table"> <tr> <th>Name</th> <td>{{model.name}}</td> </tr> <tr> <th>Alter Ego</th> <td>{{model.alterEgo}}</td> </tr> <tr> <th>Power</th> <td>{{model.power}}</td> </tr> </table> <button (click)="submitted=false" class="btn btn-primary">Edit</button> </div> </div>
web/index.html
<!DOCTYPE html> <html> <head> <script> // WARNING: DO NOT set the <base href> like this in production! // Details: https://webdev.dartlang.org/angular/guide/router (function () { var m = document.location.pathname.match(/^(\/[-\w]+)+\/web($|\/)/); document.write('<base href="' + (m ? m[0] : '/') + '" />'); }()); </script> <title>Hero Form</title> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous"> <link rel="stylesheet" href="styles.css"> <link rel="icon" type="image/png" href="favicon.png"> <script defer src="main.dart" type="application/dart"></script> <script defer src="packages/browser/dart.js"></script> </head> <body> <my-app>Loading ...</my-app> </body> </html>
web/main.dart
import 'package:angular/angular.dart'; import 'package:forms/app_component.dart'; void main() { bootstrap(AppComponent); }