前言
之前已经介绍过Angular中动态表单的基本实现思路,可参考Angular动态表单构建的简单实现,其中简单的介绍了前端的代码。这篇文章在之前的实现方式上做了部分修改,比如没有再使用[ngSwitch]
而是建立了一个自定义组件配置类,然后使用动态组件的方式来渲染组件,向对修改关闭,对扩展开放的开发原则靠拢。具体可以查看源码,已在文末给出
本文将在此基础上结合数据表结构通过模拟数据来简单演示下以教师新增申请和编辑申请作为场景时,如何将数据库中的数据转换成为v层渲染出的表单。
最终效果外观和普通表单类似,实际上每个表单都为动态渲染得到。
一、 修改说明
这篇文章中使用的类名与属性名与Angular动态表单构建的简单实现中的有所区别,先在此进行简单说明。本文中的表单对象模型将使用FormInfo
为类名,动态表单模板使用FormComponent
为类名,动态表单内容即表单项使用FieldComponent
为类名。
变化名称 | Angular动态表单构建的简单实现 | 本文 |
---|---|---|
表单对象模型 | Form | FormInfo |
动态表单模板(表单组件) | DynamicFormComponent | FormComponent |
动态表单内容(表单项组件) | DynamicFormUnitComponent | FieldComponent |
二、问题
之前的文章是直接模拟现成的FormInfo
(即之前文章中的Form
);这篇文章演示的是从后台获取原始数据,然后将原始数据转换为FormInfo
,再通过FormInfos
得到相应的表单。
所以我们要解决的主要问题是:
- 理解ER图
- 如何将原始数据转换成
FormInfo
对象模型
三、ER图模型
实现基本功能的动态表单的实体图如下,请对该实体图有个大概的了解。
四、ER图含义
4.1、 表单的构建
首先我们来讨论一下ER图的结构,此ER图中有8个表,我们暂时先只讨论申请类型ApplyType, 字段Field, 字段类型FieldType三个表。
- 申请类型:对申请进行分类,比如评奖申请,入职申请。
- 字段:记录在数据库的表单字段,比如一个表单中的姓名输入,地址输入。
- 字段类型:字段对应的自定义类型,比如姓名应该是输入文本,那么就可以用
textbox
作为其中type
属性的值;地址可以是选择类型,即以dropdown
为值(上面篇文章中定义的两个类型)。
每个申请类型对应多个字段,每个字段和一个字段类型相关联。
它们之间的关系就是一个申请类型就是一个表单模板,每个模板对应多个表单项(即字段),通过表单类型来规定表单项的形式。
4.2、 表单值记录
再讨论申请表Apply和字段记录表FieldRecord。4.1是针对新增的,新增完成之后需要对数据进行保存,而申请表Apply对应的就是新增完成之后保存的申请,通过字段记录表FieldRecord来记录它的值。
如同申请类型表ApplyType和字段表Field的关系,申请表Apply和字段表Field的关系也是这样,只是在之间加上了一个用来记录值的中间表。
4.3、 表单验证规则
继续讨论字段校验器FieldValidator,它记录了每个字段的校验规则,每个字段可能关联多个校验规则,比如required
,unique
。因为需要考虑到字段对于验证器的要求以及用户的输入,所以在定制表单的时候需要对验证器进行范围要求,不能让用户随意选择验证器。所以需要先通过字段类型表ApplyType到字段校验器表FieldValidator获取到可选验证器,然后通过用户的进一步选择,最后实现不同的字段有不同的验证规则。
4.4、字段选项
最后再加上数据集和数据项两个表,两表之间关系很简单,每个数据集对应多个数据项,主要的是为什么会有这两个表的存在。有一些字段类型需要进行选择,所以要给出选项,比如select以及radio等,这时候两个表就可以发挥作用了。对于一些静态数据,比如性别(男、女),评级(A、B、C)等可以使用数据集来提供。对于需要实时渲染的动态数据(比如学生选择,班级选择),可能会需要自定义相应的组件。
对于为什么将数据集DataSet和字段表Field建立关联而不是和字段类型表FieldType建立关系,是想到如果需要添加的数据集多的话和后者建立关系需要创建更多的自定义组件,而如果和前者建立关系无需补充组件。
五、将原始数据转化为FormInfo
本文使用的数据都是通过预先定义好的模拟数据。
新增演示
首先准备相应的模拟数据
import {Field} from '../entity/field';
import {Apply} from '../entity/apply';
import {FieldRecord} from '../entity/field-record';
import {DataSet} from '../entity/data-set';
import {DataItem} from '../entity/data-item';
import {ApplyType} from '../entity/apply-type';
import {FieldValidator} from '../entity/field-validator';
const MockDataItems = [
{id: 1, name: '男', weight: 2, dataSet: {id: 1} as DataSet},
{id: 2, name: '女', weight: 1, dataSet: {id: 1} as DataSet},
] as DataItem[];
const MockDataSets = [
{id: 1, name: '性别', dataItems: MockDataItems}
] as DataSet[];
const MockFieldValidators = [
{id: 1, name: '是否必需', key: 'required', value: true}
] as FieldValidator[];
const MockFields = [
{id: 1, fieldType: {id: 1, type: 'textbox'}, fieldValidators: MockFieldValidators,
key: 'name', label: '名称', weight: 4, type: 'text'},
{id: 2, fieldType: {id: 2, type: 'textbox'}, fieldValidators: MockFieldValidators,
key: 'username', label: '用户名', weight: 3, type: 'text'},
{id: 3, fieldType: {id: 3, type: 'textbox'}, fieldValidators: MockFieldValidators,
key: 'email', label: '邮箱', weight: 2, type: 'email'},
{id: 4, fieldType: {id: 4, type: 'textbox'}, fieldValidators: MockFieldValidators,
key: 'jobNumber', label: '工号', weight: 1, type: 'number'},
{id: 5, fieldType: {id: 5, type: 'radio'}, fieldValidators: MockFieldValidators,
key: 'sex', label: '性别', dataSet: MockDataSets[0], weight: 0, type: ''},
] as Field[];
export const MockApplyType = {
id: 1, name: '教师新增', fields: MockFields
} as ApplyType;
上述示例定义了1个申请类型,5个字段,1个验证器以及一个与男女数据项相关联的数据集。当从数据库获取这些数据之后,然后将其转换成FormInfo,FormInfos的中的对象个数取决于Fields的长度,即每一个Field都将转换成一个FormInfo。
下面是转换的具体方法:
private getFormInfo(field: Field, value?: any): FormInfo<any> {
const formInfo = new FormInfo<any>({
key: field.key,
label: field.label,
weight: field.weight,
controlType: field.fieldType.type,
type: field.type,
rule: this.getRule(field.fieldValidators) as unknown as RuleType,
value
});
if (DynamicOptionControls.includes(field.fieldType.type)) {
formInfo.options = field.dataSet.dataItems.map((item) => {
return {value: item.id, label: item.name, weight: item.weight};
});
}
return formInfo;
}
FormInfo
类中的controlType
和type
两个属性分别代表了自定义组件的类型以及细分类型(比如text,number);- 在这个方法中校验规则(
rule
)对象获取是通过获取到MockFieldValidators中各对象的key和value来实现的; - 方法中还需要注意的是数据集的使用,即通过预先定义的一个数组来判断对应的field.fieldType.type(即controlType)是否是需要添加options的字段类型,如果需要,那么就会将数据集中的数据项填充到这里,如果不需要则不进行填充。
- 这样就可以得到FormInfo的对应数组了,至于如何渲染,之前的文章已经说明,不过本文的实现会有部分不同,例如选择自定义组件没有再使用
[ngSwitch]
,而是使用的动态组件实现,这里不再过多讨论,实现方法可以参考https://worktile.com/kb/p/6517。
编辑演示
我们在模拟数据中添加以下两项:
const MockFieldRecords = [
{id: 1, apply: {id: 1}, field: MockFields[0], value: '张三'},
{id: 2, apply: {id: 1}, field: MockFields[1], value: 'zhangsan'},
{id: 3, apply: {id: 1}, field: MockFields[2], value: 'zhangsan@yunzhi.club'},
{id: 4, apply: {id: 1}, field: MockFields[3], value: '0621211001'},
{id: 4, apply: {id: 1}, field: MockFields[4], value: 1},
] as FieldRecord<any>[];
export const MockApply = {
id: 1, applyType: {id: 1, name: 'teacherAdd'}, status: 0, fieldRecords: MockFieldRecords
} as Apply;
编辑和新增基本相同,只不过编辑时获取到的表单需要设置value
初值,而这个初值就是在FieldRecord
表中已经记录过的value属性的值,也就是模拟数据中的MockFieldRecords
。
查看之前的方法,可以发现方法中通过可选参数value来设置初值。
private getFormInfo(field: Field, value?: any): FormInfo<any> {
const formInfo = new FormInfo<any>({
...,
value
});
...
}
六、总结
以上是当动态表单与数据库结合起来后简单实现,文中给出了大概思路,代码涉及较少,完整的示例代码可以参考:
- github: https://github.com/chshihang/dynamic-form-er
- gitee: https://gitee.com/chshihang/dynamic-form-er
项目目录结构如下:
├── app
│ ├── app-routing.module.ts
│ ├── app.component.html
│ ├── app.component.ts
│ ├── app.module.ts
│ ├── dynamic-form
│ │ ├── dynamic-form.module.ts
│ │ ├── field
│ │ │ ├── components
│ │ │ │ ├── control-type-list.config.ts
│ │ │ │ ├── dynamic-checkbox
│ │ │ │ │ ├── dynamic-checkbox.component.html
│ │ │ │ │ └── dynamic-checkbox.component.ts
│ │ │ │ ├── dynamic-dropdown
│ │ │ │ │ ├── dynamic-dropdown.component.html
│ │ │ │ │ └── dynamic-dropdown.component.ts
│ │ │ │ ├── dynamic-error
│ │ │ │ │ ├── dynamic-error.component.html
│ │ │ │ │ └── dynamic-error.component.ts
│ │ │ │ ├── dynamic-radio
│ │ │ │ │ ├── dynamic-radio.component.html
│ │ │ │ │ └── dynamic-radio.component.ts
│ │ │ │ └── dynamic-textbox
│ │ │ │ ├── dynamic-textbox.component.html
│ │ │ │ └── dynamic-textbox.component.ts
│ │ │ ├── field.component.html
│ │ │ └── field.component.ts
│ │ └── form
│ │ ├── form.component.html
│ │ └── form.component.ts
│ └── teacher
│ ├── teacher-add
│ │ ├── teacher-add.component.html
│ │ └── teacher-add.component.ts
│ ├── teacher-edit
│ │ ├── teacher-edit.component.html
│ │ └── teacher-edit.component.ts
│ ├── teacher-index
│ │ ├── teacher-index.component.html
│ │ └── teacher-index.component.ts
│ ├── teacher-routing.module.ts
│ └── teacher.module.ts
├── assets
│ └── mock-form-data.ts
├── entity
│ ├── apply-type.ts
│ ├── apply.ts
│ ├── data-item.ts
│ ├── data-set.ts
│ ├── field-record.ts
│ ├── field-type.ts
│ ├── field-validator.ts
│ ├── field.ts
│ ├── form-info.ts
│ └── rule-type.ts
├── environments
│ ├── environment.prod.ts
│ └── environment.ts
├── favicon.ico
├── index.html
├── interface
│ └── dynamic-form.interface.ts
├── main.ts
├── polyfills.ts
├── service
│ └── dynamic-form.service.ts
├── styles.scss
└── test.ts
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。