1

Problem Description

In my humble opinion, writing code is actually:

  • Learning rules (see official documentation)
  • Rules of use (to further understand the official documentation in the process of use)
  • Finally, customize new rules based on the original underlying official document rules (encapsulate new rules for easy reuse)

So this article describes the idea of customizing new rules based on the original el-form rules, and attaches the code that can be used directly. Let's take a look at the renderings first:

renderings

Thought analysis

The final effect is to configure "writing code", just like echarts, writing different configurations will produce different effects, which is naturally configuration, so you must consider what needs to be configured in advance. Of course, the echo of the data should also be considered.

  • Configure the form item type (validation rules should be added to the component)
  • Configure the name of the form item
  • Configure the fields of the form item
  • Whether the configuration form item is required
  • Configure the unit of the input box (if any)
  • Configure placeholder text prompts
  • Configure the drop-down box option data data (if it is a fixed drop-down box, it can be passed in the past)
  • If it is a drop-down box of enumeration value type, you need to send a request to get the data of the drop-down box option array
    Wait...

Here to mention more form item types

Configure the form item ~ the type of the input box

First of all, we need to be clear about the types of form items. For ease of understanding, here are only three types of large types. Of course, large types also include small types, and verification is also required. As for other types, after you understand these types, you can write them yourself.

  • input box type

    • Text input box type (validation must be filled in, cannot be empty)
    • Number input box type (check the type of numbers entered, such as positive integers, two decimal places, etc.)
  • dropdown box type

    • The drop-down box type of the fixed option (write it directly here, just pass it in, such as the gender drop-down box, there are only two types of options for men and women)
    • The type of radio drop-down box that enumerates multiple options (requires a request to obtain data in advance, or a visible-change event to send a request to obtain)
    • A single-select and multiple-select drop-down box type that enumerates multiple options (same as above)
  • time picker range type

Note that the result value of the binding is an array

Finally, don't forget to echo the logic

Example of el-form header data

The child component form data is dynamically rendered according to the formHeader passed from the parent component. That is, v-for is matched with v-if to present, first briefly look at the formHeader data structure, which is available in the code behind

// 表头数组数据
      formHeader: [
        {
          itemType: "text", // 输入框类型
          labelName: "姓名", // 输入框名字
          propName: "name", // 输入框字段名
          isRequired: true, // 是否必填
          placeholder: "请填写名字", // 输入框placeholder提示语加上,可用于告知用户规则
        },
        {
          itemType: "number",
          labelName: "年龄",
          propName: "age",
          isRequired: true,
          unit: "year", // 数字类型的要有单位
          placeholder: "请输入年龄(大于0的正整数)",
        },
        {
          itemType: "selectOne", // 下拉框类型一,固定的选项可以写死在配置里,比如性别只有男女
          labelName: "性别",
          propName: "gender",
          isRequired: true,
          placeholder: "请选择性别",
          optionsArr: [
            {
              label: "男",
              value: 1,
            },
            {
              label: "女",
              value: 2,
            },
          ],
        },
      ],

full code

It is recommended to copy and paste, run and run, so that the effect is more obvious and easier to understand.
After all: no words,show codes

Parent component passes configuration data

<template>
  <div class="myWrap">
    <h2>填写表单</h2>
    <br />
    <my-form
      ref="myForm"
      :formHeader="formHeader"
      @submitForm="submitForm"
      @resetForm="resetForm"
    ></my-form>
    <h2>表单数据回显</h2>
    <el-button size="small" type="primary" @click="showData"
      >点击按钮回显数据</el-button
    >
  </div>
