非常教程

AngularJS参考手册

表单

动态表单

有时候手动编写和维护表单所需工作量和时间会过大。特别是在需要编写大量表单时。表单都很相似,而且随着业务和监管需求的迅速变化,表单也要随之变化,这样维护的成本过高。

基于业务对象模型的元数据,动态创建表单可能会更划算。

本文会展示如何利用 formGroup 来动态渲染一个简单的表单,包括各种控件类型和验证规则。 这个起点很简陋,但可以在这个基础上添加丰富多彩的问卷问题、更优美的渲染以及更卓越的用户体验。

这个例子要为正在找工作的英雄们创建一个在线申请表的动态表单。英雄管理局会不断修改申请流程,你要在不修改应用代码的情况下,动态创建这些表单。

参见在线例子 / 下载范例。

启动/引导 (bootstrap)

从创建一个名叫 AppModuleNgModule 开始。

这个烹饪书使用响应式表单。

响应式表单属于另外一个叫做 ReactiveFormsModuleNgModule,所以,为了使用响应式表单类的指令,你得从 @angular/forms 库中引入 ReactiveFormsModule 模块。

main.ts 中启动 AppModule

app.module.ts

main.ts

content_copyimport { BrowserModule }                from '@angular/platform-browser';import { ReactiveFormsModule }          from '@angular/forms';import { NgModule }                     from '@angular/core'; import { AppComponent }                 from './app.component';import { DynamicFormComponent }         from './dynamic-form.component';import { DynamicFormQuestionComponent } from './dynamic-form-question.component'; @NgModule({  imports: [ BrowserModule, ReactiveFormsModule ],  declarations: [ AppComponent, DynamicFormComponent, DynamicFormQuestionComponent ],  bootstrap: [ AppComponent ]})export class AppModule {  constructor() {  }}

问卷问题模型

第一步是定义一个对象模型,用来描述所有表单功能需要的场景。英雄的申请流程涉及到一个包含很多问卷问题的表单。问卷问题是最基础的对象模型。

下面的 QuestionBase 是最基础的问卷问题基类。

src/app/question-base.ts

content_copyexport class QuestionBase<T> {  value: T;  key: string;  label: string;  required: boolean;  order: number;  controlType: string;   constructor(options: {      value?: T,      key?: string,      label?: string,      required?: boolean,      order?: number,      controlType?: string    } = {}) {    this.value = options.value;    this.key = options.key || '';    this.label = options.label || '';    this.required = !!options.required;    this.order = options.order === undefined ? 1 : options.order;    this.controlType = options.controlType || '';  }}

在这个基础上,你派生出两个新类 TextboxQuestionDropdownQuestion,分别代表文本框和下拉框。这么做的初衷是,表单能动态绑定到特定的问卷问题类型,并动态渲染出合适的控件。

TextboxQuestion 可以通过 type 属性来支持多种 HTML5 元素类型,比如文本、邮件、网址等。

src/app/question-textbox.ts

content_copyimport { QuestionBase } from './question-base';

export class TextboxQuestion extends QuestionBase<string> {
  controlType = 'textbox';
  type: string;

  constructor(options: {} = {}) {
    super(options);
    this.type = options['type'] || '';
  }
}

DropdownQuestion 表示一个带可选项列表的选择框。

src/app/question-dropdown.ts

content_copyimport { QuestionBase } from './question-base';

export class DropdownQuestion extends QuestionBase<string> {
  controlType = 'dropdown';
  options: {key: string, value: string}[] = [];

  constructor(options: {} = {}) {
    super(options);
    this.options = options['options'] || [];
  }
}

接下来定义了 QuestionControlService,一个可以把问卷问题转换为 FormGroup 的服务。 简而言之,这个 FormGroup 使用问卷模型的元数据,并允许你指定默认值和验证规则。

src/app/question-control.service.ts

content_copyimport { Injectable }   from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';

import { QuestionBase } from './question-base';

@Injectable()
export class QuestionControlService {
  constructor() { }

  toFormGroup(questions: QuestionBase<any>[] ) {
    let group: any = {};

    questions.forEach(question => {
      group[question.key] = question.required ? new FormControl(question.value || '', Validators.required)
                                              : new FormControl(question.value || '');
    });
    return new FormGroup(group);
  }
}

