element-ui表单组件增强版,支持动态配置

799 阅读7分钟

最近做 vue2 项目,用到了 element-ui,发现 element-ui 的表单组件用起来不是很方便,每个表单项都要写一大堆代码,于是就想着封装一个表单组件,支持一键配置表单项,这样就可以减少很多重复代码。

一、看组件效果

el_form2.png

原本的 el-form,各表单项代码繁多

实现以上效果,官方文档给出的代码如下:

<template>
  <el-form
    :model="ruleForm"
    :rules="rules"
    ref="ruleForm"
    label-width="100px"
    class="demo-ruleForm"
  >
    <el-form-item label="活动名称" prop="name">
      <el-input v-model="ruleForm.name"></el-input>
    </el-form-item>
    <el-form-item label="活动区域" prop="region">
      <el-select v-model="ruleForm.region" placeholder="请选择活动区域">
        <el-option label="区域一" value="shanghai"></el-option>
        <el-option label="区域二" value="beijing"></el-option>
      </el-select>
    </el-form-item>
    <el-form-item label="活动时间" required>
      <el-col :span="11">
        <el-form-item prop="date1">
          <el-date-picker
            type="date"
            placeholder="选择日期"
            v-model="ruleForm.date1"
            style="width: 100%;"
          ></el-date-picker>
        </el-form-item>
      </el-col>
      <el-col class="line" :span="2">-</el-col>
      <el-col :span="11">
        <el-form-item prop="date2">
          <el-time-picker
            placeholder="选择时间"
            v-model="ruleForm.date2"
            style="width: 100%;"
          ></el-time-picker>
        </el-form-item>
      </el-col>
    </el-form-item>
    <el-form-item label="即时配送" prop="delivery">
      <el-switch v-model="ruleForm.delivery"></el-switch>
    </el-form-item>
    <el-form-item label="活动性质" prop="type">
      <el-checkbox-group v-model="ruleForm.type">
        <el-checkbox label="美食/餐厅线上活动" name="type"></el-checkbox>
        <el-checkbox label="地推活动" name="type"></el-checkbox>
        <el-checkbox label="线下主题活动" name="type"></el-checkbox>
        <el-checkbox label="单纯品牌曝光" name="type"></el-checkbox>
      </el-checkbox-group>
    </el-form-item>
    <el-form-item label="特殊资源" prop="resource">
      <el-radio-group v-model="ruleForm.resource">
        <el-radio label="线上品牌商赞助"></el-radio>
        <el-radio label="线下场地免费"></el-radio>
      </el-radio-group>
    </el-form-item>
    <el-form-item label="活动形式" prop="desc">
      <el-input type="textarea" v-model="ruleForm.desc"></el-input>
    </el-form-item>
    <el-form-item>
      <el-button type="primary" @click="submitForm('ruleForm')"
        >立即创建</el-button
      >
      <el-button @click="resetForm('ruleForm')">重置</el-button>
    </el-form-item>
  </el-form>
</template>
<script>
export default {
  data() {
    return {
      ruleForm: {
        name: '',
        region: '',
        date1: '',
        date2: '',
        delivery: false,
        type: [],
        resource: '',
        desc: '',
      },
      rules: {
        name: [
          { required: true, message: '请输入活动名称', trigger: 'blur' },
          { min: 3, max: 5, message: '长度在 3 到 5 个字符', trigger: 'blur' },
        ],
        region: [
          { required: true, message: '请选择活动区域', trigger: 'change' },
        ],
        date1: [
          {
            type: 'date',
            required: true,
            message: '请选择日期',
            trigger: 'change',
          },
        ],
        date2: [
          {
            type: 'date',
            required: true,
            message: '请选择时间',
            trigger: 'change',
          },
        ],
        type: [
          {
            type: 'array',
            required: true,
            message: '请至少选择一个活动性质',
            trigger: 'change',
          },
        ],
        resource: [
          { required: true, message: '请选择活动资源', trigger: 'change' },
        ],
        desc: [{ required: true, message: '请填写活动形式', trigger: 'blur' }],
      },
    };
  },
  methods: {
    submitForm(formName) {
      this.$refs[formName].validate((valid) => {
        if (valid) {
          alert('submit!');
        } else {
          console.log('error submit!!');
          return false;
        }
      });
    },
    resetForm(formName) {
      this.$refs[formName].resetFields();
    },
  },
};
</script>

