动态表单的设计与实现
- 实现功能
- 基础结构的设计
- 代码实现
备注:细节可能有误,主要提供思路
实现功能
目前主要实现了以下4个功能
- 表单的可配置化;
- 具体字段的实时监听(观察者模式);
- 一个字段控制另一个字段的是否可编辑(发布订阅模式);
- 支持三种模式:新建、更新、详情
基础结构的设计
首先定义我们需要解析的json配置,基础结构如下:
[
{
groupNo: "basic",
groupName: "基本信息",
fields: [
{
component: "el-input",
fieldId: "name",
name: "姓名",
default: "", // 默认值
display: 1, // 是否展示
required: 1, // 是否必填
edit: 1, // 是否可编辑
minLength: 5, // 最小长度
maxLength: 10 // 最大长度
},
{
component: "el-select",
fieldId: "country",
name: "国家",
display: 1,
required: 1,
edit: 1,
default: "",
multiple: false, // 是否多选
remote: false, // 是否远程搜索
options: [
{
label: "中国",
value: "CN"
},
{
label: "美国",
value: "US"
},
{
label: "日本",
value: "JPN"
}
],
},
{
component: "el-radio",
fieldId: "sex",
name: "性别",
display: 1,
required: 1,
edit: 1,
default: 0,
remote: false,
options: [
{
label: "男",
value: 0
},
{
label: "女",
value: 1
},
],
},
{
component: "el-checkbox",
fieldId: "education",
name: "学历",
display: 1,
required: 1,
edit: 1,
default: 1,
remote: false,
options: [
{
label: "大专",
value: 0
},
{
label: "本科",
value: 1
},
{
label: "硕士",
value: 2
},
{
label: "博士",
value: 3
},
],
}
]
}
]
代码实现
依照该配置,我们将整个表单组件的代码结构设计为:
入口文件:Index.vue
表单分组:GroupItem.vue
表单单元:FormItem.vue
最基础的实现如下:
Index.vue:
<template>
<el-collapse v-model="activeNames">
<GroupItem
v-for="tplItem in tplList"
:ref="tplItem.groupNo"
:key="tplItem.groupName"
:tpl="tplItem"
:form-mode="formMode"
:form-data="formData"
@change="groupFormDataChange($event, tplItem)"
/>
</el-collapse>
</template>
<script>
import GroupItem from "./components/GroupItem.vue";
export default {
components: {
GroupItem,
},
props: {
formMode: {
type: String,
default: "create", // create,update,detail
},
formConfig: {
type: Array,
default: () => [],
},
},
data() {
return {
formData: {},
tplList: [],
activeNames: [],
};
},
created() {
this.tplList = this.formConfig;
},
methods: {
/**
* 组件表单数据变化
*/
groupFormDataChange(formData, groupTpl) {
this.formData[groupTpl.groupNo] = formData;
},
}
};
</script>
GroupItem.vue:
<template>
<el-collapse-item :title="tpl.groupName" :name="tpl.groupName">
<el-form
ref="hclForm"
:model="groupFormData"
:rules="rules"
:inline="true"
:validate-on-rule-change="false"
size="small"
label-width="180px"
class="hcl-group-form"
>
<template v-for="fieldItem in tpl.fields">
<el-form-item
v-if="fieldItem.display"
:key="fieldItem.fieldId"
:ref="fieldItem.fieldId"
:prop="fieldItem.fieldId"
:label="fieldItem.name"
class="hcl-el-form-parse"
>
<FormItem
v-model="groupFormData[fieldItem.fieldId]"
:item="fieldItem"
:form-data="formData"
:form-type="formType"
:tpl="tpl"
@change="handleFormChange"
/>
</el-form-item>
</template>
</el-form>
</el-collapse-item>
</template>
<script>
import FormItem from "./FormItem.vue";
export default {
components: {
FormItem
},
props: {
tpl: {
type: Object,
default: () => ({}),
},
formData: {
type: Object,
default: () => ({}),
},
formType: {
type: String,
default: "create",
},
},
data() {
return {
groupFormData: {},
rules: {},
};
},
methods: {
handleFormChange() {
}
}
};
</script>
FormItem.vue:
<template>
<el-input
v-if="item.component === 'el-input'"
v-model="inpValue"
clearable
:maxlength="inpMaxLength"
:type="item.inputType || 'text'"
:placeholder="placeholder"
:disabled="inpDisabled"
@change="elInputChange"
></el-input>
<el-select
v-else-if="item.component === 'el-select'"
v-model="inpValue"
clearable
collapse-tags
:placeholder="placeholder"
:multiple="item.multiple"
:filterable="item.filterable"
:remote="item.remote"
:remote-method="selectRemoteEvt"
:disabled="inpDisabled"
@change="elSelectChange"
>
<el-option
v-for="(optionItem, index) in optionList"
:key="`${optionItem.value}_${index}`"
:label="optionItem.label"
:value="optionItem.value"
/>
</el-select>
<el-radio-group
v-else-if="item.component === 'el-radio'"
v-model="inpValue"
:disabled="inpDisabled"
@change="elRadioChange"
>
<el-radio
v-for="(radioItem, index) in optionList"
:key="`${radioItem.value}_${index}`"
:label="radioItem.value"
>
{{ radioItem.label }}
</el-radio>
</el-radio-group>
<el-checkbox-group
v-else-if="item.component === 'el-checkbox'"
v-model="inpValue"
:disabled="inpDisabled"
@change="elCheckBoxChange"
>
<el-checkbox
v-for="(checkboxItem, index) in optionList"
:key="`${checkboxItem.value}_${index}`"
:label="checkboxItem.label"
>
{{ checkboxItem.label }}
</el-checkbox>
</el-checkbox-group>
<el-date-picker
v-else-if="item.component === 'el-date-picker'"
v-model="inpValue"
type="date"
value-format="yyyy-MM-dd"
:placeholder="placeholder"
:readonly="inpReadonly"
:disabled="inpDisabled"
:picker-options="item.pickerOptions ? item.pickerOptions : {}"
@change="elInputChange"
/>
</template>
<script>
export default {
name: "",
props: {
formData: {
type: Object,
default: () => ({}),
},
item: {
type: Object,
default: () => ({}),
},
tpl: {
type: Object,
default: () => ({}),
},
value: null,
},
data() {
return {
inpValue: null,
optionList: [],
defaultInputMaxLength: 20,
defaultTextareaMaxLength: 100,
defaultPlaceholder: {
input: "请输入",
select: "请选择",
},
};
},
computed: {
inpDisabled() {
return this.item.edit === 0;
},
inpMaxLength() {
if (this.item.maxlength) {
return this.item.maxlength;
}
if (this.item.component === "el-input-textarea") {
return this.defaultTextareaMaxLength;
} else {
return this.defaultInputMaxLength;
}
},
placeholder() {
if (this.item.component === "el-input") {
return this.item.placeholder || this.defaultPlaceholder.input;
}
return this.item.placeholder || this.defaultPlaceholder.select;
},
},
watch: {
value: {
handler(val) {
this.inpValue = val;
},
immediate: true,
},
inpValue: {
handler(val) {
this.$emit("input", val);
},
immediate: true,
},
},
mounted() {
this.init();
},
methods: {
/**
* 初始化
* 默认值和options
*/
init() {
if (this.item.default !== undefined) {
this.inpValue = this.item.default;
}
const { component, options } = this.item;
switch (component) {
case "el-select":
if (this.item.multiple) {
this.inpValue = [];
}
this.optionList = options;
break;
case "el-radio":
this.optionList = options;
break;
case "el-checkbox":
this.inpValue = [];
this.optionList = options;
break;
}
},
/**
* select远程搜索
* 这里根据实际业务自行补调整
*/
selectRemoteEvt(label) {
const { searchUrl, searchField } = this.item;
const params = {};
if (label) {
params[searchField] = label;
}
request({
url: searchUrl,
method: "get",
params,
}).then((res) => {
// 根据实际数据结构进行调整
this.optionList = res;
});
},
elInputChange(val) {},
elSelectChange(val) {},
elRadioChange(val) {},
elCheckBoxChange(val) {},
},
};
</script>
调用方法
<template>
<DynamicForm
ref="dynamic-form"
:form-mode="formMode"
:form-config="formConfig"
/>
</template>
<script>
// 上面Index.vue的路径
import DynamicForm from "@/components/dynamicForm/Index.vue";
// 上面配置的配置文件路径
import formConfig from "./config.js";
export default {
components: {
DynamicForm,
},
data() {
return {
formMode: "create",
formConfig: formConfig,
};
},
};
</script>
到这里,整个动态表单的雏形已经完成了。下面进行对表单验证进行补充
新建目录rulesModel,为不同的表单类型创建规则的模型,这里以el-input为例:
export default function (origin) {
const {
required,
minLength,
maxLength
} = origin;
const rule = [];
// 读取规则
if (required === 1) {
rule.push({
required: true,
message: '请输入',
trigger: 'change',
});
}
if (minLength || maxLength) {
const ruleItem = {};
if (minLength) {
ruleItem.min = minLength;
}
if (maxLength) {
ruleItem.max = maxLength;
}
if (minLength && maxLength) {
ruleItem.message = `长度必须在${minLength}-${maxLength}之间`;
} else
if (maxLength) {
ruleItem.message = `最大长度为${maxLength}`;
} else
if (minLength) {
ruleItem.message = `最小长度为${minLength}`;
}
rule.push({
...ruleItem,
trigger: 'change'
});
}
return rule;
};
新建index.js,将所有的模型引入
import elInput from "./input";
import elSelect from "./select";
import elRadio from "./radio";
import elCheckbox from "./checkbox";
export default {
"el-input": elInput,
"el-select": elSelect,
"el-radio": elRadio,
"el-checkbox": elCheckbox,
}
ok,基本的规则已经创建完成,接下来将规则注入到表单中。
新建form-format.js,添加如下内容,这里对我们的配置文件进行解析处理
import rulesModel from "./rulesModel";
import validate from './utils/validate';
/**
* 解析表单配置
*/
function format(list = []) {
if (!Array.isArray(list)) {
return [];
}
const tplList = list.map((i) => {
const groupItem = { ...i };
const {
fields,
formData,
rules,
} = handleFormFieldList.call(this, i.fields, groupItem);
groupItem.fields = fields;
groupItem.$form = {
formData,
rules,
};
return groupItem;
});
return {
tplList
}
}
/**
* 解析配置中的每个字段
*/
function handleFormFieldList(originFields, groupItem) {
const formData = {};
const rules = {};
const fields = originFields.map(o => {
const fieldItem = { ...o };
const model = rulesModel[o.component];
if (!model) {
return fieldItem;
}
rules[o.fieldId] = model.call(this, o);
// 为每个分组的字段赋初始值
// validate.isSet是用于判断该变量是否设置了值,这里不贴源码了
formData[o.fieldId] = validate.isSet(o.default) ? o.default: '';
return fieldItem;
});
return {
formData,
fields,
rules,
}
}
export default format;
回到Index.vue,修改以下内容:
import formFormat from "./form-format.js";
created() {
// this.tplList = this.formConfig;
this.formatTpls();
},
methods: {
formatTpls() {
const { tplList } = formFormat.call(this, this.formConfig);
this.tplList = tplList;
}
}
回到GroupItem.vue,新增如下内容,将rules注入
created() {
this.init();
},
methods: {
init() {
if (!this.tpl.$form) {
return;
}
const { formData = {}, rules = {} } = this.tpl.$form;
this.groupFormData = {
...formData,
};
this.rules = {
...rules,
};
},
}
好了,到这里,整个动态表单的基本功能就已经完成了。
下一篇继续分析字段的监听和字段的控制。