Angular最初使用它最核心价值之一就是处理表单能力,而且这种处理根本不需要额外的代码,光依赖于HTML5本身的Input类型就足够解决绝大部分需求。

Angular框架本身已经包括双向绑定、变化跟踪、验证、错误处理等,接下来会按部就班学习以下内容:

  • 使用组件和模板构建Angular表单
  • 使用双向绑定 [(ngMmodel)] 读写表单表。
  • 使用 ngControl 跟踪表单状态和有效性。
  • 根据 ngControl 状态添加相应的CSS。
  • 显示验证错误消息并启用/禁止表单。
  • 使用本地模板变量共享控制间的信息。

一、模板驱动表单

Angular框架已经已经内置很多模板语法,只需要使用这些语法就可以构建任何您想要的表单,诸如:登录表单、联系表单等。以下学习如何模板驱动的形式构建一个添加Hero表单的示例:

hero-form-1

三个字段,其中两个必填值,如果空会有错误提醒,当有错误存在时提交按钮是不可用的。接下来在官网快速入门的基础上开始学习。

二、创建 Hero 类

在app目录下创建 hero.ts 文件,类包括三个必填字段、一个可选字段:

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

alterEgo 是可为空字段,这是TypeScript支持的一种声明方式。以下是创建一个 myHero 类实例:

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"

三、创建Form组件

一个Form组件包含两部分:一是基于HTML的模板,二是处理数据和用户交互部分。

在app目录下创建 hero-form.component.ts 文件,内容为:

import { Component } from 'angular2/core';
import { NgForm } from 'angular2/common';
import { Hero } from './hero';

@Component({
    selector: 'hero-form',
    templateUrl: 'app/hero-form.component.html'
})

export class HeroFormComponent {
    // 能力下拉列表数据
    powers = ['Really Smart', 'Super Flexible', 'Super Hot', 'Weather Changer'];

    // 当前hero实例
    model = new Hero(18, 'Dr IQ', this.powers[0], 'Chuck Overstreet');

    // 标识是否已经提交
    submitted = false;

    // 提交方法
    onSubmit() {
        this.submitted = true;
    }

    get diagnostic() {
        return JSON.stringify(this.model);
    }
}

组件类我已经有部分注释,但是如果想要看懂这个类,需要结合之前的文章。

  1. 从Angular模块库加载 Component
  2. @Componet 元数据定义组件类连接位置,这里是一个 <hero-form> 元素。
  3. templateUrl 指定模板URL路径。
  4. 组件类里定义了数据模型和能力下拉列表的虚拟数据,以及一个用于提交的方法,现在是一个空方法,不做任何处理。
  5. diagnostic 将数据模型转换成JSON,目的为了将数据模型以JSON的形式在页面展示,这样便于查看输入时的内容。

四、修改app.component.ts

app.component.ts 是应用根组件,现在加载 HeroFormComponent 组件并置入根组件当中。

import {Component} from 'angular2/core';
import { HeroFormComponent } from './hero-form.component';

@Component({
    selector: 'my-app',
    template: '<hero-form></hero-form>',
    directives: [HeroFormComponent]
})
export class AppComponent { }

修改三处:

  1. 加载 HeroFormComponent 组件。
  2. template 模板修改组件 selector 属性所指定的元素标识。
  3. directives 告诉Angular模板依赖哪些指令。

五、创建 HTML 表单模板

在app目录下创建 hero-form.component.html 模板文件,内容为:

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

这是纯HTML模板,呈现name和alterEgo两个字段。而且这里使用了bootstrap,所以我们还需要安装:

npm install --save bootstrap

然后在 index.html 加入相应样式文件。

<link rel="stylesheet" href="node_modules/bootstrap/dist/css/bootstrap.min.css">

六、使用 *ngFor 绑定下拉列表项

powers 已经在组件类定义了一些虚拟数据。使用 *ngFor 循环 <option> 输出,而 #p 本地模板变量每一次迭代会有不同值;最后使用插值法显示名称。

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

七、使用 ngModel 双向数据绑定

效果如下:

hero-form-3