增强版的 el-form - 去掉表单项组件代码,改成schema动态配置

增强版的 el-form,只需要配置表单项的 schema,就可以实现以上效果,代码如下:

<template>
  <!-- <el-form :model="ruleForm" :rules="rules" ref="ruleForm" label-width="100px" class="demo-ruleForm" > 增加了schema 去掉了rules 去掉了内部的表单项 -->
  <EnhancedElForm
    :model="formData"
    :schema="schema"
    ref="ruleForm"
    label-width="100px"
    class="demo-ruleForm"
  >
    <el-form-item>
      <el-button type="primary" @click="submitForm('ruleForm')"
        >立即创建</el-button
      >
      <el-button @click="resetForm('ruleForm')">重置</el-button>
    </el-form-item>
  </EnhancedElForm>
</template>
<script>
export default {
  data() {
    return {
      // 不变
      ruleForm: {
        name: '',
        region: '',
        date1: '',
        date2: '',
        delivery: false,
        type: [],
        resource: '',
        desc: '',
      },
      // 表单项配置 rules合并在单个表单项的配置中
      schema: [
        // 输入框
        // <el-form-item label="活动名称" prop="name"> <el-input v-model="ruleForm.name"></el-input> </el-form-item>
        {
          component: 'el-input',
          modelKey: 'name',
          label: '活动名称',
          // 验证规则
          rules: [
            { required: true, message: '请输入活动名称', trigger: 'blur' },
            {
              min: 3,
              max: 5,
              message: '长度在 3 到 5 个字符',
              trigger: 'blur',
            },
          ],
        },
        // 选择器
        //  <el-form-item label="活动区域" prop="region"> <el-select v-model="ruleForm.region" placeholder="请选择活动区域"> <el-option label="区域一" value="shanghai"></el-option> <el-option label="区域二" value="beijing"></el-option> </el-select> </el-form-item>
        {
          component: 'el-select',
          modelKey: 'region',
          label: '活动区域',
          props: {
            placeholder: '请选择活动区域',
            options: [
              {
                value: 'shanghai',
                label: '区域一',
              },
              {
                value: 'beijing',
                label: '区域二',
              },
            ],
          },
          rules: [
            { required: true, message: '请选择活动区域', trigger: 'change' },
          ],
        },
        // 日期选择器 以及 同一个label对应多个输入框
        // <el-form-item label="活动时间" required> <el-col :span="11"> <el-form-item prop="date1"> <el-date-picker type="date" placeholder="选择日期" v-model="ruleForm.date1" style="width: 100%;"></el-date-picker> </el-form-item> </el-col> <el-col class="line" :span="2">-</el-col> <el-col :span="11"> <el-form-item prop="date2"> <el-time-picker placeholder="选择时间" v-model="ruleForm.date2" style="width: 100%;"></el-time-picker> </el-form-item> </el-col> </el-form-item>
        {
          label: '活动时间',
          props: {
            required: true,
          },
          cols: [
            {
              span: 11,
              component: 'el-date-picker',
              modelKey: 'date1',
              props: {
                placeholder: '选择日期',
              },
              rules: [
                { required: true, message: '请选择日期', trigger: 'change' },
              ],
            },
            {
              span: 2,
              class: 'line',
              text: '-',
            },
            {
              span: 11,
              component: 'el-time-picker',
              modelKey: 'date2',
              props: {
                placeholder: '选择时间',
              },
              rules: [
                { required: true, message: '请选择时间', trigger: 'change' },
              ],
            },
          ],
        },
        // 开关
        // <el-form-item label="即时配送" prop="delivery"> <el-switch v-model="ruleForm.delivery"></el-switch> </el-form-item>
        {
          component: 'el-switch',
          modelKey: 'delivery',
          label: '即时配送',
        },
        // 复选框
        //  <el-form-item label="活动性质" prop="type"> <el-checkbox-group v-model="ruleForm.type"> <el-checkbox label="美食/餐厅线上活动" name="type"></el-checkbox> <el-checkbox label="地推活动" name="type"></el-checkbox> <el-checkbox label="线下主题活动" name="type"></el-checkbox> <el-checkbox label="单纯品牌曝光" name="type"></el-checkbox> </el-checkbox-group> </el-form-item>
        {
          component: 'el-checkbox-group',
          modelKey: 'type',
          label: '活动性质',
          props: {
            options: [
              {
                value: '美食/餐厅线上活动',
                label: '美食/餐厅线上活动',
              },
              {
                value: '地推活动',
                label: '地推活动',
              },
              {
                value: '线下主题活动',
                label: '线下主题活动',
              },
              {
                value: '单纯品牌曝光',
                label: '单纯品牌曝光',
              },
            ],
          },
          rules: [
            {
              type: 'array',
              required: true,
              message: '请至少选择一个活动性质',
              trigger: 'change',
            },
          ],
        },
        // 单选框
        // <el-form-item label="特殊资源" prop="resource"> <el-radio-group v-model="ruleForm.resource"> <el-radio label="线上品牌商赞助"></el-radio> <el-radio label="线下场地免费"></el-radio> </el-radio-group> </el-form-item>
        {
          component: 'el-radio-group',
          modelKey: 'resource',
          label: '特殊资源',
          props: {
            options: [
              {
                value: '线上品牌商赞助',
                label: '线上品牌商赞助',
              },
              {
                value: '线下场地免费',
                label: '线下场地免费',
              },
            ],
          },
          rules: [
            { required: true, message: '请选择特殊资源', trigger: 'change' },
          ],
        },
        // 文本区域输入框
        // <el-form-item label="活动形式" prop="desc"> <el-input type="textarea" v-model="ruleForm.desc"></el-input> </el-form-item>
        {
          component: 'el-input',
          modelKey: 'desc',
          label: '活动形式',
          props: {
            type: 'textarea',
          },
          rules: [
            { required: true, message: '请填写活动形式', trigger: 'blur' },
          ],
        },
      ],
    };
  },
  methods: {
    // ...不变
  },
};
</script>

