日常前端开发中,最常见的组件就是表格和表单,写一个表单的流程如下:
- 写
el-form和el-form-item标签 - 由于业务中的表单有只读和编辑状态,所以需要分别定义表单只读和编辑状态下的值显示方式。只读时,显示的值可能需要经过格式化处理,或者使用其他组件来展示。编辑时,显示的组件可能是
el-input,也可能是el-input-number,el-switch,el-checkbox,el-radio等其他组件。 - 要注意只读和编辑时的状态分离,即读写分离。
- “保存” 和 “取消保存” 时的状态修改
每个页面的每个表单的每个字段,都要这么写一遍,开发起来比较繁琐。
我们希望有一种更配置化的方式,只需要传入元数据,就能动态生成表单。
设计
我们先对目前的表单代码进行分析,发现常见的表单包括几部分内容:
- 表单项的名字
- 只读和编辑时的状态分离(读写分离)
- 只读状态下的显示内容。包括 3 种情况:直接显示、经过函数处理后显示、使用自定义组件来显示
- 编辑状态下的显示内容。常见的组件类型包括:文本,多行文本,密码,数字,开关,单选框,复选框
对于表单项,可以采用传入 formItems 数组的方式。
每个 formItem 对象定义了 prop 和 label 属性,来给 form-item 组件使用。
每个 formItem 对象也需要传入 readonly 和 edit 属性.
为了区分只读状态和编辑状态,动态表单组件需要传入参数 isEdit.
编辑时修改的数据,不能影响只读时的数据.所以需要做读写分离。为了读写分离,动态表单组件需要传入只读时的数据对象 data 和编辑时的数据对象 model,编辑时的数据对象需要做双向绑定,以便及时更新到父组件中。
动态表单组件的参数
| 属性 | 说明 | 类型 |
|---|---|---|
| isEdit | 区分只读和编辑状态 | Boolean |
| formItems | 表单项的元数据 | Array |
| data | 只读状态时的数据模型 | Object |
| model | 编辑状态时的数据模型 | Object |
IFormItem 属性
| 属性 | 说明 | 类型 |
|---|---|---|
| prop | 字段在 data 中的名字 | String |
| label | 显示的字段名 | String |
| readonly | 只读状态下需要的参数 | IReadonly |
| edit | 编辑状态下需要的参数 | IEdit |
下面讲解 IReadonly 和 IEdit 的类型定义。
只读状态时的显示情况
- 直接显示
- 经过函数处理后显示
- 使用自定义组件来显示
以上 3 种情况的处理思路如下:
- 直接
data[item.prop] - 定义
format函数;由于可能需要用到其他属性,所以传入的参数有:data中的字段值,和整个data对象;组件内部调用,把函数返回的结果显示出来 - 为了足够灵活,我们借鉴了
Vue的render函数的写法,使用JSX的方式来编写动态组件;render函数传入h函数和当前列对象 column,以及当前行对象 row;通过在template中嵌入JSX的方式来渲染
IReadonly 对象的属性如下:
| 属性 | 说明 | 类型 |
|---|---|---|
| format | 格式化函数,对原始值进行转换,返回转换后的结果 | (value,data)=>String |
| render | 渲染函数 | (h,value,data)=>JSX |
如何在 template 中嵌入 JSX 呢?
我们编写一个函数式组件 Vnodes:
{
functional: true,
render: (h, ctx) => ctx.props.vnodes,
}
在需要嵌入 JSX 的 template 中使用 Vnodes 组件
<Vnodes :vnodes="col.render($createElement, col, row)" />
编辑状态时的处理逻辑
对于常见的类型,文本,多行文本,密码,下拉框,数字,开关,单选框,复选框,可以在动态表单组件中实现。在对应的 element 组件传入需要的属性。
总结了一下 edit 中可能会用到的属性
| 类型 | 组件 | 常见参数 | 备注 |
|---|---|---|---|
| 普通文本框 | el-input | type='text' | |
| 多行文本框 | el-input | rows,type='textarea' | |
| 密码 | el-input | type='password' | |
| 数字 | el-input-number | max,min,step,type='number' | |
| 下拉框 | el-select | options,type='select' | |
| 开关 | el-switch | active-value,inactive-value,type='switch' | |
| 单选框 | el-radio | options,type='radioGroup' | options 用于动态生成选项 |
| 复选框 | el-checkbox-group | options,change,checkAll,isIndeterminate,type='checkbox' | options 用于动态生成选项 |
所以,IEdit 的参数如下:
| 属性 | 说明 | 类型 | 可选值 |
|---|---|---|---|
| prop | |||
| label | String | ||
| type | String | text,textarea,password | |
| options | Array | ||
| min | Number | ||
| max | Number | ||
| change | (val)=>void | ||
| checkAll | Boolean | ||
| isIndeterminate | Boolean |
至此,动态表格组件已经设计好了,可以开发了
开发
下面基于 Vue2 版的 Element 组件,来开发动态表单组件(基于 Vue3 版的 Element Plus 或其他组件库的实现思路类似)
读写分离的实现如下: 用两个变量保存只读和编辑时的数据
export default {
props: {
data: Object,
model: Object,
},
};
只读状态的实现如下:
<template v-if="typeof item.readonly.render === 'function'">
<Vnodes
:vnodes="item.readonly.render($createElement, data[item.prop], data)"
></Vnodes>
</template>
<template v-else-if="typeof item.readonly.format === 'function'">
{{ item.readonly.format(data[item.prop], data) }}
</template>
<template v-else> {{ getDisplay(data[item.prop], item) }} </template>
编辑状态的实现如下:
<template v-if="typeof item.edit.render === 'function'">
<Vnodes :vnodes="item.edit.render($createElement)" />
</template>
<template v-else-if="isInput(item.edit.type)">
<el-input
v-model="currentModel[item.prop]"
:type="item.edit.type"
:rows="item.edit.rows"
@change="typeof item.edit.change === 'function' ? item.edit.change : () => {}"
></el-input>
</template>
<template v-else-if="item.edit.type === 'number'">
<el-input-number
v-model="currentModel[item.prop]"
:min="item.edit.min"
:max="item.edit.max"
:step="item.edit.step"
@change="item.edit.change"
></el-input-number>
</template>
<template v-else-if="item.edit.type === 'select'">
<el-select v-model="currentModel[item.prop]" class="select">
<el-option
v-for="option of item.edit.options"
:key="option.value"
:value="option.value"
:label="option.label"
></el-option>
</el-select>
</template>
<template v-else-if="item.edit.type === 'switch'">
<el-switch
v-model="currentModel[item.prop]"
:active-value="item.edit.activeValue"
:inactive-value="item.edit.inactiveValue"
></el-switch>
</template>
<template v-else-if="item.edit.type === 'radioGroup'">
<el-radio-group v-model="currentModel[item.prop]">
<el-radio
v-for="option of item.edit.options"
:key="option.value"
:value="option.value"
:label="option.label"
:name="option.name"
></el-radio>
</el-radio-group>
</template>
测试
写了以下代码来构造动态表单
<dynamic-form
:data="data"
:model.sync="model"
:isEdit="isEdit"
:formItems="formItems"
></dynamic-form>
import DynamicForm from "./DynamicForm.vue";
export default {
components: {
DynamicForm,
},
data() {
return {
isEdit: false,
data: {
name: "test",
age: 18,
sex: "man",
desc: "描述描述",
open: 0,
password: "123456",
select: "one",
checkbox: "two",
},
model: null,
formItems: [
{
prop: "name",
label: "姓名",
readonly: {},
edit: {
type: "text",
},
},
{
prop: "desc",
label: "描述",
readonly: {},
edit: {
type: "textarea",
rows: 4,
},
},
{
prop: "age",
label: "年龄",
readonly: {},
edit: {
type: "number",
min: 1,
max: 100,
step: 1,
},
},
{
prop: "open",
label: "开关",
readonly: {
format: (value) => {
return value ? "开" : "关";
},
},
edit: {
type: "switch",
activeValue: 1,
inactiveValue: 0,
},
},
{
prop: "sex",
label: "性别",
readonly: {},
edit: {
type: "radioGroup",
options: [
{
name: "man",
value: "man",
label: "男",
},
{
name: "woman",
value: "woman",
label: "女",
},
],
},
},
{
prop: "password",
label: "密码",
readonly: {},
edit: {
type: "password",
},
},
{
prop: "select",
label: "选择框",
readonly: {},
edit: {
type: "select",
options: [
{
value: "one",
label: "一",
},
{
value: "two",
label: "二",
},
],
},
},
{
prop: "checkbox",
label: "多选",
readonly: {},
edit: {
type: "checkbox",
options: [
{
value: "one",
label: "一",
},
{
value: "two",
label: "二",
},
],
},
},
],
};
},
methods: {
changeStatus() {
if (this.isEdit) {
this.model = { ...this.data };
}
},
},
};
运行效果
只读
编辑
下一步优化的方向
未完待续