表单

版本:Angular 5.0.0-alpha

表单是商业应用的支柱,我们用它来执行登录、求助、下单、预订机票、安排会议,以及不计其数的其它数据录入任务。

在开发表单时,创建数据方面的体验是非常重要的,它能指引用户明细、高效的完成工作流程。

开发表单需要设计能力(那超出了本章的范围),而框架支持双向数据绑定、变更检测、验证和错误处理,在本章你将会学习它们。

本章展示了如何从草稿构建一个简单的表单。在这个过程中你将学会如何:

  • 用组件和模板构建 Angular 表单。
  • ngModel创建双向数据绑定,以读取和写入输入控件的值。
  • 跟踪状态的变化,并验证表单控件。
  • 使用特殊的 CSS 类来跟踪控件的状态并给出视觉反馈。
  • 向用户显示验证错误提示,以及启用/禁用表单控件。
  • 使用模板引用变量在 HTML 元素之间共享信息。

你可以运行在线示例(查看源代码)。

模板驱动表单

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

你还可以使用响应式(也叫模型驱动)的方式来构建表单。不过本章中只介绍模板驱动表单。

利用 Angular 模板,可以构建几乎所有表单——登录表单、联系人表单, 以及任何非常漂亮的商务表单。可以创造性的摆放各种控件、把它们绑定到数据、指定校验规则、显示校验错误、有条件的禁用或启用特定的控件、触发内置的视觉反馈等等,不胜枚举。

Angular 通过处理大量重复的、模板化的任务,简化了过程,从而使你不必陷入与自己的斗争中。

你将学习构建如下的“模板驱动”表单:

表单_第1张图片

英雄职业介绍所,使用这个表单来维护英雄们的个人信息。每个英雄都需要一份工作。公司的使命就是让合适的英雄去应对合适的危机。

表单中的三个字段,其中两个是必填的。根据 material design 指南,必填的字段用星号(*)标出。

如果删除了英雄的名字,表单就会用醒目的样式把验证错误显示出来。

表单_第2张图片

注意,提交按钮被禁用了,而且输入控件从绿色变为了红色。

你将一小步一小步地构建此表单:

  1. 创建Hero模型类。
  2. 创建控制此表单的组件。
  3. 创建具有初始表单布局的模板。
  4. 使用 ngModel 双向数据绑定语法把数据属性绑定到每个表单输入控件。
  5. 为每个表单输入控件添加 ngControl 指令。
  6. 添加自定义 CSS 来提供视觉反馈。
  7. 显示和隐藏有效性验证的错误信息。
  8. 使用 ngSubmit 处理表单提交。
  9. 禁用此表单的提交按钮,直到表单变为有效。

配置

根据配置的说明创建一个名为forms的新项目。

添加 angular_forms

Angular 表单的功能在 angular_forms 库中,它有自己的包,添加包到 pub 依赖中:

// {quickstart → forms}/pubspec.yaml

dependencies:
    angular: ^5.0.0-alpha       
+  angular_forms: ^2.0.0-alpha

创建模型

当用户输入表单数据时,需要捕获它们的变化,并更新到模型的实例中。除非知道模型的样子,否则无法设计表单的布局。

