开始
本次我们将会学会如何用Angular创建表单、two-way data binding(双向数据绑定)、change tracking(检测变化)、validation(验证) 和 error handling(错误处理)等功能以及ngModel、ngControl和ngForm等指令的应用。
这次内容比较长,在最后会给出一个完成的工程来实践本篇的内容。在这个过程中你可以配合演示来实时地观察效果。
参考官方文档
工程结构
/
|-package.json
|-index.html
|-node_modules/
|-libraries
|-...
|-HeroForm/
|-index.html
|-app/
|-main.js
|-app.component.js
|-hero.js
|-hero-form.component.js
|-template/
|-hero-form.component.template
|-css/
|-styles.css
正文
概览
表单创造了了一种有cohesive(粘合力)、effective(有效)并且compelling(引人入胜)的数据输入体验(来自官方文档,我不能很好的翻译和解释这句话)。一个Angular表单用来协调进行了数据绑定的用户控件、track(检测)改变、验证输入和显示错误。
我们都使用过表单进行过登录、提交帮助请求、下订单、订飞机、设定会议日程和其他无数的数据输入任务。表单支撑起了商业应用。
任何经验丰富web开发者都可以用正确的标签来拼凑HTML表单。更具挑战的是通过表单背后的数据流来创建有粘合力的数据输入体验以帮助用户的行为变得有效且高效。
在表单这部分,Angular处理了很多重复、模式化的任务,因此我们不必浪费很多精力。
我们将要创建一个表单,用于输入一个hero的相关信息,它有name、power(能力)、alter ego(密友)这三个属性,其中name、power是必填的,alter ego是可选的,power是一个列表以选择其中一项。当用户没有填写name时,会给出验证错误,提示name是必填项。当没有正确填写时,不可点击提交按钮。点击提交后会显示出刚刚提交的信息,此时点击编辑会重新填写表单。
创建hero的Model
创建表单,首先需要一个数据模型提供基础。在用户输入表单数据时我们会捕捉到改变并且更新到Model的一个实例。
这个Model只包含了hero的三个属性。
创建/HeroForm/app/hero.js,内容为:
(function(app) {
app.Hero = Hero;
function Hero(id, name, power, alterEgo) {
this.id = id;
this.name = name;
this.power = power;
this.alterEgo = alterEgo;
}
})(window.app || (window.app = {}));
这段代码非常简单,不在赘述。
表单组件
一个Angular表单包含两部分:基于HTML的模板和基于代码的组件来处理数据和用户交互(Model对Angular表单来说不是必须的)。
接下来创建一个组件来协调View和Model。
创建/HeroForm/app/hero-form.component.js,内容为:
(function(app) {
app.HeroFormComponent = ng.core.Component({
selector: 'hero-form',
templateUrl: '/HeroForm/app/template/hero-form.component.template'
}).Class({
constructor: function() {
this.powers = ['Really Smart', 'Super Flexible',
'Super Hot', 'Weather Changer'
];
this.model = new app.Hero(18, 'Dr IQ', this.powers[0],'Chuck Overstreet');
this.submitted = false;
},
onSubmit: function() {
this.submitted = true;
},
diagnostic: function() {
return JSON.stringify(this.model);
}
})
})(window.app || (window.app={}));
可以看到定义了一个HeroFormComponent组件到window.app
,这个组件选择了模板中的<hero-form>
标签来进行实例化,模板通过URL定位。在构造函数中初始化了power数组,定义了hero的各种能力供用户选择。这在之前的文章都已经提到过了。在构造函数中还实例化了一个Hero作为model成员,这个成员会和表单进行交互。submitted表示表单是否被提交过了,控制表单的显示。diagnostic用于调试,让我们直观地看到model的变化。
创建一个HTML模板初始化表单
创建/HeroForm/app/template/hero-form.component.template,内容为:
<div class="container">
<div [hidden]="submitted">
<h1>Hero Form</h1>
<form (ngSubmit)="onSubmit()" #heroForm="ngForm">
{{diagnostic()}}
<div class="form-group">
<label for="name">Name</label>
<input type="text" class="form-control" required [(ngModel)]="model.name" ngControl="name" #name="ngForm" >
<div [hidden]="name.valid" 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" [(ngModel)]="model.alterEgo" ngControl="alterEgo" >
</div>
<div class="form-group">
<label for="power">Hero Power</label>
<select class="form-control" required [(ngModel)]="model.power" ngControl="power" #power="ngForm" >
<option *ngFor="#p of powers" [value]="p">{{p}}</option>
</select>
<div [hidden]="power.valid" class="alert alert-danger">
Power is required
</div>
</div>
<button type="submit" class="btn btn-default" [disabled]="!heroForm.form.valid">Submit</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-default" (click)="submitted=false">Edit</button>
</div>
</div>
基础内容
Name的<input>
标签和Power的<select>
标签包含了HTML5的required属性以表示它们是必须填写的。
class中的大部分样式(除了ng-valid与ng-invalid)均来自Twitter Bootstrap,这些都是为了美化。Angular不需要任何Class或Style的外部库,你可以使用任何库或根本不用。
先看看Power的选单,这里用到了之间文章中说到的,通过ngFor指令遍历数组,对每个元素产生<option>
。#p
迭代了powers中的而每一个元素,在{{p}}
中显示出来。
最后的<button class="btn btn-default" (click)="submitted=false">Edit</button>
会在按钮被点击时执行submitted=false
,重启编辑状态。
双向数据绑定
ngModel指令使我们简单的将表单绑定到Model。
看看Name的<input>
标签,你会看到[(ngModel)]="model.name"
的用法。[()]
表示这里使用了双向绑定。在绑定中,数据从Model流向View使用[]
,数据从View流向Model用()
。因此双向的数据绑定使用[()]
,也就是说你在表单上的更改会更新到Model,修改Model也会同步显示在表单中。
你也可以将它改为:
<input type="text" class="form-control" required [ngModel]="model.name" (ngModelChange)="model.name = $event" ngControl="name" #name="ngForm" >
ngModelChange不是<input>
元素的事件,它时间上是ngModel指令的一个event property(事件属性)。当Angular看到一个绑定标记如[(x)]
,它认为指令x有一个叫做x的输入指令和一个叫做xChange的输出指令。
对于model.name = $event
。ngModelChange不会产生DOM事件。这是一个Angular EventEmitter属性,当它被fire(激活)时返回了输入值,这正是Model的name属性所需要的。
ngForm
<select class="form-control" required [(ngModel)]="model.power" #power="ngForm" >
Angular将ngForm暗中添加进了<form>
中。这里设置了一个模板本地变量power并使用ngForm为他赋值。我个人认为这里使用的ngForm指代了当前它所在的控件。
ngForm指令为<form>
添加了一些附加的特性。它收集表单中的被标记ngControl(后面会提到)标记了的元素并监测他们的属性(包括是否可用)。它也有自己的valid属性,当每一个包含的control为true时它会为true。
因此<form (ngSubmit)="onSubmit()" #heroForm="ngForm">
和<button type="submit" class="btn btn-default" [disabled]="!heroForm.form.valid">Submit</button>
配合,模板本地变量heroFrom代表了整个表单,通过整个表单的valid值判断是否可以提交。
ngControl
表单不仅仅是数据绑定,我们同样需要知道表单上的控件的状态。ngControl指令持续检测控件的状态。ngControl和ngForm类似,只能用于<form>
中的元素。我们的应用可以通过ngControl知道用户是否触摸了控件,值是否改变了以及内容是否值变得无效了等。
<input type="text" class="form-control" required [(ngModel)]="model.name" ngControl="name" #name="ngForm" >
我们用值ngForm初始化模板本地变量name,Angular会将name设置为ngControl指令的实例。或者说,对于这个<input>
,name handle(我喜欢翻译为“作为..的句柄”)了这个ngControl对象。
ngControl不仅仅检测状态,它使用特殊的Angular CSS classes来更新控件。我们可以通过改变class的值来改变控件的外观并且使消息显示或者消失。
解释 | 为true时的Class | 为false时的Class |
控件被访问了 | ng-touched | ng-untouched |
控件值被改变了 | ng-dirty | ng-pristine |
控件值是可用的 | ng-valid | ng-invalid |
表单中控件的class内容会随着控件内容的变化而改变。本工程会用到ng-valid和ng-invalid,因为我们需要强烈的视觉信息来告诉用户哪些值是必填的(非空值是ng-valid,空值是ng-invalid),后面我们会写这部分CSS。
通过ngSubmit提交表单
用户在完成填写后需要提交表单。Submit按钮本身不会做任何事情,但它会触发一个表单的提交(type="submit"
)。我们需要为<form>
添加ngSubmit指令,并且通过一个event绑定将它绑定到HeroFormComponent.submit()
方法上。
其他
HTML最后一部分的<div [hidden]="!submitted">
将submitted做用户hidden
,控制元素的可见性。
CSS
添加CSS以支持上文中说的ng-valid和ng-invalid样式。
添加/HeroForm/app/css/styles.css,内容为:
.ng-valid[required] {
border-left: 5px solid #42A948; /* green */
}
.ng-invalid {
border-left: 5px solid #a94442; /* red */
}
关于CSS的内容不再赘述。
其他部分
工程还有一些文件作为必要的模块如引导等,我将它们集中起来描述。
根组件
创建/HeroForm/app/app.component.js,内容为:
(function(app) {
app.AppComponent = ng.core.Component({
selector: 'my-app',
template: '<hero-form></hero-form>',
directives: [app.HeroFormComponent]
}).Class({
constructor: function() {}
});
})(window.app || (window.app = {}));
这段代码很熟悉了,唯一不同的是将app.HeroFormComponent
作为指令使用,这个根组件使用了HeroFormComponent,它会被正确使用。
引导
创建/HeroForm/app/main.js,内容为:
(function(app){
document.addEventListener('DOMContentLoaded',function(){
ng.platform.browser.bootstrap(app.AppComponent);
});
})(window.app || (window.app = {}));
通过根组件来引导应用。
index.html
创建/HeroForm/index.html,内容为:
<html>
<head>
<title>HeroForm</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<!-- 1. Load libraries -->
<!-- IE required polyfill -->
<script src="/node_modules/es6-shim/es6-shim.min.js"></script>
<script src="/node_modules/angular2/bundles/angular2-polyfills.js"></script>
<script src="/node_modules/rxjs/bundles/Rx.umd.js"></script>
<script src="/node_modules/angular2/bundles/angular2-all.umd.js"></script>
<link rel="stylesheet" href="/HeroForm/app/css/styles.css">
<!--引用了www.bootcss.com中的bootstrap样式,这样做可以减少读者在实践过程中出错的可能性,因为在第一篇笔记的基础结构中没有引入bootstrap。-->
<link href="http://cdn.bootcss.com/bootstrap/3.3.4/css/bootstrap.min.css" rel="stylesheet">
<!-- 2. Load our 'modules' -->
<script src='/HeroForm/app/hero.js'></script>
<script src='/HeroForm/app/hero-form.component.js'></script>
<script src='/HeroForm/app/app.component.js'></script>
<script src='/HeroForm/app/main.js'></script>
</head>
<!-- 3. Display the application -->
<body>
<my-app>Loading...</my-app>
</body>
</html>
正像注释中所说,我在第一篇笔记中没有引入bootstrap,为了减少大家出错的可能性,我直接引用了www.bootcss.com
的bootstrap,在此表示感谢。
运行
在/下执行npm start
。