表单

版本:Angular 5.0.0-alpha

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

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

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

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

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

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

模板驱动表单

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

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

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

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

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

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

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

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

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

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

  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<String> _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<String> get powers => _powers;

  void onSubmit() => submitted = true;
}

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

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

  • 这段代码导入了 Angular 核心库以及你刚刚创建的Hero模型。
  • @Component选择器hero-form表示可以用<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: '<hero-form></hero-form>',
  directives: [HeroFormComponent],
)
class AppComponent {}

创建初始 HTML 表单模板

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

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

<div class="container">
  <h1>Hero Form</h1>
  <form>
    <div class="form-group">
      <label for="name">Name&nbsp;*</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">*&nbsp;Required</small>
    </div>
  </form>
</div>

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

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

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

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

在模板驱动表单中,你只要导入了angular_forms库,就不用对<form>做任何其它的事情来使用库的功能。接下来你会看到它的原理。

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

给表单添加样式

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

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

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

// web/index.html (bootstrap)

<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.1.1/css/bootstrap.min.css" integrity="sha384-WskhaSGFgHYWDcbwN70/dfYBj47jz9qbsMId/iRN3ewGhXQFZCSftd1LZCfmhktB" crossorigin="anonymous">

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

使用 *ngFor 添加 powers

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

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

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

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

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

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

使用 ngModel 双向数据绑定

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

你看不到英雄数据因为还没有绑定到Hero。在前面的章节我们知道怎么去做。显示数据介绍了属性绑定。用户输入展示了如何通过事件绑定来监听 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&nbsp;*</label>
  <input type="text" class="form-control" id="name" required
         [(ngModel)]="model.name"
         ngControl="name">
</div>

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

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

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

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

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

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

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

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

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

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

// 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&nbsp;*</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&nbsp;*</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属性用它来匹配到对应的输入控件。
  • 每个 input 元素都有ngControl指令,这是 Angular 表单注册表单控件所必须的。

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

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

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

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

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

追踪控件状态

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

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

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

控件样式

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

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

Name<input>标签临时添加另一个名为spy的模板引用变量,用来显示输入框的 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. Name 输入框。
    • 它有一个绿色的边框。
    • 它有form-controlis-valid类。
  2. 添加一些字符来改变 name。类名依然不变。
  3. 删除 name。
    • 输入边框变红。
    • is-invalid类变成了is-valid

删除#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};
}

使用上面方法返回的 map 值,绑定到 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 输入框是必填的,清空它会使输入框变红。这表明有些东西错了,但用户不知道错在哪里,或者如何纠正。利用控件状态来显示有用的信息。

使用 valid 和 pristine 状态

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

要达到这个效果,在紧跟着 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。错误信息就显示出来了。

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

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

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

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

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

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

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

添加清除按钮

在组件类中添加clear()方法:

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

void clear() {
  model.name = '';
  model.power = _powers[0];
  model.alterEgo = '';
}

提交 按钮的右边添加一个绑定click事件的 Clear 按钮:

// lib/src/hero_form_component.html (Clear button)

<button (click)="clear()" type="button" class="btn">
  Clear
</button>

刷新浏览器。点击 Clear 按钮。文本域都被清空,如果之前改变了Power 的值,也会重置为默认值。

使用 ngSubmit 提交表单

在填表完成之后,用户应该能提交这个表单。Submit 按钮位于表单的底部,它自己不做任何事,但因为它的 type 值 (type="submit"),所以会触发表单提交。

此时提交表单是无意义的。为了让它有意义,指定表单组件的onSubmit()方法,绑定到表单的ngSubmit事件绑定:

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

注意模板引用变量#heroForm。正如前面所说的,变量heroForm被绑定到管理整个表单的NgForm指令。

NgForm指令

Angular 自动创建并添加 NgForm 指令到<form>标签。

NgForm指令给表单元素附加了额外的特性。它会控制那些带有ngModelngControl指令的控件元素,监听他们的属性,包括其有效性。

你要把表单的总体有效性通过heroForm变量绑定到按钮的disabled属性上。

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

刷新浏览器。你会发现按钮是可用的——尽管它还没有做任何有用的事。

现在如果我们删除 Name,就违反了“required”规则,它会恰当的显示在错误信息中。提交 按钮也被禁用了。

不觉得了不起吗?再想一想。如果没有 Angular 的帮助,我们又该怎样让按钮的禁用/启用状态和表单的有效性关联起来呢?

有了 Angular,它就是这么简单:

  1. 在(增强的)form 元素上定义一个模板引用变量。
  2. 在许多行之外的按钮上引用该变量。

显示模型 (可选)

此时提交表单没有视觉特效。

改进 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>

表单从一开始就是可见的,因为submitted属性是 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>

刷新浏览器,并提交表单。submitted标记变成 true,表单消失了。你会看到英雄模型的值(只读)显示在表格中。

这个视图包含一个 Edit 按钮,它的点击事件绑定清除submitted标志。当你点击 Edit 按钮时,表格消失,可编辑的表单又出现了。

总结

Angular 表单提供了数据修改、验证等支持。在本章中学到了怎样使用下面的特性:

  • 一个 HTML 表单模板和一个带有@Component注解的表单组件类。
  • 通过一个ngSubmit事件绑定处理表单提交。
  • 模板引用变量,例如heroFormname
  • 双向数据绑定([(ngModel)])。
  • 用于验证和追踪表单元素变化的ngControl指令。
  • input 控件的valid属性(通过模板引用变量获取),用于检查控件的有效性和显示/隐藏错误信息。
  • NgForm.form的有效性来设置 提交 按钮的激活状态。
  • 定制 CSS 类来给用户提供控件状态的视觉反馈。

下一步

依赖注入

推荐阅读更多精彩内容

  • angular提供了模板驱动和模型驱动两种方式来构建表单。模板驱动模式使用模板表单内置指令、内置校验的方式来构建表...
    oWSQo阅读 302评论 0 0
  • 表单创建一个有机、有效、引人注目的数据输入体验。 Angular 表单协调一组数据绑定控件,跟踪变更,验证输入的有...
    赵然228阅读 1,027评论 0 0
  • 细说 Angular 2+ 的表单(二):响应式表单 摘要 在企业应用开发时,表单是一个躲不过去的事情,和面向消费...
    接灰的电子产品阅读 4,035评论 8 26
  • 主要内容 第一节 - 创建最简单的输入框 第二节 - 添加简单的验证功能 第三节 - 显示验证失败的错误信息 第四...
    semlinker阅读 688评论 0 3
  • 异业合作资源推荐 公司名称:磐维科技(医学皮肤管理) 拥有资源: 本公司拥有高端会员200多位.微信公众号粉丝60...
    小BD阅读 19评论 0 0