问卷表单组件

现在你已经有一个定义好的完整模型了,接着就可以开始创建一个展现动态表单的组件。

DynamicFormComponent 是表单的主要容器和入口点。

dynamic-form.component.html

dynamic-form.component.ts

content_copy<div>  <form (ngSubmit)="onSubmit()" [formGroup]="form">     <div *ngFor="let question of questions" class="form-row">      <app-question [question]="question" [form]="form"></app-question>    </div>     <div class="form-row">      <button type="submit" [disabled]="!form.valid">Save</button>    </div>  </form>   <div *ngIf="payLoad" class="form-row">    <strong>Saved the following values</strong><br>{{payLoad}}  </div></div>

它代表了问卷问题列表,每个问题都被绑定到一个 <app-question> 组件元素。 <app-question> 标签匹配到的是组件 DynamicFormQuestionComponent,该组件的职责是根据各个问卷问题对象的值来动态渲染表单控件。

dynamic-form-question.component.html

dynamic-form-question.component.ts

content_copy<div [formGroup]="form">  <label [attr.for]="question.key">{{question.label}}</label>   <div [ngSwitch]="question.controlType">     <input *ngSwitchCase="'textbox'" [formControlName]="question.key"            [id]="question.key" [type]="question.type">     <select [id]="question.key" *ngSwitchCase="'dropdown'" [formControlName]="question.key">      <option *ngFor="let opt of question.options" [value]="opt.key">{{opt.value}}</option>    </select>   </div>    <div class="errorMessage" *ngIf="!isValid">{{question.label}} is required</div></div>

请注意,这个组件能代表模型里的任何问题类型。目前,还只有两种问题类型,但可以添加更多类型。可以用 ngSwitch 决定显示哪种类型的问题。

在这两个组件中,你依赖 Angular 的 formGroup 来把模板 HTML 和底层控件对象连接起来,该对象从问卷问题模型里获取渲染和验证规则。

formControlNameformGroup 是在 ReactiveFormsModule 中定义的指令。这个模板之所以能使用它们,是因为你曾从 AppModule 中导入了 ReactiveFormsModule

问卷数据

DynamicForm 期望得到一个问题列表,该列表被绑定到 @Input() questions 属性。

QuestionService 会返回为工作申请表定义的那组问题列表。在真实的应用程序环境中,你会从数据库里获得这些问题列表。

关键是,你完全根据 QuestionService 返回的对象来控制英雄的工作申请表。 要维护这份问卷,只要非常简单地添加、修改和删除 questions 数组中的对象就可以了。

src/app/question.service.ts