</template>
<script>
import myForm from "./myForm.vue";
export default {
  components: {
    myForm,
  },
  data() {
    return {
      // 表头数组数据
      formHeader: [
        /**
         * 输入框类型3种
         *    1. 普通文本输入框 text
         *    2. 数字类型输入框 number
         *    3. 文本域输入框 textarea
         *
         * 下拉框select类型2中
         *    1. 固定配置的el-option selectOne
         *    2. 枚举值的el-option单选 selectTwo
         *    2. 枚举值的el-option多选 selectThree
         *
         * 时间选择器类型1种
         *    1. 两个时间选择器、选取一个范围
         *
         * 等等,还有其他类型,这里举三种类型,别的类型仿照着即可写出来
         * 组件封装适可而止。如果是比较复杂(奇葩)的需要联动的表单,建议一个个写
         * 毕竟过度的封装,会导致代码不好维护(个人愚见)
         *
         * */
        {
          itemType: "text", // 输入框类型
          labelName: "姓名", // 输入框名字
          propName: "name", // 输入框字段名
          isRequired: true, // 是否必填
          placeholder: "请填写名字", // 输入框placeholder提示语加上,可用于告知用户规则
        },
        {
          itemType: "number",
          labelName: "年龄",
          propName: "age",
          isRequired: true,
          unit: "year", // 数字类型的要有单位
          placeholder: "请输入年龄(大于0的正整数)",
        },
        {
          itemType: "number",
          labelName: "工资",
          propName: "salary",
          isRequired: true,
          unit: "元/月", // 数字类型的要有单位
          placeholder: "请输入每月工资金额(大于0且保留两位小数)",
        },
        {
          itemType: "textarea",
          labelName: "备注",
          propName: "remark",
          isRequired: true,
          placeholder: "请填写备注",
        },
        {
          itemType: "selectOne", // 下拉框类型一,固定的选项可以写死在配置里,比如性别只有男女
          labelName: "性别",
          propName: "gender",
          isRequired: true,
          placeholder: "请选择性别",
          optionsArr: [
            {
              label: "男",
              value: 1,
            },
            {
              label: "女",
              value: 2,
            },
          ],
        },
        {
          itemType: "selectTwo", // 下拉框类型二,枚举值单选,在点击下拉选项时根据枚举id发请求,获取枚举值
          labelName: "可选职业",
          propName: "job",
          isRequired: true,
          placeholder: "请选择职业",
          enumerationId: "123123123",
        },
        {
          itemType: "selectTwo", // 下拉框类型二,枚举值单选,在点击下拉选项时根据枚举id发请求,获取枚举值
          labelName: "愿望",
          propName: "wish",
          isRequired: true,
          placeholder: "请选择愿望",
          enumerationId: "456456456",
        },
        {
          itemType: "selectThree", // 下拉框类型三,枚举值多选,在点击下拉选项时根据枚举id发请求,获取枚举值
          labelName: "爱好",
          propName: "hobby",
          isRequired: true,
          placeholder: "请选择爱好",
          enumerationId: "789789789",
        },
        {
          itemType: "selectThree", // 下拉框类型三,枚举值多选,在点击下拉选项时根据枚举id发请求,获取枚举值
          labelName: "想买手机",
          propName: "wantPhone",
          isRequired: true,
          placeholder: "请选择手机",
          enumerationId: "147258369",
        },
        {
          itemType: "dateRange", // 日期范围类型
          labelName: "日期",
          propName: "date",
          isRequired: true,
        },
      ],
    };
  },
  mounted() {
    // 数据回显的时候,要先发请求获取枚举值下拉框的值才能够正确的回显,所以
    // 就提前发请求获取对应下拉框的值了,这里要注意!注意!注意!
    this.formHeader.forEach((item) => {
      if ((item.itemType == "selectTwo") | (item.itemType == "selectThree")) {
        this.$refs.myForm.getOptionsArrData(item);
      }
    });
  },
  methods: {
    showData() {
      let apiData = {
        name: "孙悟空",
        age: 500,
        salary: 6666.66,
        remark: "齐天大圣是也",
        gender: 1, // 1代表男
        job: 1, // 1医生 2教师 3公务员
        wish: 3, // 1成为百万富翁 2长生不老 3家人健康幸福平安
        hobby: [1, 2, 3], // 1乒乓球 2羽毛球 3篮球
        wantPhone: [1, 2, 4], // 1华为 2小米 3苹果 4三星
        date: ["2018-06-06", "2022-05-05"],
      };
      setTimeout(() => {
        this.$refs.myForm.form = apiData;
      }, 300);
    },
    submitForm(form) {
      console.log("表单提交喽", form);
    },
    resetForm() {
      console.log("表单重置喽");
    },
  },
};
</script>
<style lang='less' scoped>
.myWrap {
  width: 100%;
  height: 100%;
  box-sizing: border-box;
  padding: 25px;
  overflow-y: auto;
}
</style>