二、EnhancedElForm组件实现

2.1 基本思路

  1. 通过 schema 配置表单项,schema 是一个数组,每个元素代表一个表单项的配置。
  2. 通过 v-for 遍历 schema,根据配置生成对应的表单项。
  3. 表单项的配置包括:component、modelKey、label、rules、props、cols等属性。
    • 通过 component 区分不同的表单项,如 el-input、el-select、el-switch、el-checkbox-group、el-radio-group 等
    • 通过 modelKey 绑定表单项的值
    • 通过 label 设置表单项的标签
    • 通过 rules 配置表单项的验证规则
    • 通过 props 配置表单组件上的属性。
    • 通过 cols 配置同一个 label 对应多个属性的情况

2.2 实现逻辑

新建 EnhancedElForm.vue 组件,接收 modelschema 两个 propsmodel 是表单数据,schema 是表单项配置。在组件中遍历 schema,根据配置生成对应的表单项,同时将 rulesschema 中提取出来,传递给 el-form 组件。

2.3 基础实现

先不考虑复杂的情况,
表单其他属性和事件用v-bindv-on直接传递给 el-form 组件。
model 直接放在 form 上,schema 遍历生成每个表单项。
表单每项都是el-form-item,只是内部有所区分,使用component组件,表单项本身的属性和使用通过propslisteners进行直接传递。 所以 EnhancedElForm.vue 组件如下:

<template>
  <el-form :model="model" v-bind="$attrs" v-on="$listeners">
    <template v-for="config in schema">
      <el-form-item
        :label="config.label"
        :prop="config.modelKey"
        :key="config.modelKey"
      >
        <component
          :is="config.component"
          v-model="model[config.modelKey]"
          v-bind="config.props"
          v-on="config.listeners"
        />
      </el-form-item>
    </template>
    <slot name="default"></slot>
  </el-form>
</template>
<script>
export default {
  name: 'enhanced-el-form',
  props: {
    model: {
      type: Object,
      default() {
        return {};
      },
    },
    schema: {
      type: Array,
      default() {
        return {};
      },
    },
  },
};
</script>

2.4 增加选项类组件

对于 el-select、el-checkbox-group、el-radio-group 这类组件,因为内部实现不太一样,但是希望使用的时候,统一配置 options 就行,所以需要区分这几个组件。

<template>
  <!-- 不变 -->
  <component
    :is="config.component"
    v-model="model[config.modelKey]"
    v-bind="config.props"
    v-on="config.listeners"
  >
    <template v-if="config.component === 'el-radio-group'">
      <el-radio
        v-for="(item, index) in config.props.options"
        :key="index"
        :label="typeof item === 'object' ? item.value : item"
        >{{ typeof item === 'object' ? item.label : item }}</el-radio
      >
    </template>
    <template v-else-if="config.component === 'el-checkbox-group'">
      <el-checkbox
        v-for="(item, index) in config.props.options"
        :key="index"
        :label="typeof item === 'object' ? item.value : item"
        >{{ typeof item === 'object' ? item.label : item }}</el-checkbox
      >
    </template>
    <template v-else-if="config.component === 'el-select'">
      <el-option
        v-for="(item, index) in config.props.options"
        :key="index"
        :value="typeof item === 'object' ? item.value : item"
        :label="typeof item === 'object' ? item.label : item"
      ></el-option>
    </template>
  </component>
  <!-- 不变 -->
</template>

2.5 增加验证规则

将 rules 从 schema 中提取出来,传递给 el-form 组件。

<template>
  <el-form :model="model" :rules="rules" v-bind="$attrs" v-on="$listeners">
    <!-- 不变 -->
  </el-form>
</template>
<script>
export default {
  // ...
  computed: {
    computed: {
      rules() {
        return this.schema.reduce((acc, cur) => {
          acc[cur.modelKey] = cur.rules;
          return acc;
        }, {});
      },
    },
  },
};
</script>

2.6 增加 cols 配置

对于一些特殊的组件,比如日期选择器,需要配置多个组件,这时候需要使用 cols 配置。 或者对于一些表单项,需要多个组件,比如同一个 label 对应多个输入框,这时候也需要使用 cols 配置。 这里考虑到 cols 的时候,component 可能没有值,所以默认加个 div。

<template>
  <!-- 不变 -->
  <component
    :is="config.component || 'div'"
    v-model="model[config.modelKey]"
    v-bind="config.props"
    v-on="config.listeners"
  >
    <template v-if="config.cols">
      <el-col v-for="(col, index) in config.cols" :key="index" :span="col.span">
        <component
          v-if="col.component"
          :is="col.component"
          v-model="model[col.modelKey]"
          v-bind="col.props"
          v-on="col.listeners"
        />
        {{ col.text }}
      </el-col>
    </template>
  </component>
  <!-- 不变 -->
</template>

2.7 增加 自定义组件

对于一些需要自定义组件,封装的时候,props 需要 value 值,值发生变化的时候,需要触发 input 事件,使用 v-model 进行双向绑定。配置的时候,component 设置为自定义组件的名称即可。

比如定义一个输入名字的NameInput组件:

<template>
  <input
    type="text"
    :value="innerValue"
    @input="
      ($event) => {
        innerValue = $event.target.value;
      }
    "
    v-bind="$attrs"
  />
