填写这份《一分钟调查》,帮我们(开发组)做得更好!去填写Home

构建动态表单

Building dynamic forms

许多表单(例如问卷)可能在格式和意图上都非常相似。为了更快更轻松地生成这种表单的不同版本,你可以根据描述业务对象模型的元数据来创建动态表单模板。然后就可以根据数据模型中的变化,使用该模板自动生成新的表单。

Many forms, such as questionaires, can be very similar to one another in format and intent. To make it faster and easier to generate different versions of such a form, you can create a dynamic form template based on metadata that describes the business object model. You can then use the template to generate new forms automatically, according to changes in the data model.

如果你有这样一种表单,其内容必须经常更改以满足快速变化的业务需求和监管需求,该技术就特别有用。一个典型的例子就是问卷。你可能需要在不同的上下文中获取用户的意见。用户要看到的表单格式和样式应该保持不变,而你要提的实际问题则会因上下文而异。

The technique is particularly useful when you have a type of form whose content must change frequently to meet rapidly changing business and regulatory requirements. A typical use case is a questionaire. You might need to get input from users in different contexts. The format and style of the forms a user sees should remain constant, while the actual questions you need to ask vary with the context.

在本教程中,你将构建一个渲染基本问卷的动态表单。你要为正在找工作的英雄们建立一个在线应用。英雄管理局会不断修补应用流程,但是借助动态表单,你可以动态创建新的表单,而无需修改应用代码。

In this tutorial you will build a dynamic form that presents a basic questionaire. You will build an online application for heroes seeking employment. The agency is constantly tinkering with the application process, but by using the dynamic form you can create the new forms on the fly without changing the application code.

本教程将指导你完成以下步骤。

The tutorial walks you through the following steps.

  1. 为项目启用响应式表单。

    Enable reactive forms for a project.

  2. 建立一个数据模型来表示表单控件。

    Establish a data model to represent form controls.

  3. 使用范例数据填充模型。

    Populate the model with sample data.

  4. 开发一个组件来动态创建表单控件。

    Develop a component to create form controls dynamically.

你创建的表单会使用输入验证和样式来改善用户体验。它有一个 Submit 按钮,这个按钮只会在所有的用户输入都有效时启用,并用色彩和一些错误信息来标记出无效输入。

The form you create uses input validation and styling to improve the user experience. It has a Submit button that is only enabled when all user input is valid, and flags invalid input with color coding and error messages.

这个基本版可以不断演进,以支持更多的问题类型、更优雅的渲染体验以及更高大上的用户体验。

The basic version can evolve to support a richer variety of questions, more graceful rendering, and superior user experience.

先决条件

Prerequisites

在做本教程之前,你应该对下列内容有一个基本的了解。

Before doing this tutorial, you should have a basic understanding to the following.

为项目启用响应式表单

Enable reactive forms for your project

动态表单是基于响应式表单的。为了让应用访问响应式表达式指令,根模块会@angular/forms 库中导入 ReactiveFormsModule

Dynamic forms are based on reactive forms. To give the application access reactive forms directives, the root module imports ReactiveFormsModule from the @angular/forms library.

以下代码展示了此范例在根模块中所做的设置。

The following code from the example shows the setup in the root module.

      
      import { 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() {
  }
}
    

创建一个表单对象模型

Create a form object model

动态表单需要一个对象模型来描述此表单功能所需的全部场景。英雄应用表单中的例子是一组问题 - 也就是说,表单中的每个控件都必须提问并接受一个答案。

A dynamic form requires an object model that can describe all scenarios needed by the form functionality. The example hero-application form is a set of questions—that is, each control in the form must ask a question and accept an answer.

此类表单的数据模型必须能表示一个问题。本例中包含 DynamicFormQuestionComponent,它定义了一个问题作为模型中的基本对象。

The data model for this type of form must represent a question. The example includes the DynamicFormQuestionComponent, which defines a question as the fundamental object in the model.

这个 QuestionBase 是一组控件的基类,可以在表单中表示问题及其答案。

The following QuestionBase is a base class for a set of controls that can represent the question and its answer in the form.