The encapsulated child components are dynamically rendered according to the passed configuration data

<template>
  <div class="formWrap">
    <el-form ref="form" label-position="top" :model="form" label-width="80px">
      <template v-for="(item, index) in formHeader">
        <!-- 当类型为普通文本输入框时 -->
        <el-form-item
          v-if="item.itemType == 'text'"
          :key="index"
          :label="item.labelName"
          :prop="item.propName"
          :rules="
            item.isRequired
              ? [
                  {
                    required: true, // 是否必填 是
                    trigger: 'blur', // 触发方式,失去焦点
                    itemType: 'text', // 当前类型,文字输入框
                    labelName: item.labelName, // 当前输入框的名字
                    value: form[item.propName], // 输入框输入的绑定的值
                    validator: validateEveryData, // 校验规则函数
                  },
                ]
              : []
          "
        >
          <el-input
            :placeholder="item.placeholder"
            v-model.trim="form[item.propName]"
            clearable
            size="small"
          ></el-input>
        </el-form-item>
        <!-- 当类型为数字类型输入框时 -->
        <el-form-item
          v-if="item.itemType == 'number'"
          :key="index"
          :label="item.labelName"
          :prop="item.propName"
          :rules="
            item.isRequired
              ? [
                  {
                    required: true, // 是否必填 是
                    trigger: 'blur', // 触发方式,失去焦点
                    itemType: 'number', // 当前类型,文字输入框
                    labelName: item.labelName, // 当前输入框的名字
                    value: form[item.propName], // 输入框输入的绑定的值
                    validator: validateEveryData, // 校验规则函数
                  },
                ]
              : []
          "
        >
          <el-input
            :placeholder="item.placeholder"
            v-model.trim="form[item.propName]"
            @change="checkInput(item)"
            clearable
            size="small"
          >
            <span slot="suffix">{{ item.unit }}</span>
          </el-input>
        </el-form-item>
        <!-- 当类型为文本域输入框时 -->
        <el-form-item
          v-if="item.itemType == 'textarea'"
          :key="index"
          :label="item.labelName"
          :prop="item.propName"
          :rules="
            item.isRequired
              ? [
                  {
                    required: true, // 是否必填 是
                    trigger: 'blur', // 触发方式,失去焦点
                    itemType: 'textarea', // 当前类型,文本域输入框
                    labelName: item.labelName, // 当前输入框的名字
                    value: form[item.propName], // 输入框输入的绑定的值
                    validator: validateEveryData, // 校验规则函数
                  },
                ]
              : []
          "
        >
          <el-input
            type="textarea"
            :placeholder="item.placeholder"
            v-model.trim="form[item.propName]"
            clearable
            size="small"
          ></el-input>
        </el-form-item>
        <!-- 当类型为下拉框一时,固定下拉选项 -->
        <el-form-item
          v-if="item.itemType == 'selectOne'"
          :key="index"
          :label="item.labelName"
          :prop="item.propName"
          :rules="
            item.isRequired
              ? [
                  {
                    required: true, // 是否必填 是
                    trigger: '', // blur 或 change 这里就不指定触发方式了,保存提交时再校验
                    itemType: 'selectOne', // 当前类型,固定下拉框类型
                    labelName: item.labelName, // 当前输入框的名字
                    value: form[item.propName], // 输入框输入的绑定的值
                    validator: validateEveryData, // 校验规则函数
                  },
                ]
              : []
          "
        >
          <el-select
            v-model="form[item.propName]"
            :placeholder="item.placeholder"
            clearable
            size="small"
          >
            <el-option
              v-for="(ite, ind) in item.optionsArr"
              :key="ind"
              :label="ite.label"
              :value="ite.value"
            ></el-option>
          </el-select>
        </el-form-item>
        <!-- 当类型为下拉框二时,属于枚举值(单选)下拉框,需要根据枚举id发请求获取枚举值 -->
        <el-form-item
          v-if="item.itemType == 'selectTwo'"
          :key="index"
          :label="item.labelName"
          :prop="item.propName"
          :rules="
            item.isRequired
              ? [
                  {
                    required: true, // 是否必填 是
                    trigger: '', // blur 或 change 这里就不指定触发方式了,保存提交时再校验
                    itemType: 'selectTwo', // 当前类型,枚举值单选
                    labelName: item.labelName, // 当前输入框的名字
                    value: form[item.propName], // 输入框输入的绑定的值
                    validator: validateEveryData, // 校验规则函数
                  },
                ]
              : []
          "
        >
          <el-select
            v-model="form[item.propName]"
            :placeholder="item.placeholder"
            clearable
            @visible-change="
              (flag) => {
                getOptionsArr(flag, item);
              }
            "
            :loading="loadingSelect"
            size="small"
          >
            <el-option
              v-for="(ite, ind) in selectTwoOptionsObj[item.propName]"
              :key="ind"
              :label="ite.label"
              :value="ite.value"
            ></el-option>
          </el-select>
        </el-form-item>
        <!-- 当类型为下拉框三时,属于枚举值(多选)下拉框,需要根据枚举id发请求获取枚举值 -->
        <el-form-item
          v-if="item.itemType == 'selectThree'"
          :key="index"
          :label="item.labelName"
          :prop="item.propName"
          :rules="
            item.isRequired
              ? [
                  {
                    required: true, // 是否必填 是
                    trigger: 'blur', // 这里用blur,防止初次默认校验触发
                    itemType: 'selectThree', // 当前类型,枚举值多选
                    labelName: item.labelName, // 当前输入框的名字
                    value: form[item.propName], // 输入框输入的绑定的值
                    validator: validateEveryData, // 校验规则函数
                    type: 'number',
                  },
                ]
              : []
          "
        >
          <el-select
            v-model="form[item.propName]"
            :placeholder="item.placeholder"
            clearable
            @visible-change="
              (flag) => {
                getOptionsArr(flag, item);
              }
            "
            :loading="loadingSelect"
            multiple
            collapse-tags
            size="small"
          >
            <el-option
              v-for="(ite, ind) in selectTwoOptionsObj[item.propName]"
              :key="ind"
              :label="ite.label"
              :value="ite.value"
            ></el-option>
          </el-select>
        </el-form-item>
        <!-- 当类型为日期范围 -->
        <el-form-item
          v-if="item.itemType == 'dateRange'"
          :key="index"
          :label="item.labelName"
          :prop="item.propName"
          :rules="
            item.isRequired
              ? [
                  {
                    required: true, // 是否必填 是
                    trigger: '',
                    itemType: 'dateRange', // 当前类型,枚举值多选
                    labelName: item.labelName, // 当前输入框的名字
                    value: form[item.propName], // 输入框输入的绑定的值
                    validator: validateEveryData, // 校验规则函数
                  },
                ]
              : []
          "
        >
          <el-date-picker
            v-model="form[item.propName]"
            format="yyyy-MM-dd"
            value-format="yyyy-MM-dd"
            clearable
            type="daterange"
            range-separator="至"
            start-placeholder="开始日期"
            end-placeholder="结束日期"
            size="small"
          >
          </el-date-picker>
        </el-form-item>
      </template>
    </el-form>
    <!-- 提交表单和重置表单部分 -->
    <div class="btns">
      <el-button type="primary" @click="submitForm" size="small"
        >保存</el-button
      >
      <el-button @click="resetForm" size="small">重置</el-button>
    </div>
  </div>