</template>
<script>
export default {
  name: 'NameInput',
  props: {
    value: {
      type: String,
      required: true,
    },
  },
  computed: {
    innerValue: {
      get() {
        return this.value;
      },
      set(value) {
        this.$emit('input', value);
      },
    },
  },
};
</script>

使用的时候,只需要配置 component 为NameInput即可。

import NameInput from './NameInput.vue';
const schema = [
  {
    component: NameInput,
    modelKey: 'name',
    label: '姓名',
    rules: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
  },
];

如果不想单独抽离成一个组件,也可以使用 slot 来处理。

<template>
  <slot v-if="config.slotName" :name="config.slotName" v-bind="config"></slot>
  <component v-else ... >
</template>

使用的时候,要配置 slotName,然后在组件内部写逻辑。

const schema = [
  {
    slotName: 'username',
    label: '姓名',
    rules: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
  },
];

组件中使用slot,并且传递配置。

<template>
  <EnhancedElForm
    :model="formData"
    :schema="schema"
    ref="dataForm"
    label-width="100px"
    class="demo-ruleForm"
  >
    <template #username="config">
      <el-input v-model="formData[config.modelKey]"></el-input>
    </template>
  </EnhancedElForm>
</template>

2.7 直接访问 el-form 的方法

通常来说, 想要访问 el-form 的方法,是需要通过定义在EnhancedElForm组件上的ref,然后通过ref访问到EnhancedElForm组件,再通过$refs访问到ElForm组件上的方法。比如访问validate方法,需要这样:

this.$refs.ruleForm.$refs.elForm.validate((valid) => {
  // ...
});

怎么能够直接访问到el-form的方法呢?直接将el-form的方法暴露出来,这样就可以直接访问到el-form的方法了。下次访问就是this.$refs.ruleForm.validate

<template>
  <el-form
    ref="elForm"
    :model="model"
    :rules="rules"
    v-bind="$attrs"
    v-on="$listeners"
  >
    <!-- 不变 -->
  </el-form>
</template>
<script>
export default {
  // ...
  methods: {
    // 等同于 validate(...args) { return this.$refs.elForm.validate(...args) },...
    ...['validate', 'validateField', 'resetFields', 'clearValidate'].reduce(
      (methods, method) => {
        methods[method] = function (...args) {
          return this.$refs.elForm && this.$refs.elForm[method](...args);
        };
        return methods;
      },
      {}
    ),
  },
};
</script>

2.8 配置查询表单

后台系统中,查询表单通常是横向的,且会有查询和重置按钮,这里通过属性isQueryForm来控制是否是查询表单。

<template>
  <el-form :inline="isQueryForm" ..>
    >
    <!-- 不变 -->
    <el-form-item class="query-form-buttons" v-if="isQueryForm">
      <el-button type="primary" @click="$emit('query')">查询</el-button>
      <el-button v-if="showReset" @click="$emit('reset')">重置</el-button>
      <slot name="default" />
    </el-form-item>
    <slot v-else name="default" />
  </el-form>
</template>
<script>
export default {
  props: {
    isQueryForm: {
      type: Boolean,
      default: false,
    },
    showReset: {
      type: Boolean,
      default: true,
    },
  },
};
</script>

2.9 EnhancedElForm的全部代码