src/app/question-base.ts
      
      export class QuestionBase<T> {
  value: T;
  key: string;
  label: string;
  required: boolean;
  order: number;
  controlType: string;
  type: string;
  options: {key: string, value: string}[];

  constructor(options: {
      value?: T;
      key?: string;
      label?: string;
      required?: boolean;
      order?: number;
      controlType?: string;
      type?: string;
      options?: {key: string, value: 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 || '';
    this.type = options.type || '';
    this.options = options.options || [];
  }
}
    

定义控件类

Define control classes

此范例从这个基类派生出两个新类,TextboxQuestionDropdownQuestion,分别代表不同的控件类型。当你在下一步中创建表单模板时,你会实例化这些具体的问题类,以便动态渲染相应的控件。

From this base, the example derives two new classes, TextboxQuestion and DropdownQuestion, that represent different control types. When you create the form template in the next step, you will instantiate these specific question types in order to render the appropriate controls dynamically.

  • TextboxQuestion 控件类型表示一个普通问题,并允许用户输入答案。

    The TextboxQuestion control type presents a question and allows users to enter input.

    src/app/question-textbox.ts
          
          import { QuestionBase } from './question-base';
    
    export class TextboxQuestion extends QuestionBase<string> {
      controlType = 'textbox';
    }
        

    TextboxQuestion 控件类型将使用 <input> 元素表示在表单模板中。该元素的 type 属性将根据 options 参数中指定的 type 字段定义(例如 textemailurl )。

    The TextboxQuestion control type will be represented in a form template using an <input> element. The type attribute of the element will be defined based on the type field specified in the options argument (for example text, email, url).

  • DropdownQuestion 控件表示在选择框中的一个选项列表。

    The DropdownQuestion control presents a list of choices in a select box.

    src/app/question-dropdown.ts
          
          import { QuestionBase } from './question-base';
    
    export class DropdownQuestion extends QuestionBase<string> {
      controlType = 'dropdown';
    }
        

编写表单组

Compose form groups

动态表单会使用一个服务来根据表单模型创建输入控件的分组集合。下面的 QuestionControlService 会收集一组 FormGroup 实例,这些实例会消费问题模型中的元数据。你可以指定一些默认值和验证规则。

A dynamic form uses a service to create grouped sets of input controls, based on the form model. The following QuestionControlService collects a set of FormGroup instances that consume the metadata from the question model. You can specify default values and validation rules.

src/app/question-control.service.ts
      
      import { Injectable } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';

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

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

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

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

编写动态表单内容

Compose dynamic form contents

动态表单本身就是一个容器组件,稍后你会添加它。每个问题都会在表单组件的模板中用一个 <app-question> 标签表示,该标签会匹配 DynamicFormQuestionComponent 中的一个实例。

The dynamic form itself will be represented by a container component, which you will add in a later step. Each question is represented in the form component's template by an <app-question> tag, which matches an instance of DynamicFormQuestionComponent.

DynamicFormQuestionComponent 负责根据数据绑定的问题对象中的各种值来渲染单个问题的详情。该表单依靠 [formGroup] 指令来将模板 HTML 和底层的控件对象联系起来。DynamicFormQuestionComponent 会创建表单组,并用问题模型中定义的控件来填充它们,并指定显示和验证规则。

The DynamicFormQuestionComponent is responsible for rendering the details of an individual question based on values in the data-bound question object. The form relies on a [formGroup] directive to connect the template HTML to the underlying control objects. The DynamicFormQuestionComponent creates form groups and populates them with controls defined in the question model, specifying display and validation rules.

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

DynamicFormQuestionComponent 的目标是展示模型中定义的各类问题。你现在只有两类问题,但可以想象将来还会有更多。模板中的 ngSwitch 语句会决定要显示哪种类型的问题。这里用到了带有 formControlNameformGroup选择器的指令。这两个指令都是在 ReactiveFormsModule 中定义的。

The goal of the DynamicFormQuestionComponent is to present question types defined in your model. You only have two types of questions at this point but you can imagine many more. The ngSwitch statement in the template determines which type of question to display. The switch uses directives with the formControlNameand formGroupselectors. Both directives are defined in ReactiveFormsModule.

提供数据

Supply data

还要另外一项服务来提供一组具体的问题,以便构建出一个单独的表单。在本练习中,你将创建 QuestionService 以从硬编码的范例数据中提供这组问题。在真实世界的应用中,该服务可能会从后端获取数据。重点是,你可以完全通过 QuestionService 返回的对象来控制英雄的求职申请问卷。要想在需求发生变化时维护问卷,你只需要在 questions 数组中添加、更新和删除对象。

Another service is needed to supply a specific set of questions from which to build an individual form. For this exercise you will create the QuestionService to supply this array of questions from the hard-coded sample data. In a real-world app, the service might fetch data from a backend system. The key point, however, is that you control the hero job-application questions entirely through the objects returned from QuestionService. To maintain the questionnaire as requirements change, you only need to add, update, and remove objects from the questions array.

QuestionService 以一个绑定到 @Input() 的问题数组的形式提供了一组问题。

The QuestionService supplies a set of questions in the form of an array bound to @Input() questions.

src/app/question.service.ts
      
      import { Injectable } from '@angular/core';

import { DropdownQuestion } from './question-dropdown';
import { QuestionBase } from './question-base';
import { TextboxQuestion } from './question-textbox';
import { of } from 'rxjs';

@Injectable()
export class QuestionService {

  // TODO: get from a remote source of question metadata
  getQuestions() {

    const questions: QuestionBase<string>[] = [

      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 of(questions.sort((a, b) => a.order - b.order));
  }
}
    

创建一个动态表单模板

Create a dynamic form template

DynamicFormComponent 组件是表单的入口点和主容器,它在模板中用 <app-dynamic-form> 表示。

The DynamicFormComponent component is the entry point and the main container for the form, which is represented using the <app-dynamic-form> in a template.

DynamicFormComponent 组件通过把每个问题都绑定到一个匹配 DynamicFormQuestionComponent<app-question> 元素来渲染问题列表。

The DynamicFormComponent component presents a list of questions by binding each one to an <app-question> element that matches the DynamicFormQuestionComponent.

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

显示表单

Display the form

要显示动态表单的一个实例,AppComponent 外壳模板会把一个 QuestionService 返回的 questions 数组传给表单容器组件 <app-dynamic-form>

To display an instance of the dynamic form, the AppComponent shell template passes the questions array returned by the QuestionService to the form container component, <app-dynamic-form>.

app.component.ts
      
      import { Component } from '@angular/core';

import { QuestionService } from './question.service';
import { QuestionBase } from './question-base';
import { Observable } from 'rxjs';

@Component({
  selector: 'app-root',
  template: `
    <div>
      <h2>Job Application for Heroes</h2>
      <app-dynamic-form [questions]="questions$ | async"></app-dynamic-form>
    </div>
  `,
  providers:  [QuestionService]
})
export class AppComponent {
  questions$: Observable<QuestionBase<any>[]>;

  constructor(service: QuestionService) {
    this.questions$ = service.getQuestions();
  }
}
    

这个例子为英雄提供了一个工作申请表的模型,但是除了 QuestionService 返回的对象外,没有涉及任何跟英雄有关的问题。这种模型和数据的分离,允许你为任何类型的调查表复用这些组件,只要它与这个问题对象模型兼容即可。

The example provides a model for a job application for heroes, but there are no references to any specific hero question other than the objects returned by QuestionService. This separation of model and data allows you to repurpose the components for any type of survey as long as it's compatible with the question object model.

确保数据有效

Ensuring valid data

表单模板使用元数据的动态数据绑定来渲染表单,而不用做任何与具体问题有关的硬编码。它动态添加了控件元数据和验证标准。

The form template uses dynamic data binding of metadata to render the form without making any hardcoded assumptions about specific questions. It adds both control metadata and validation criteria dynamically.

要确保输入有效,就要禁用 “Save” 按钮,直到此表单处于有效状态。当表单有效时,你可以单击 “Save” 按钮,该应用就会把表单的当前值渲染为 JSON。

To ensure valid input, the Save button is disabled until the form is in a valid state. When the form is valid, you can click Save and the app renders the current form values as JSON.

最终的表单如下图所示。

The following figure shows the final form.

下一步

Next steps

  • 不同类型的表单和控件集合

    Different types of forms and control collection

    本教程展示了如何构建一个问卷,它只是一种动态表单。这个例子使用 FormGroup 来收集一组控件。关于不同类型动态表单的范例,请参阅在响应式表单中的创建动态表单一节。那个例子还展示了如何使用 FormArray 而不是 FormGroup 来收集一组控件。

    This tutorial shows how to build a a questionaire, which is just one kind of dynamic form. The example uses FormGroup to collect a set of controls. For an example of a different type of dynamic form, see the section Creating dynamic forms in the Reactive Forms guide. That example also shows how to use FormArray instead of FormGroup to collect a set of controls.

  • 验证用户输入

    Validating user input

    验证表单输入部分介绍了如何在响应式表单中进行输入验证的基础知识。

    The section Validating form input introduces the basics of how input validation works in reactive forms.

    表单验证指南更深入地介绍了本主题。

    The Form validation guide covers the topic in more depth.