content_copyimport { Injectable }       from '@angular/core'; import { DropdownQuestion } from './question-dropdown';import { QuestionBase }     from './question-base';import { TextboxQuestion }  from './question-textbox'; @Injectable()export class QuestionService {   // TODO: get from a remote source of question metadata  // TODO: make asynchronous  getQuestions() {     let questions: QuestionBase<any>[] = [       new DropdownQuestion({        key: 'brave',        label: 'Bravery Rating',        options: [          {key: 'solid',  value: 'Solid'},          {key: 'great',  value: 'Great'},          {key: 'good',   value: 'Good'},          {key: 'unproven', value: 'Unproven'}        ],        order: 3      }),       new TextboxQuestion({        key: 'firstName',        label: 'First name',        value: 'Bombasto',        required: true,        order: 1      }),       new TextboxQuestion({        key: 'emailAddress',        label: 'Email',        type: 'email',        order: 2      })    ];     return questions.sort((a, b) => a.order - b.order);  }}

最后,在 AppComponent 里显示出表单。

app.component.ts

content_copyimport { Component }       from '@angular/core'; import { QuestionService } from './question.service'; @Component({  selector: 'app-root',  template: `    <div>      <h2>Job Application for Heroes</h2>      <app-dynamic-form [questions]="questions"></app-dynamic-form>    </div>  `,  providers:  [QuestionService]})export class AppComponent {  questions: any[];   constructor(service: QuestionService) {    this.questions = service.getQuestions();  }}

动态模板

在这个例子中,虽然你是在为英雄的工作申请表建模,但是除了 QuestionService 返回的那些对象外,没有其它任何地方是与英雄有关的。

这点非常重要,因为只要与问卷对象模型兼容,就可以在任何类型的调查问卷中复用这些组件。 这里的关键是用到元数据的动态数据绑定来渲染表单,对问卷问题没有任何硬性的假设。除控件的元数据外,还可以动态添加验证规则。

表单验证通过之前,保存按钮是禁用的。验证通过后,就可以点击保存按钮,程序会把当前值渲染成 JSON 显示出来。 这表明任何用户输入都被传到了数据模型里。至于如何储存和提取数据则是另一话题了。

完整的表单是这样的:

动态表单

AngularJS

Angular 是一个开发平台。它能帮你更轻松的构建 Web 应用。Angular 集声明式模板、依赖注入、端到端工具和一些最佳实践于一身,为你解决开发方面的各种挑战。

AngularJS目录

1.快速上手 | quick start
2.语言服务
3.安全
4.环境准备与部署
5.Service Worker
6.保持最新
7.从 AngularJS 升级
8.服务端渲染
9.Visual Studio 2015 快速上手
10.风格指南
11.国际化
12.测试
13.路由与导航
14. 教程 | Tutorial
15.架构
16.组件与模板
17.表单
18.可观察对象与RxJS
19.引导启动
20.Angular 模块
21.依赖注入
22.HttpClient
23.词汇表
24.AngularJS 应用
25.AngularJS 模块
26.AngularJS 事件
27.AngularJS HTML DOM
28.AngularJS 过滤器
29.AngularJS 控制器
30.AngularJS 指令
31.AngularJS 表达式
32.AngularJS 简介
33.AngularJS 参考手册
34.AngularJS 实例
35.AngularJS 输入验证
36.AngularJS 表单
37.AngularJS SQL
38.AngularJS 表格
39.AngularJS Http
40.AngularJS 包含
41.AngularJS Bootstrap
42.AngularJS API
43.AngularJS ng-checked 指令
44.AngularJS ng-change 指令
45.AngularJS ng-blur 指令
46.AngularJS ng-bind-template 指令
47.AngularJS ng-bind-html 指令
48.AngularJS ng-bind 指令
49.AngularJS ng-app 指令
50.AngularJS Scope(作用域)
51.AngularJS ng-model 指令
52.AngularJS ng-dblclick 指令
53.AngularJS ng-cut 指令
54.AngularJS ng-csp 指令
55.AngularJS ng-copy 指令
56.AngularJS ng-controller 指令
57.AngularJS ng-cloak 指令
58.AngularJS ng-click 指令
59.AngularJS ng-class-odd 指令
60.AngularJS ng-class-even 指令
61.AngularJS ng-class 指令
62.AngularJS ng-keyup 指令
63.AngularJS ng-keypress 指令
64.AngularJS ng-keydown 指令
65.AngularJS ng-init 指令
66.AngularJS ng-include 指令
67.AngularJS ng-if 指令
68.AngularJS ng-href 指令
69.AngularJS ng-hide 指令
70.AngularJS ng-focus 指令
71.AngularJS ng-disabled 指令
72.AngularJS ng-non-bindable 指令
73.AngularJS ng-mouseup 指令
74.AngularJS ng-mouseover 指令
75.AngularJS ng-mousemove 指令
76.AngularJS ng-mouseleave 指令
77.AngularJS ng-mouseenter 指令
78.AngularJS ng-mousedown 指令
79.AngularJS ng-model-options 指令
80.AngularJS ng-model 指令
81.AngularJS ng-list 指令
82.AngularJS ng-style 指令
83.AngularJS ng-srcset 指令
84.AngularJS ng-src 指令
85.AngularJS ng-show 指令
86.AngularJS ng-selected 指令
87.AngularJS ng-repeat 指令
88.AngularJS ng-readonly 指令
89.AngularJS ng-paste 指令
90.AngularJS ng-options 指令
91.AngularJS ng-open 指令
92.AngularJS ng-value 指令
93.AngularJS ng-switch 指令
94.AngularJS ng-submit 指令
95.AngularJS 服务(Service)
96.AngularJS Select(选择框)
97.AngularJS 动画
98.AngularJS 依赖注入