Vue动态表单组件的一点点小想法

3,922 阅读3分钟

Vue动态表单组件封装

本文章基于Vue2版本,使用的UI库为 elementUI。源于日常开发。

使用到的Vue技巧:

  1. 定义v-model
  2. <component is="componentName"></component> 动态组件
  3. v-onv-bind$attrs$listeners、透传attribute
  4. slot插槽

1、关于组件的猜想

 <my-component config="config"></my-component>

对于一个完美的组件,如上代码所示:丢入一堆config配置,组件输出我想要的页面。

1676516217126.png

那么对于一个表单组件,会需要什么呢?
基于elementUI官网中Form组件的第一个实例进行分析。 1676516277759.jpg
得出结论

  1. 表单左侧的文字每一行左侧的文字:得出属性label。
  2. 表单组件的渲染如图中的 el-input、el-select、el-radio等组件的名称:属性component
  3. 表单中 el-input、el-select、el-radio-group等组件双向绑定的值: 属性key。

(el-checkbox-group 或 el-radio-group) 类的组件,尽量使用组合的模式便于双向绑定

基于最简单的需求,总结出:

// 数据模型
 const config = [
    {
        label: "活动名称",
        component: "el-input",
        key: "name",
    },
    {
        label: "活动区域",
        component: "el-select",
        key: "area",
    },
]
// 组件使用
<my-form config="config"></my-component>
<template>
    <el-form class="dynamic-form" ref="form" :model="formModel" label-width="80px">
        <el-form-item v-for="(item, idx) in config" :key="idx" :label="item.label" :prop="item.key">
            <el-input v-model="formModel[item.key]" v-if="item.component === 'el-input'"></el-input>
            <el-select v-model="formModel[item.key]" v-if="item.component === 'el-select'"></el-select>
        </el-form-item>
    </el-form>
</template>

<script>
export default {
    name: 'DynamicForm',
    props: {
        config: {
            type: Array,
            default: () => []
        }
    },
    data() {
        return {
            formModel: {
                name: '',
                area: ''
            }
        }
    }
}
</script>

收获页面渲染结果如下: 1676537480111.jpg

基于以上的输出结果得出以下痛点:

  1. props参数只读,v-model需要内部变量去处理(这里指formModel要先定义好变量)。
  2. 使用v-for + v-if的判断去处理,如果思考缺少了部分组件,需要在组件内追加,繁琐。
  3. input、select等组件不添加参数。
  4. 组件与外部没有通信。
  5. 表单没办法添加校验
  6. 数据没办法回填
    ............

2、二次分析功能

基于上一节的痛点,对于组件的需求进行二次分析。

  1. 表单组件的结果要在外部方便获取。
    需要在外部修改数值时回填到组件内部 (添加自定义v-model)
  2. input、select等组件不能添加参数,
    el-form-item、el-form也需要参数配置的添加。
    (v-on, v-bind的批量绑定 以及透传Attributes)。
  3. 组件内部需要写大量的判断当前组件是什么类型,考虑不足是会造成后续组件的追加。(Vue动态组件解决) 1676537323360.jpg

由此展开第二轮配置信息数据:

属性字段
labellabel值
key需要绑定的内容
slot具名插槽
component组件名称
options列表数据: 如 el-select、el-cascader 需要使用到的子节点数据
formItemAttr表单item事件
formItemEven表单item属性
componentAttr表单控件属性
componentEvent表单控件事件

3、产出

组件使用部分

<template>
  <div>
    <MYform style="margin:60px" label-width="100px" v-model="formData" :config="config">
      <template #slider>
        <el-slider v-model="formData.slot"></el-slider>
      </template>
    </MYform>
</div>
</template>

<script>
import MYform from "./components/myForm.vue"
export default {
  name: "app",
  components: {
    MYform
  },
  data() {
    return {
      formData: {}
    };
  },
  mounted() {
  },
  computed: {
    config() {
      return [
        {
          label: "活动名称", // label值
          key: "name", // 需要绑定的内容
          component: "el-input", // 组件名称
          formItemAttr: {
            rules: [{ required: true, message: '请输入邮箱地址', trigger: 'blur' }],
          },  // 表单item属性
          formItemEven: {}, // 表单item事件
          componentAttr: {
            clearable: true,
            prefixIcon: 'el-icon-search',
          }, // 表单控件属性
          componentEvent: {},
        },
        {
          label: "活动内容", // label值
          key: "type", // 需要绑定的内容
          component: "el-select", // 组件名称
          options: [{ label: "活动1", value: 1 }, { label: "活动2", value: 2 }],
          formItemAttr: {},  // 表单item属性
          formItemEven: {}, // 表单item事件
          componentAttr: {
            clearable: true,
          }, // 表单控件属性
          componentEvent: {},// 表单控件事件
        }, {
          label: "使用slot", // label值
          key: "slot", // 需要绑定的内容
          slot: "slider",
          formItemAttr: {},  // 表单item属性
          formItemEven: {}, // 表单item事件
          componentAttr: {
            clearable: true,
          }, // 表单控件属性
          componentEvent: {},// 表单控件事件
        }
      ]
    }
  },
};
</script>

