基于Vue设计一个动态表单

864 阅读4分钟

日常前端开发中,最常见的组件就是表格和表单,写一个表单的流程如下:

  1. el-formel-form-item 标签
  2. 由于业务中的表单有只读和编辑状态,所以需要分别定义表单只读和编辑状态下的值显示方式。只读时,显示的值可能需要经过格式化处理,或者使用其他组件来展示。编辑时,显示的组件可能是 el-input,也可能是 el-input-number,el-switchel-checkbox,el-radio 等其他组件。
  3. 要注意只读和编辑时的状态分离,即读写分离。
  4. “保存” 和 “取消保存” 时的状态修改

每个页面的每个表单的每个字段,都要这么写一遍,开发起来比较繁琐。

我们希望有一种更配置化的方式,只需要传入元数据,就能动态生成表单。

设计

我们先对目前的表单代码进行分析,发现常见的表单包括几部分内容:

  1. 表单项的名字
  2. 只读和编辑时的状态分离(读写分离)
  3. 只读状态下的显示内容。包括 3 种情况:直接显示、经过函数处理后显示、使用自定义组件来显示
  4. 编辑状态下的显示内容。常见的组件类型包括:文本,多行文本,密码,数字,开关,单选框,复选框

对于表单项,可以采用传入 formItems 数组的方式。

每个 formItem 对象定义了 proplabel 属性,来给 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

下面讲解 IReadonlyIEdit 的类型定义。

只读状态时的显示情况
  1. 直接显示
  2. 经过函数处理后显示
  3. 使用自定义组件来显示

以上 3 种情况的处理思路如下:

  1. 直接 data[item.prop]
  2. 定义 format 函数;由于可能需要用到其他属性,所以传入的参数有:data 中的字段值,和整个 data 对象;组件内部调用,把函数返回的结果显示出来
  3. 为了足够灵活,我们借鉴了 Vuerender函数的写法,使用 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,
    }

在需要嵌入 JSXtemplate 中使用 Vnodes 组件

<Vnodes :vnodes="col.render($createElement, col, row)" />
编辑状态时的处理逻辑

对于常见的类型,文本,多行文本,密码,下拉框,数字,开关,单选框,复选框,可以在动态表单组件中实现。在对应的 element 组件传入需要的属性。

总结了一下 edit 中可能会用到的属性

类型组件常见参数备注
普通文本框el-inputtype='text'
多行文本框el-inputrows,type='textarea'
密码el-inputtype='password'
数字el-input-numbermax,min,step,type='number'
下拉框el-selectoptions,type='select'
开关el-switchactive-value,inactive-value,type='switch'
单选框el-radiooptions,type='radioGroup'options 用于动态生成选项
复选框el-checkbox-groupoptions,change,checkAll,isIndeterminate,type='checkbox'options 用于动态生成选项

所以,IEdit 的参数如下:

属性说明类型可选值
prop
labelString
typeStringtext,textarea,password
optionsArray
minNumber
maxNumber
change(val)=>void
checkAllBoolean
isIndeterminateBoolean

至此,动态表格组件已经设计好了,可以开发了

开发

下面基于 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 };
      }
    },
  },
};

运行效果

只读

20240616_205959_image.png

编辑

20240616_205924_image.png

下一步优化的方向

未完待续