我们看不到显示 hero 数据,因为还没有绑定 Hero 对象。之前显示数据用户输入两篇文章描述过显示数据、属性绑定、事件绑定等相关知识。

但是之前介绍的是结合事件的方法,这里使用一新更便利的方法,使用 [(ngModel)] 实现双向绑定。

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

只不过在原来HTML加上 [(ngModel)]="model.name",对文本框的添加或删除文本都会立即在TODO里展示出来,效果如下:

ng-model-in-action

同样道理加上其他双向绑定代码,完整的模板代码为:

<div class="container">
    <h1>Hero Form</h1>
    <form>
        {{diagnostic}}
        <div class="form-group">
            <label for="name">Name</label>
            <input type="text" class="form-control" required [(ngModel)]="model.name">
        </div>
        <div class="form-group">
            <label for="alterEgo">Alter Ego</label>
            <input type="text" class="form-control" [(ngModel)]="model.alterEgo">
        </div>
        <div class="form-group">
            <label for="power">Hero Power</label>
            <select class="form-control" required [(ngModel)]="model.power">
                <option *ngFor="#p of powers" [value]="p">{{p}}</option>
            </select>
        </div>
        <button type="submit" class="btn btn-default">Submit</button>
    </form>
</div>

运行后改变表单内容,会立即会看到调试信息。

ng-model-in-action-2

[(ngModel)] 原理

[()] 语法实际是由属性绑定和事件绑定的结合体。

属性绑定,数据流从模型到视图,用[]来表示。
事件绑定,数据流从视图到模型,用()来表示。

二者都是单向数据绑定,Angular把两者结合后变成 [()] 就是双向绑定的标识。(到这我才释怀为什么Angular2用了这么难用、纠结体双向绑定,毕竟双向绑定会用得比较多)

事实上,可以把他们拆分开来表述双向绑定,比如:

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

上面的属性绑定非常奇怪,和我们认识的不一样。ngModelChange 并不是 <input> 元素事件,事实是 NgModel 指令的事件属性。Angular希望当看到 [(x)] 时,x 指令有一个 x 输入属性和 xChange 输出事件属性。

另外一个怪异表达式 model.name = $event,前面我们看到 $event 来自DOM事件,而 ngModelChange 属性又不产生DOM事件;而是Angular EventEmitter 属性。所以需要在触发时给他重新赋值让他返回一个DOM事件。

大部分用 [(ngModel)],可如果我们想做一些录入节流动作时可能就需要用到这种分开的写法。

八、使用NgControl跟踪状态变化与有效性

表单不光只有数据绑定,还需要表单控件状态。

ngControl 指令可以告知是否触发控件、是否改变值、值是否有效。指令不光只是跟踪状态,还会使用特殊CSS更新至控件,这样我们可以利用这些CSS样式来改变元素外观。

下面给所有元素添加 ngControl 指令,比如name控件:

<input type="text" class="form-control" required
  [(ngModel)]="model.name"
  ngControl="name">

ngControl 这里指定值为 “name”,其实并不一定需要,可以是任意值或只需要一个 ngControl 即可。

八、添加自定义视觉CSS

NgControl 指令不仅跟踪状态,还会更新CSS类来反映状态。

状态 有效时 无效时
控件被访问 ng-touched ng-untouched
控件值发生过变化 ng-dirty ng-pristine
控件值有效 ng-valid ng-invalid

添加一个本地模板变量来看看CSS的变化情况:

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

接下来利用四个动作来跟踪CSS的状态:

  1. 首次加载后,不进行任何操作。
  2. 单击input文本框,然后再单击其他位置。
  3. 文本框录入斜线。
  4. 删除所有录入文本。

四个动作的动作效果图:

control-state-transitions-anim

ng-control-class-changes

接着可以利用这些CSS类的变化,来改变input的外观,比如最有意义的 ng-validng-invalid,当值无效时我们用input的左边框加上一个很显眼的红色块。

validity-required-indicator

给根目录下 style.css 加上以下样式:

.ng-valid[required] {
  border-left: 5px solid #42A948; /* green */
}

.ng-invalid {
  border-left: 5px solid #a94442; /* red */
}

九、显示和隐藏验证错误消息

