2017-05-26 15:28:06 -04:00
|
|
|
|
# Dynamic Forms
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-07-29 12:03:22 -04:00
|
|
|
|
# 动态表单
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-04-30 16:10:32 -04:00
|
|
|
|
{@a top}
|
2017-03-31 19:57:13 -04:00
|
|
|
|
|
2018-03-03 08:06:01 -05:00
|
|
|
|
Building handcrafted forms can be costly and time-consuming,
|
|
|
|
|
especially if you need a great number of them, they're similar to each other, and they change frequently
|
2017-02-22 13:09:39 -05:00
|
|
|
|
to meet rapidly changing business and regulatory requirements.
|
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
有时候手动编写和维护表单所需工作量和时间会过大。特别是在需要编写大量表单时。表单都很相似,而且随着业务和监管需求的迅速变化,表单也要随之变化,这样维护的成本过高。
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2018-03-03 08:06:01 -05:00
|
|
|
|
It may be more economical to create the forms dynamically, based on
|
|
|
|
|
metadata that describes the business object model.
|
2017-07-22 23:51:25 -04:00
|
|
|
|
|
2017-08-27 23:54:59 -04:00
|
|
|
|
基于业务对象模型的元数据,动态创建表单可能会更划算。
|
|
|
|
|
|
2018-03-03 08:06:01 -05:00
|
|
|
|
This cookbook shows you how to use `formGroup` to dynamically
|
2017-04-12 15:53:18 -04:00
|
|
|
|
render a simple form with different control types and validation.
|
|
|
|
|
It's a primitive start.
|
2017-02-22 13:09:39 -05:00
|
|
|
|
It might evolve to support a much richer variety of questions, more graceful rendering, and superior user experience.
|
|
|
|
|
All such greatness has humble beginnings.
|
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
在此烹饪宝典中,我们会展示如何利用`formGroup`来动态渲染一个简单的表单,包括各种控件类型和验证规则。
|
2017-08-27 23:54:59 -04:00
|
|
|
|
这个起点很简陋,但可以在这个基础上添加丰富多彩的问卷问题、更优美的渲染以及更卓越的用户体验。
|
|
|
|
|
|
2018-03-03 08:06:01 -05:00
|
|
|
|
The example in this cookbook is a dynamic form to build an
|
2017-04-12 15:53:18 -04:00
|
|
|
|
online application experience for heroes seeking employment.
|
2017-02-22 13:09:39 -05:00
|
|
|
|
The agency is constantly tinkering with the application process.
|
2018-03-03 08:06:01 -05:00
|
|
|
|
You can create the forms on the fly *without changing the application code*.
|
2017-07-22 23:51:25 -04:00
|
|
|
|
|
|
|
|
|
在本例中,我们使用动态表单,为正在找工作的英雄们创建一个在线申请表。英雄管理局会不断修改申请流程,我们要在*不修改应用代码*的情况下,动态创建这些表单。
|
|
|
|
|
|
2017-03-31 19:57:13 -04:00
|
|
|
|
{@a toc}
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-04-21 20:21:45 -04:00
|
|
|
|
See the <live-example name="dynamic-form"></live-example>.
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-07-29 12:03:22 -04:00
|
|
|
|
参见<live-example name="dynamic-form"></live-example>。
|
2017-07-22 23:51:25 -04:00
|
|
|
|
|
2017-03-31 19:57:13 -04:00
|
|
|
|
{@a bootstrap}
|
|
|
|
|
|
|
|
|
|
## Bootstrap
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2018-03-03 08:06:01 -05:00
|
|
|
|
## 启动/引导 (bootstrap)
|
2017-07-22 23:51:25 -04:00
|
|
|
|
|
2017-04-12 15:53:18 -04:00
|
|
|
|
Start by creating an `NgModule` called `AppModule`.
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
让我们从创建一个名叫`AppModule`的`NgModule`开始。
|
|
|
|
|
|
2017-04-12 15:53:18 -04:00
|
|
|
|
This cookbook uses [reactive forms](guide/reactive-forms).
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
这个烹饪书使用[响应式表单](guide/reactive-forms)。
|
|
|
|
|
|
2017-04-26 08:11:02 -04:00
|
|
|
|
Reactive forms belongs to a different `NgModule` called `ReactiveFormsModule`,
|
|
|
|
|
so in order to access any reactive forms directives, you have to import
|
2017-04-12 15:53:18 -04:00
|
|
|
|
`ReactiveFormsModule` from the `@angular/forms` library.
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
响应式表单属于另外一个叫做`ReactiveFormsModule`的`NgModule`,所以,为了使用响应式表单类的指令,我们得从`@angular/forms`库中引入`ReactiveFormsModule`模块。
|
|
|
|
|
|
2017-04-12 15:53:18 -04:00
|
|
|
|
Bootstrap the `AppModule` in `main.ts`.
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
我们在`main.ts`中启动`AppModule`。
|
|
|
|
|
|
2017-03-27 11:08:53 -04:00
|
|
|
|
<code-tabs>
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-04-21 20:21:45 -04:00
|
|
|
|
<code-pane title="app.module.ts" path="dynamic-form/src/app/app.module.ts">
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-03-27 11:08:53 -04:00
|
|
|
|
</code-pane>
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-04-21 20:21:45 -04:00
|
|
|
|
<code-pane title="main.ts" path="dynamic-form/src/main.ts">
|
2017-03-27 11:08:53 -04:00
|
|
|
|
|
|
|
|
|
</code-pane>
|
|
|
|
|
|
|
|
|
|
</code-tabs>
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-03-31 19:57:13 -04:00
|
|
|
|
{@a object-model}
|
|
|
|
|
|
2017-04-12 15:53:18 -04:00
|
|
|
|
## Question model
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
## 问卷问题模型
|
|
|
|
|
|
2017-02-22 13:09:39 -05:00
|
|
|
|
The next step is to define an object model that can describe all scenarios needed by the form functionality.
|
2017-04-12 15:53:18 -04:00
|
|
|
|
The hero application process involves a form with a lot of questions.
|
|
|
|
|
The _question_ is the most fundamental object in the model.
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
第一步是定义一个对象模型,用来描述所有表单功能需要的场景。英雄的申请流程涉及到一个包含很多问卷问题的表单。问卷问题是最基础的对象模型。
|
|
|
|
|
|
2017-04-12 15:53:18 -04:00
|
|
|
|
The following `QuestionBase` is a fundamental question class.
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
下面是我们建立的最基础的问卷问题基类,名叫`QuestionBase`。
|
|
|
|
|
|
2017-04-21 20:21:45 -04:00
|
|
|
|
<code-example path="dynamic-form/src/app/question-base.ts" title="src/app/question-base.ts">
|
2017-03-27 11:08:53 -04:00
|
|
|
|
|
|
|
|
|
</code-example>
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-04-26 08:11:02 -04:00
|
|
|
|
From this base you can derive two new classes in `TextboxQuestion` and `DropdownQuestion`
|
2017-04-12 15:53:18 -04:00
|
|
|
|
that represent textbox and dropdown questions.
|
2017-04-26 08:11:02 -04:00
|
|
|
|
The idea is that the form will be bound to specific question types and render the
|
2017-04-12 15:53:18 -04:00
|
|
|
|
appropriate controls dynamically.
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
在这个基础上,我们派生出两个新类`TextboxQuestion` 和 `DropdownQuestion`,分别代表文本框和下拉框。这么做的初衷是,表单能动态绑定到特定的问卷问题类型,并动态渲染出合适的控件。
|
|
|
|
|
|
2017-04-26 08:11:02 -04:00
|
|
|
|
`TextboxQuestion` supports multiple HTML5 types such as text, email, and url
|
2017-04-12 15:53:18 -04:00
|
|
|
|
via the `type` property.
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
`TextboxQuestion`可以通过`type`属性来支持多种HTML5元素类型,比如文本、邮件、网址等。
|
|
|
|
|
|
2017-04-21 20:21:45 -04:00
|
|
|
|
<code-example path="dynamic-form/src/app/question-textbox.ts" title="src/app/question-textbox.ts" linenums="false">
|
2017-03-27 11:08:53 -04:00
|
|
|
|
|
|
|
|
|
</code-example>
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
|
|
|
|
`DropdownQuestion` presents a list of choices in a select box.
|
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
`DropdownQuestion`表示一个带可选项列表的选择框。
|
|
|
|
|
|
2017-04-21 20:21:45 -04:00
|
|
|
|
<code-example path="dynamic-form/src/app/question-dropdown.ts" title="src/app/question-dropdown.ts" linenums="false">
|
2017-03-27 11:08:53 -04:00
|
|
|
|
|
|
|
|
|
</code-example>
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-04-12 15:53:18 -04:00
|
|
|
|
Next is `QuestionControlService`, a simple service for transforming the questions to a `FormGroup`.
|
2017-04-26 08:11:02 -04:00
|
|
|
|
In a nutshell, the form group consumes the metadata from the question model and
|
2017-04-12 15:53:18 -04:00
|
|
|
|
allows you to specify default values and validation rules.
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
接下来,我们定义了`QuestionControlService`,一个可以把问卷问题转换为`FormGroup`的服务。
|
|
|
|
|
简而言之,这个`FormGroup`使用问卷模型的元数据,并允许我们设置默认值和验证规则。
|
|
|
|
|
|
2017-04-21 20:21:45 -04:00
|
|
|
|
<code-example path="dynamic-form/src/app/question-control.service.ts" title="src/app/question-control.service.ts" linenums="false">
|
2017-03-27 11:08:53 -04:00
|
|
|
|
|
|
|
|
|
</code-example>
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-03-31 19:57:13 -04:00
|
|
|
|
{@a form-component}
|
|
|
|
|
|
|
|
|
|
## Question form components
|
2017-07-22 23:51:25 -04:00
|
|
|
|
|
|
|
|
|
## 问卷表单组件
|
|
|
|
|
|
2017-04-26 08:11:02 -04:00
|
|
|
|
Now that you have defined the complete model you are ready
|
2017-04-12 15:53:18 -04:00
|
|
|
|
to create components to represent the dynamic form.
|
2017-03-31 19:57:13 -04:00
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
现在我们已经有一个定义好的完整模型了,接着就可以开始创建一个展现动态表单的组件。
|
|
|
|
|
|
2018-03-03 08:06:01 -05:00
|
|
|
|
`DynamicFormComponent` is the entry point and the main container for the form.
|
2017-07-22 23:51:25 -04:00
|
|
|
|
|
|
|
|
|
`DynamicFormComponent`是表单的主要容器和入口点。
|
2017-03-31 19:57:13 -04:00
|
|
|
|
|
2017-03-27 11:08:53 -04:00
|
|
|
|
<code-tabs>
|
|
|
|
|
|
2017-04-21 20:21:45 -04:00
|
|
|
|
<code-pane title="dynamic-form.component.html" path="dynamic-form/src/app/dynamic-form.component.html">
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-03-27 11:08:53 -04:00
|
|
|
|
</code-pane>
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-04-21 20:21:45 -04:00
|
|
|
|
<code-pane title="dynamic-form.component.ts" path="dynamic-form/src/app/dynamic-form.component.ts">
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-03-27 11:08:53 -04:00
|
|
|
|
</code-pane>
|
|
|
|
|
|
|
|
|
|
</code-tabs>
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2018-01-10 04:45:05 -05:00
|
|
|
|
It presents a list of questions, each bound to a `<app-question>` component element.
|
|
|
|
|
The `<app-question>` tag matches the `DynamicFormQuestionComponent`,
|
2017-04-26 08:11:02 -04:00
|
|
|
|
the component responsible for rendering the details of each _individual_
|
2017-04-12 15:53:18 -04:00
|
|
|
|
question based on values in the data-bound question object.
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2018-03-07 02:42:49 -05:00
|
|
|
|
它代表了问卷问题列表,每个问题都被绑定到一个`<app-question>`组件元素。
|
|
|
|
|
`<app-question>`标签匹配到的是组件`DynamicFormQuestionComponent`,该组件的职责是根据各个问卷问题对象的值来动态渲染表单控件。
|
2017-07-22 23:51:25 -04:00
|
|
|
|
|
2017-03-27 11:08:53 -04:00
|
|
|
|
<code-tabs>
|
|
|
|
|
|
2017-04-21 20:21:45 -04:00
|
|
|
|
<code-pane title="dynamic-form-question.component.html" path="dynamic-form/src/app/dynamic-form-question.component.html">
|
2017-03-27 11:08:53 -04:00
|
|
|
|
|
|
|
|
|
</code-pane>
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-04-21 20:21:45 -04:00
|
|
|
|
<code-pane title="dynamic-form-question.component.ts" path="dynamic-form/src/app/dynamic-form-question.component.ts">
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-03-27 11:08:53 -04:00
|
|
|
|
</code-pane>
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-03-27 11:08:53 -04:00
|
|
|
|
</code-tabs>
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-04-12 15:53:18 -04:00
|
|
|
|
Notice this component can present any type of question in your model.
|
|
|
|
|
You only have two types of questions at this point but you can imagine many more.
|
2017-02-22 13:09:39 -05:00
|
|
|
|
The `ngSwitch` determines which type of question to display.
|
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
请注意,这个组件能代表模型里的任何问题类型。目前,还只有两种问题类型,但可以添加更多类型。可以用`ngSwitch`决定显示哪种类型的问题。
|
|
|
|
|
|
2017-04-12 15:53:18 -04:00
|
|
|
|
In both components you're relying on Angular's **formGroup** to connect the template HTML to the
|
2017-02-22 13:09:39 -05:00
|
|
|
|
underlying control objects, populated from the question model with display and validation rules.
|
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
在这两个组件中,我们依赖Angular的**formGroup**来把模板HTML和底层控件对象连接起来,该对象从问卷问题模型里获取渲染和验证规则。
|
|
|
|
|
|
2017-04-26 08:11:02 -04:00
|
|
|
|
`formControlName` and `formGroup` are directives defined in
|
|
|
|
|
`ReactiveFormsModule`. The templates can access these directives
|
2017-04-12 15:53:18 -04:00
|
|
|
|
directly since you imported `ReactiveFormsModule` from `AppModule`.
|
2017-07-22 23:51:25 -04:00
|
|
|
|
|
|
|
|
|
`formControlName`和`formGroup`是在`ReactiveFormsModule`中定义的指令。我们之所以能在模板中使用它们,是因为我们往`AppModule`中导入了`ReactiveFormsModule`。
|
2018-03-03 08:06:01 -05:00
|
|
|
|
|
2017-04-12 15:53:18 -04:00
|
|
|
|
{@a questionnaire-data}
|
2017-03-31 19:57:13 -04:00
|
|
|
|
|
|
|
|
|
## Questionnaire data
|
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
## 问卷数据
|
|
|
|
|
|
2017-04-12 15:53:18 -04:00
|
|
|
|
`DynamicFormComponent` expects the list of questions in the form of an array bound to `@Input() questions`.
|
|
|
|
|
|
2018-03-03 08:06:01 -05:00
|
|
|
|
`DynamicForm`期望得到一个问题列表,该列表被绑定到`@Input() questions`属性。
|
2017-07-22 23:51:25 -04:00
|
|
|
|
|
2017-04-12 15:53:18 -04:00
|
|
|
|
The set of questions you've defined for the job application is returned from the `QuestionService`.
|
|
|
|
|
In a real app you'd retrieve these questions from storage.
|
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
`QuestionService`会返回为工作申请表定义的那组问题列表。在真实的应用程序环境中,我们会从数据库里获得这些问题列表。
|
|
|
|
|
|
2017-04-26 08:11:02 -04:00
|
|
|
|
The key point is that you control the hero job application questions
|
2017-04-12 15:53:18 -04:00
|
|
|
|
entirely through the objects returned from `QuestionService`.
|
2017-04-26 08:11:02 -04:00
|
|
|
|
Questionnaire maintenance is a simple matter of adding, updating,
|
2017-04-12 15:53:18 -04:00
|
|
|
|
and removing objects from the `questions` array.
|
2018-03-03 08:06:01 -05:00
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
关键是,我们完全根据`QuestionService`返回的对象来控制英雄的工作申请表。
|
|
|
|
|
要维护这份问卷,只要非常简单的添加、更新和删除`questions`数组中的对象就可以了。
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-04-21 20:21:45 -04:00
|
|
|
|
<code-example path="dynamic-form/src/app/question.service.ts" title="src/app/question.service.ts">
|
2017-03-27 11:08:53 -04:00
|
|
|
|
|
|
|
|
|
</code-example>
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-04-12 15:53:18 -04:00
|
|
|
|
Finally, display an instance of the form in the `AppComponent` shell.
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
最后,在`AppComponent`里显示出表单。
|
|
|
|
|
|
2017-04-21 20:21:45 -04:00
|
|
|
|
<code-example path="dynamic-form/src/app/app.component.ts" title="app.component.ts">
|
2017-03-27 11:08:53 -04:00
|
|
|
|
|
|
|
|
|
</code-example>
|
2017-02-22 13:09:39 -05:00
|
|
|
|
|
2017-03-31 19:57:13 -04:00
|
|
|
|
{@a dynamic-template}
|
|
|
|
|
|
|
|
|
|
## Dynamic Template
|
2017-08-06 01:21:34 -04:00
|
|
|
|
|
|
|
|
|
## 动态模板
|
|
|
|
|
|
2018-03-03 08:06:01 -05:00
|
|
|
|
Although in this example you're modelling a job application for heroes, there are
|
2017-04-12 15:53:18 -04:00
|
|
|
|
no references to any specific hero question
|
|
|
|
|
outside the objects returned by `QuestionService`.
|
2017-07-22 23:51:25 -04:00
|
|
|
|
|
|
|
|
|
在这个例子中,虽然我们是在为英雄的工作申请表建模,但是除了`QuestionService`返回的那些对象外,没有其它任何地方是与英雄有关的。
|
2017-04-12 15:53:18 -04:00
|
|
|
|
|
|
|
|
|
This is very important since it allows you to repurpose the components for any type of survey
|
2018-03-03 08:06:01 -05:00
|
|
|
|
as long as it's compatible with the *question* object model.
|
|
|
|
|
The key is the dynamic data binding of metadata used to render the form
|
|
|
|
|
without making any hardcoded assumptions about specific questions.
|
2017-04-12 15:53:18 -04:00
|
|
|
|
In addition to control metadata, you are also adding validation dynamically.
|
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
这点非常重要,因为只要与*问卷*对象模型兼容,就可以在任何类型的调查问卷中复用这些组件。
|
|
|
|
|
这里的关键是用到元数据的动态数据绑定来渲染表单,对问卷问题没有任何硬性的假设。除控件的元数据外,还可以动态添加验证规则。
|
|
|
|
|
|
2017-04-12 15:53:18 -04:00
|
|
|
|
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.
|
2017-02-22 13:09:39 -05:00
|
|
|
|
This proves that any user input is bound back to the data model.
|
|
|
|
|
Saving and retrieving the data is an exercise for another time.
|
2017-03-31 19:57:13 -04:00
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
表单验证通过之前,*保存*按钮是禁用的。验证通过后,就可以点击*保存*按钮,程序会把当前值渲染成JSON显示出来。
|
|
|
|
|
这表明任何用户输入都被传到了数据模型里。至于如何储存和提取数据则是另一话题了。
|
|
|
|
|
|
2017-02-22 13:09:39 -05:00
|
|
|
|
The final form looks like this:
|
2017-03-30 15:04:18 -04:00
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
完整的表单是这样的:
|
|
|
|
|
|
2017-05-09 18:53:32 -04:00
|
|
|
|
<figure>
|
2018-03-03 08:06:01 -05:00
|
|
|
|
<img src="generated/images/guide/dynamic-form/dynamic-form.png" alt="Dynamic-Form">
|
|
|
|
|
</figure>
|
2017-03-31 19:57:13 -04:00
|
|
|
|
|
2017-07-22 23:51:25 -04:00
|
|
|
|
[Back to top](guide/dynamic-form#top)
|
|
|
|
|
|
2018-03-03 08:06:01 -05:00
|
|
|
|
|
2018-03-06 22:25:56 -05:00
|
|
|
|
[回到顶部](guide/dynamic-form#top)
|