填寫這份《一分鐘調查》,幫我們(開發組)做得更好!去填寫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|undefined;
  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.