上面单纯只是颜色块的警示,但是不能明确告知是什么错误,或者提供一些有意义的辅助解决信息。因此,在基础上增加一个错误消息提示:

name-required-error

为了达到这么个效果,需要:

  1. 添加一个本地模板变量。
  2. 控件无效值时才显示。
<div class="form-group">
    <label for="name">Name</label>
    <input type="text" class="form-control" required 
      [(ngModel)]="model.name" 
      ngControl #name="ngForm">
      <div [hidden]="name.valid || name.pristine" class="alert alert-danger">
        姓名是必填项
      </div>
</div>

在模板里,我们需要指定一个本地变量来访问input文本框Angular控件信息,这里是指定 ngFormname 本地变量。

为什么是 ngForm 变量呢?

  • 一个指令的 exportAs 属性,是告诉Angular要如何连接本地变量到指令。
  • NgControlName 指令的 exportAs 属性正是 ngForm。(NgForm、NgModel、NgControlName、NgControlGroup的exportAs都是ngForm,但我们只能使用其中一个指令)

所以,本地变量 #name 赋值为 ngForm,就变得合情合理。

十、添加一个Hero并重置表单

添加一个按钮,并绑定组件事件。

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

相应组件方法:

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

运行后并点击New Hero按钮,文本框会左边会显示红色;但不会显示错误提醒框(即:姓名必填项)。

输入名称后点击New Hero按钮,错误提醒框又出现了!这是为什么呢?(因为不应该出现)

点击New Hero按钮时虽然会清空数据,但通过Chrome浏览器的元素检查发现,name文本框依然是原始状态(即文本框有ng-pristine样式)。Angular是无法分辨model是被替换还是清除name属性值,只能手动重置表单控件,为组件添加一个 active 标记,初始值 true,当添加新Hero时,将 active 变为 false,同时使用 setTimeout 切换为 true

active = true;

newHero() {
    this.model = new Hero(42, '', '');
    this.active = false;
    setTimeout(() => this.active = true, 0);
}
<form *ngIf="active">

ngIf 会触发DOM的移除或添加,虽然页面会闪一下,但新添加的表单都是未修改的初始状态。

十一、使用ngSubmit提交表单

普通 HTML 我们只需要添加一个 type="submit" 这样,该按钮就可以提交表单。但是在Angular里,这并没有效果,还需要加给 <form>NgSubmit 并指定组件类的 submit() 方法。

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

以上代码还加入一个本地模板变量 #heroForm 并赋值 ngForm,这样 heroForm 就可以管理整个表单。

NgForm指令

什么是 NgForm 指令?我们并没有在任何位置上添加过。其实,Angular会自动为 <form> 添加 NgForm 指令。

此外,NgForm 指令还为 <from> 元素添加一些额外信息。包含 ngControl 属性用来监视属性及有效性,以及自身的 valid 属性来判断整个表单是否有效。

把表单有效性绑到按钮 disabled 属性:

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

十二、切换表单区域

表单提交后,把表单隐藏,同时显示提交后的信息。

首先给表单包裹一个 <div/> 并把组件类的 submitted 属性绑定到DIV元素 hidden 属性上。

<div  [hidden]="submitted">
    <h1>Hero Form</h1>
    <form *ngIf="active" (ngSubmit)="onSubmit()" #heroForm="ngForm">

       <!-- ... all of the form ... -->

    </form>
  </div>

组件类添加 submitted 属性,当提交后值变成 true,这样整个form将被隐藏。

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

现在我们把提交后的数据显示出来,在刚包裹的 <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>

上面并没有什么特殊的代码,都是一些数据绑定。

最终效果:

angular2-forms-demo

总结

本篇讨论的有关Angular2框架提供的表单跟踪数据修改、验证等等。表单绝对是Angular最核心的内容了,因为像我们目前的Angular1项目都是后台管理类的,有着大量表单。Angular1的确为我们极大简化开发成本,几乎一个人一天最少可以做三个页面以上。

其实相比Angular1需要更多的代码量,而目前我还未在实际项目中使用Angular2,具体如何更合理去组织目前还真无法给个合理的方案。