</template>

<script>
export default {
  props: {
    // 父组件传递过来的表头的数据
    formHeader: {
      type: Array,
      default: () => {
        return [];
      },
    },
  },
  data() {
    var validateEveryData = (rule, value, callback) => {
      //   console.log("callback", callback);
      //   console.log("校验某一项的规则对象", rule);
      //   console.log("用户输入的值", value);

      // 对输入框类型的校验
      if (value) {
        if ((value + "").length > 0) {
          // 用于回显时候的校验,因为输入的时候是字符串类型的数字,但是回显的时候可能就是数字
          callback(); // cb函数告知校验结果,必须要加
          return;
        }
      }

      // 对下拉框类型的校验
      if (
        (rule.itemType == "selectOne") |
        (rule.itemType == "selectTwo") |
        (rule.itemType == "selectThree")
      ) {
        if (value) {
          if ((value + "").length > 0) {
            // 注意枚举值是数字类型的,所以这里要转换成为字符串类型的
            callback();
            return;
          }
        }
      }

      // 根据不同的类型给予不同的校验提示
      switch (rule.itemType) {
        case "text":
          callback(new Error(rule.labelName + "不能为空")); // 文本类型的规则简单,就是得填写
          break;
        case "number":
          callback(new Error(rule.labelName + "请按规则填写")); // 数字类型的规则比较繁多
          break;
        case "textarea":
          callback(new Error(rule.labelName + "不能为空")); // 文本域类型的规则也简单,就是得填写
          break;
        case "selectOne":
          callback(new Error("请选择" + rule.labelName)); // 下拉框类型一 得填写
          break;
        case "selectTwo":
          callback(new Error("请选择" + rule.labelName)); // 下拉框类型二 得填写
          break;
        case "selectThree":
          callback(new Error("请选择" + rule.labelName)); // 下拉框类型三 多选数组得填写
          break;
        case "dateRange":
          callback(new Error("请选择" + rule.labelName + "范围")); // 下拉框类型三 多选数组得填写
          break;

        default:
          break;
      }
    };
    return {
      // 此对象用于存储各个下拉框的数组数据值,其实也可以挂在vue的原型上,不过个人认为写在data中好些
      selectTwoOptionsObj: {},
      // 用于下拉框加载时的效果
      loadingSelect: false,
      // 绑定的数据
      form: {},
      // 校验规则
      validateEveryData: validateEveryData,
    };
  },
  methods: {
    // 获取下拉框数据
    async getOptionsArr(flag, item) {
      //   console.log(flag, item);
      // 为true时表示展开,这里模拟根据枚举值id发请求,获取下拉框的值的
      if (flag) {
        this.loadingSelect = true; // 使用了加载中效果,最好加上一个try catch捕获异常
        // let result = await this.$api.getEnumList({id:item.enumerationId})
        this.getOptionsArrData(item);
      } else {
        // 解决多选下拉框失去焦点校验规则仍然存在问题
        if (item.itemType == "selectThree") {
          //   console.log("关闭时校验多选值", this.form[item.propName]);
          if (this.form[item.propName].length > 0) {
            //  如果至少选择一个了,说明符合要求,就再校验一次,这样校验规则就去掉了
            this.$refs.form.validateField(item.propName);
          }
        }
      }
    },
    getOptionsArrData(item) {
      setTimeout(() => {
        this.loadingSelect = false;
        if (item.enumerationId == "123123123") {
          this.selectTwoOptionsObj[item.propName] = [
            {
              label: "医生",
              value: 1,
            },
            {
              label: "教师",
              value: 2,
            },
            {
              label: "公务员",
              value: 3,
            },
          ];
        }
        if (item.enumerationId == "456456456") {
          this.selectTwoOptionsObj[item.propName] = [
            {
              label: "成为百万富翁",
              value: 1,
            },
            {
              label: "长生不老",
              value: 2,
            },
            {
              label: "家人健康幸福平安",
              value: 3,
            },
          ];
        }
        if (item.enumerationId == "789789789") {
          this.selectTwoOptionsObj[item.propName] = [
            {
              label: "乒乓球",
              value: 1,
            },
            {
              label: "羽毛球",
              value: 2,
            },
            {
              label: "篮球",
              value: 3,
            },
          ];
        }
        if (item.enumerationId == "147258369") {
          this.selectTwoOptionsObj[item.propName] = [
            {
              label: "华为",
              value: 1,
            },
            {
              label: "小米",
              value: 2,
            },
            {
              label: "苹果",
              value: 3,
            },
            {
              label: "三星",
              value: 4,
            },
          ];
        }
        this.$forceUpdate(); // 这里需要强制更新一下,否则渲染不出来下拉框选项
      }, 300);
    },
    // 数字类型加校验规则
    checkInput(item) {
      console.log("数字类型的再细分规则,可以根据item.labelName再写判断", item);
      if (item.labelName == "年龄") {
        let reg = /^[1-9]\d*$/;
        if (reg.test(this.form[item.propName] * 1)) {
          // console.log("符合要求,年龄大于0的正整数");
        } else {
          this.form[item.propName] = null;
        }
      }
      if (item.labelName == "工资") {
        let reg = /^((0{1}\.\d{1,2})|([1-9]\d*\.{1}\d{1,2})|([1-9]+\d*))$/;
        if (reg.test(this.form[item.propName] * 1)) {
          // console.log("符合要求,工资保留两位小数");
          this.form[item.propName] = (this.form[item.propName] * 1).toFixed(2);
        } else {
          this.form[item.propName] = null;
        }
      }
      if ("某个数字类型字段值") {
        // 加对应规则
      }
    },
    // 保存提交表单
    async submitForm() {
      this.$refs.form.validate((valid) => {
        if (valid) {
          this.$emit("submitForm", this.form);
        } else {
          console.log("error submit!!");
          return false;
        }
      });
    },
    // 重置表单
    resetForm() {
      this.$refs.form.resetFields();
      this.form = {}; // 这里重置完了以后,要重新初始化数据,否则会出现输入不上去的问题
      this.$emit("resetForm");
    },
  },
};
</script>

<style lang='less' scoped>
.formWrap {
  width: 100%;
  /deep/ .el-form {
    display: flex;
    flex-wrap: wrap;
    justify-content: space-between;
    .el-form-item {
      width: 47%;
      margin-bottom: 12px !important;
      .el-form-item__label {
        padding: 0 !important;
        line-height: 24px !important;
      }
      .el-form-item__content {
        // 给文本域类型定高度
        .el-textarea {
          textarea {
            height: 75px !important;
          }
        }
        // 给下拉框指定宽度百分比
        .el-select {
          width: 100% !important;
        }
        // 时间选择器指定宽度百分比
        .el-date-editor {
          width: 100% !important;
          .el-range-separator {
            width: 10% !important;
          }
        }
        .el-form-item__error {
          padding-top: 1px !important;
        }
      }
    }
  }
  .btns {
    width: 100%;
    text-align: center;
    margin-top: 12px;
  }
}
</style>
A good memory is not as good as a bad writing, so record it. Criticisms and corrections are welcome ^_^

水冗水孚
1.1k 声望585 粉丝

每一个不曾起舞的日子,都是对生命的辜负