最终输出的结果如下: 动画1.gif 组件代码:

<template>
<!-- v-bind="$attrs" 用于 透传属性的接收 v-on="$listeners" 方法的接收 -->
  <el-form 
      class="dynamic-form"
      ref="form"
      v-bind="$attrs" 
      v-on="$listeners"
      :model="formModel">
    <el-form-item 
        v-for="(item, idx) in config" 
        :key="idx" :label="item.label" 
        :prop="item.key"
        v-bind="item.formItemAttr">
      <!-- 具名插槽 -->
      <slot v-if="item.slot" :name="item.slot"></slot>
      <!-- 1、动态组件(用于取代遍历判断。 is直接赋值为组件的名称即可) -->
      <component v-else :is="item.component" 
        v-model="formModel[item.key]" 
        v-bind="item.componentAttr"
        v-on="item.componentEvent" 
        @change="onUpdate"
       >
        <!-- 单独处理 select 的options(当然也可以基于 el-select进行二次封装,去除options遍历这一块 ) -->
        <template v-if="item.component === 'el-select'">
          <el-option v-for="option in item.options" :key="option.value" :label="option.label" :value="option.value">
          </el-option>
        </template>
      </component>
       <!-- 默认插槽 -->
      <slot></slot>
    </el-form-item>
</el-form>
</template>

<script>
export default {
  name: 'MyForm',
  props: {
    config: {
      type: Array,
      default: () => []
    },
    modelValue: {}
  },
  model: {
    prop: 'modelValue', // v-model绑定的值,因为v-model也是props传值,所以props要存在该变量
    event: 'change' // 需要在v-model绑定的值进行修改时的触发事件。
  },
  computed: {

  },
  data() {
    return {
      formModel: {},
    }
  },
  watch: {
    // v-model的值发生改变时,同步修改form内部的值
    modelValue(val) {
      // 更新formModel
      this.updateFormModel(val);
    },
  },
  created() {
    // 初始化
    this.initFormModel();
  },
  methods: {
    // 初始化表单数值
    initFormModel() {
      let formModelInit = {};
      this.config.forEach((item) => {
        // el-checkbox-group 必须为数组,否则会报错
        if (item.componentName === "el-checkbox-group") {
          formModelInit[item.key] = [];
        } else {
          formModelInit[item.key] = null;
        }
      });
      this.formModel = Object.assign(formModelInit, this.modelValue);
      this.onUpdate();
    },
    // 更新内部值
    updateFormModel(modelValue) {
      // 合并初始值和传入值
      const sourceValue = modelValue ? modelValue : this.formModel;
      this.formModel = Object.assign(this.formModel, sourceValue);
    },
    onUpdate() {
      // 触发v-model的修改
      this.$emit("change", this.formModel);
    },
  },
};
</script>

4、结束

当然,动态组件并不是万能的,但是可以减少CV,以上代码只是一个概念篇的思想输出。但是在一定程度上也能够使用。
对于组件的完善,还是需要个人喜好来处理。
比如说:

  1. 添加methods的方法,像element一样 this.$refs[formName].resetFields(); 去重置数据或清空校验。(当然有了v-model, 其实可以直接修改v-model的值也可以完成重置数据)。
  2. 对el-select进一步封装,就可以避免去写 el-options 的遍历判断。
  3. el-checkbox-group、el-radio-group 这类型的组件尽量不使用单个的,用group便于双向绑定。
  4. el-checkbox-group、el-radio-group也可以进一步的进行封装,通过添加options配置的方式,去除内部额外添加 v-for的遍历。
  5. 还可以添加el-row、el-col的layout布局。
  6. 还有添加 form-item 的显示隐藏
  7. 甚至还可以把数据进行抽离成JSON的格式。
    ........