最简单的模型是个“属性包”,用来存放关于应用重点的资料。这里使用了描述Hero类的三个必备字段 (idnamepower),和一个可选字段 (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 _powers = [
  'Really Smart',
  'Super Flexible',
  'Super Hot',
  'Weather Changer'
];

@Component(
  selector: 'hero-form',
  templateUrl: 'hero_form_component.html',
  directives: [coreDirectives, formDirectives],
)
class HeroFormComponent {
  Hero model = new Hero(18, 'Dr IQ', _powers[0], 'Chuck Overstreet');
  bool submitted = false;

  List get powers => _powers;

  void onSubmit() => submitted = true;
}

这个组件没有什么特别的地方,没有表单相关的东西,与之前写过的组件没什么不同。

只需要前面章节中学过的 Angular 概念,就可以完全理解这个组件:

  • 这段代码导入了 Angular 核心库以及你刚刚创建的Hero模型。
  • @Component选择器hero-form表示可以用元素把这个表单放进父模板。
  • templateUrl属性指向一个独立的 HTML 模板文件(稍后创建)。
  • modelpowers定义模拟数据。

接下来,你可以注入一个数据服务,以获取或保存真实的数据,或者把这些属性暴露为输入属性和输出属性(参见模板语法中的输入和输出属性)来绑定到一个父组件。这不是现在需要关心的问题,未来的更改不会影响到这个表单。

修改 app component

AppComponent 是应用的根组件,HeroFormComponent 将被放在其中。

使用下面的内容替换初始版本:

// lib/app_component.dart

import 'package:angular/angular.dart';

import 'src/hero_form_component.dart';

@Component(
  selector: 'my-app',
  template: '',
  directives: [HeroFormComponent],
)
class AppComponent {}

创建初始 HTML 表单模板

使用下面的内容创建模板文件:

// lib/src/hero_form_component.html (start)

Hero Form

* Required

这是一段简单的 HTML5 代码。我们展现了Hero的两个字段,namealterEgo,提供给用户在输入框中输入。

Name控件具有 HTML5 的required属性;Alter Ego控件没有,因为alterEgo是可选的。

在底部添加了一个具有一些 CSS 类的提交按钮。

你还没有用到 Angular。没有绑定,没有额外的指令,只有布局。

在模板驱动表单中,你只要导入了angular_forms库,就不用对

做任何其它的事情来使用库的功能。接下来你会看到它的原理。

刷新浏览器。你会看到一个简单的,没有样式的表单。

给表单添加样式

containerbtn类都来自 Bootstrap。Bootstrap 也有特定的表单类,包括form-controlform-group。它们给表单添加了一点样式。

Angular 不需要使用 Bootstrap 类或任意外部库的样式。Angular 应用可以使用任意 CSS 库或一点也不用。

index.html插入下面的链接来添加 Bootstrap 样式。

// web/index.html (bootstrap)


刷新浏览器。你会看到一个带有样式的表单。

使用 *ngFor 添加 powers

英雄必须从认证过的固定列表中选择一项超能力。你在内部维护这个列表(在HeroFormComponent)。

在表单中添加select,用ngForpowers列表绑定到列表选项,在之前的显示数据一章中使用过的技术。

在紧跟着 Alter Ego 组的下方添加如下 HTML:

// lib/src/hero_form_component.html (powers)

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

使用 ngModel 双向数据绑定

现在运行此应用,有点令人失望。

表单_第3张图片

你看不到英雄数据因为还没有绑定到Hero。在前面的章节我们知道怎么去做。显示数据介绍了属性绑定。用户输入展示了如何通过事件绑定来监听 DOM 事件,以及如何用显示的值更新组件的属性。

现在,需要同时进行显示、监听和提取。

虽然可以在表单中再次使用这些已知的技术。但是,你将使用新的[(ngModel)]语法,使表单绑定到模型的工作变得更容易。

找到Name对应的标签,并且像这样更新它:

// lib/src/hero_form_component.html (name)


{{model.name}}

在 form-group 标签前添加用于诊断的插值表达式,以看清正在发生什么事。给自己留个注释,提醒你完成后移除它。

聚焦到绑定语法:[(ngModel)]="..."上。

现在运行应用,开始在Name 输入框中键入,添加和删除字符,我们将看到它们从诊断文本中显示和消失。某一瞬间,它看起来可能是这样:

表单_第4张图片

诊断信息可以证明,数据确实从输入框流动到模型,再反向流动回来。

这就是双向数据绑定!。更多信息,参见模板语法章节的使用 NgModel 双向绑定

注意,标签还添加了ngControl指令,并设置为 "name",表示英雄的名字。使用任何唯一的值都可以,但使用具有描述性的“name”会更有帮助。当在表单组合中使用[(ngModel)]时,必须要定义ngControl指令。

在内部,Angular 创建了一些NgFormControl,并把它们注册到NgForm指令,再将该指令附加到标签。每个NgFormControl都都以你分配给NgFormControl指令的名称注册。稍后会看到更多 NgForm 的信息。

Alter EgoHero Power 添加类似的[(ngModel)]绑定和ngControl指令。

使用model替换诊断绑定表达式。这样就能确认双向数据绑定在整个 Hero 模型上都能正常工作了。

修改之后,这个表单的核心是这样的:

// lib/src/hero_form_component.html (controls)


{{model}}
  • 每个 input 元素都有id属性,label元素的for属性用它来匹配到对应的输入控件。
  • 每个 input 元素都有ngControl指令,这是 Angular 表单注册表单控件所必须的。

如果现在运行本应用,修改每个 Hero 模型的属性,表单可能显示如下:

表单_第5张图片

表单顶部的诊断信息证实了你所做的一切更改都反映在了 model 中。

从模板删除诊断绑定因为它已经完成了它的使命。

基于控件状态提供视觉反馈

使用 CSS 和类绑定,可以改变表单控件的外观来反映它的状态。

追踪控件状态

一个 Angular 表单控件可以告诉你,用户是否碰过此控件,值是否发生改变,以及值是否无效。

Angular 表单的每个控件(NgControl)追踪自身的状态,并通过检查下面的成员字段使状态可用:

  • dirtypristine表明控件的值是否发生改变。
  • toucheduntouched表明控件是否被访问。
  • valid反映了控件值的有效性。

控件样式

valid控件属性是最引人注意的,因为当控件的值无效时,你希望发出强烈的视觉信号。要创建这样的视觉反馈,你需要使用 Bootstrap custom-forms 的类is-validis-invalid

Name标签添加一个名为name的模板引用变量。使用name和类绑定有条件的指定恰当的表单有效性的类。

Name标签临时添加另一个名为spy的模板引用变量,用来显示输入框的 CSS 类。

// lib/src/hero_form_component.html (name)



{{spy.className}}
模板引用变量

spy模板引用变量绑定到了DOM元素,然而name变量(#name="ngForm")绑定到了与 input 元素相关联的 NgModel。

为什么是 “ngForm”?指令的 exportAs 属性告诉 Angular 如何链接模板引用变量到指令。把name设置为 “ngForm” 是因为 ngModel 指令的exportAs属性是 “ngForm”。

刷新浏览器,遵循下面的步骤:

  1. Name 输入框。
    • 它有一个绿色的边框。
    • 它有form-controlis-valid类。
  2. 添加一些字符来改变 name。类名依然不变。
  3. 删除 name。
    • 输入边框变红。
    • is-invalid类变成了is-valid

删除#spy模板引用变量和使用到它的诊断信息。

另一种类绑定的方法,可以使用 NgClass 指令给控件添加样式。首先添加下面的方法来设置控件的状态依赖 CSS 类名:

// lib/src/hero_form_component.dart (setCssValidityClass)

Map setCssValidityClass(NgControl control) {
  final validityClass = control.valid == true ? 'is-valid' : 'is-invalid';
  return {validityClass: true};
}

使用上面方法返回的 map 值,绑定到 NgClass 指令——更多关于这个指令及其替代品的信息请看模板语法章节。

// lib/src/hero_form_component.html (power)


显示和隐藏验证错误信息

你可以改进表单。Name 输入框是必填的,清空它会使输入框变红。这表明有些东西错了,但用户不知道错在哪里,或者如何纠正。利用控件状态来显示有用的信息。

使用 valid 和 pristine 状态

当用户删除姓名时,表单看起来应该是这样的:

表单_第6张图片

要达到这个效果,在紧跟着 Name 标签后面添加下面的

// lib/src/hero_form_component.html (hidden error message)

Name is required

刷新浏览器,删除输入框中的Name。错误信息就显示出来了。

基于name控件的状态,通过设置div的 hidden 属性,显式地控制错误信息。

在这个例子中,当控件是 valid 或 pristine 时,隐藏消息。 “pristine” 意味着从它被显示在表单中开始,用户还从未修改过它的值。

用户体验取决于开发人员的选择

有些开发人员会希望任何时候都显示这条消息。如果忽略了 pristine 状态,就会只在值有效时隐藏此消息。如果往这个组件中传入全新(空)的英雄,或者无效的英雄,将立刻看到错误信息 —— 虽然你还什么都没做。

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

Alter Ego 是可选项,所以不用改它。

英雄的超能力选项是必填的。只要愿意,可以往