<template>
  <el-form
    ref="elForm"
    :model="model"
    :rules="rules"
    :inline="isQueryForm"
    v-bind="$attrs"
    v-on="$listeners"
  >
    <template v-for="config in schema">
      <el-form-item
        :label="config.label"
        :prop="config.modelKey"
        :key="config.modelKey"
      >
        <slot
          v-if="config.slotName"
          :name="config.slotName"
          v-bind="config"
        ></slot>
        <component
          v-else
          :is="config.component || 'div'"
          v-model="model[config.modelKey]"
          v-bind="config.props"
          v-on="config.listeners"
        >
          <template v-if="config.cols">
            <el-col
              v-for="(col, index) in config.cols"
              :key="index"
              :span="col.span"
            >
              <component
                v-if="col.component"
                :is="col.component"
                v-model="model[col.modelKey]"
                v-bind="col.props"
                v-on="col.listeners"
              />
              {{ col.text }}
            </el-col>
          </template>
          <template v-else-if="config.component === 'el-radio-group'">
            <el-radio
              v-for="(item, index) in config.props.options"
              :key="index"
              :label="typeof item === 'object' ? item.value : item"
              >{{ typeof item === 'object' ? item.label : item }}</el-radio
            >
          </template>
          <template v-else-if="config.component === 'el-checkbox-group'">
            <el-checkbox
              v-for="(item, index) in config.props.options"
              :key="index"
              :label="typeof item === 'object' ? item.value : item"
              >{{ typeof item === 'object' ? item.label : item }}</el-checkbox
            >
          </template>
          <template v-else-if="config.component === 'el-select'">
            <el-option
              v-for="(item, index) in config.props.options"
              :key="index"
              :value="typeof item === 'object' ? item.value : item"
              :label="typeof item === 'object' ? item.label : item"
            ></el-option>
          </template>
        </component>
      </el-form-item>
    </template>
    <el-form-item class="query-form-buttons" v-if="isQueryForm">
      <el-button type="primary" @click="$emit('query')">查询</el-button>
      <el-button v-if="showReset" @click="$emit('reset')">重置</el-button>
      <slot name="default" />
    </el-form-item>
    <slot v-else name="default" />
  </el-form>
</template>
<script>
export default {
  name: 'enhanced-el-form',
  props: {
    model: {
      type: Object,
      default() {
        return {};
      },
    },
    schema: {
      type: Array,
      default() {
        return {};
      },
    },
    isQueryForm: {
      type: Boolean,
      default: false,
    },
    showReset: {
      type: Boolean,
      default: true,
    },
  },
  computed: {
    rules() {
      return this.schema.reduce((acc, cur) => {
        acc[cur.modelKey] = cur.rules;
        return acc;
      }, {});
    },
  },
  methods: {
    ...['validate', 'validateField', 'resetFields', 'clearValidate'].reduce(
      (methods, method) => {
        methods[method] = function (...args) {
          return this.$refs.elForm && this.$refs.elForm[method](...args);
        };
        return methods;
      },
      {}
    ),
  },
};
</script>

三、实战应用

3.1 新建和编辑表单

el_form2.png

<template>
  <EnhancedElForm
    :model="formData"
    :schema="schema"
    ref="dataForm"
    label-width="100px"
    class="demo-ruleForm"
  >
    <el-form-item>
      <el-button type="primary" @click="submitForm">立即创建</el-button>
      <el-button @click="resetForm">重置</el-button>
    </el-form-item>
  </EnhancedElForm>
</template>
<script>
import EnhancedElForm from './EnhancedElForm.vue';
export default {
  data() {
    return {
      formData: {
        name: '',
        region: '',
        date1: '',
        date2: '',
        delivery: false,
        type: [],
        resource: '',
        desc: '',
      },
      schema: [] /** schema懒得重复写了 */,
    };
  },
  methods: {
    submitForm() {
      this.$refs.dataForm.validate((valid) => {
        if (valid) {
          alert('submit!');
        } else {
          console.log('error submit!!');
          return false;
        }
      });
    },
    resetForm() {
      this.$refs.dataForm.resetFields();
    },
  },
};
</script>

3.2 查询表单

el_form1.png

<template>
  <EnhancedElForm
    :model="queryData"
    :schema="querySchema"
    isQueryForm
    ref="dataForm"
    @query="query"
    @reset="reset"
  >
      <el-button>创建桌面</el-button>
  </EnhancedElForm>
</template>
<script>
import EnhancedElForm from './EnhancedElForm.vue';
export default {
  data() {
    return {
      formData: {
        name: '',
        state: '',
      },
      schema: []/** schema懒得重复写了 */,
    };
  },
  methods: {
    query() {
      console.log('query');
    },
    reset() {
      console.log('reset');
    },
  },
};