基于el-form智能的FormSmart表单组件

1,755 阅读4分钟

目的

  • el-form 表单配置化,后期可以利用后端进行配置表单
  • 支持栅格布局
  • el-form 属性大部分都支持
  • 公共配置可以统一处理,如trim、input宽度等
  • 最基本的组件,利用这组件后期可以优化简单的新增编辑弹窗写法
  • 【disable,hide,rules】是可以写方法的!相当于可以动态控制隐藏显示,禁用启用,和动态规则
  • row col formItem 都支持slotName

源码

<script>
export default {
    props: {
        model: {
            type: Object,
            default: () => { },
        },
        itemWidth: {
            type: String,
            default: '200px',
        },
        rowItems: {
            type: Array,
            default: () => [],
        },
        formItems: {
            type: Array,
            default: () => [],
        },
    },
    data() {
        return {
        };
    },

    mounted() {
        // 默认选中光标,设置ref=focus
        this.generateModel();
        this.focus();
        // 重新覆盖方法
        this.$nextTick(() => {
            const entries = Object.entries(this.$refs.form);
            for (const [key, value] of entries) {
                if (['clearValidate', 'validate', 'validateField'].includes(key)) {
                    this[key] = value;
                }
            }
        });
    },
    methods: {
        resetFields() {
            // 重置
            this.$refs.form.resetFields();
        },
        // 处理v-modelv value 值
        hanlderValue(prop) {
            // 对数组返回类型的组件处理
            if (Array.isArray(prop) && prop.length > 0) {
                const _key = prop.join('|');
                return this.model[_key];
            }
            // 处理|下划线的
            return this.model[prop];
        },
        // 处理FormItem绑定值
        hanlderFormItemProp(prop) {
            // 时间范围选择组件处理
            if (Array.isArray(prop) && prop.length > 0) {
                // 处理时间范围选择的类型数组的
                return prop.join('|');
            }
            return prop;
        },
        // 处理改变值
        hanlderModel(prop, value) {
            // 时间范围选择组件处理
            if (prop && Array.isArray(prop)) {
                if (Array.isArray(value)) {
                    // 数组对应值
                    prop.forEach((key, index) => {
                        this.$set(this.model, key, value && value.length > 0 ? value[index] : null);
                    });
                } else if ([null, '', undefined].includes(value)) {
                    // 数组有清除按钮时,数组一起清理
                    prop.forEach((key) => {
                        this.$set(this.model, key, undefined);
                    });
                }
            } else {
                this.$set(this.model, prop, value);
            }
        },
        // 默认生成字段,减少重复赋值繁琐工作
        generateModel() {
            let _formItems = [...this.formItems];
            if (this.rowItems.length) {
                _formItems = this.rowItems.reduce((arr, item) => {
                    if (item.cols && item.cols.length) {
                        item.cols.forEach((list) => {
                            if (list.formItem) {
                                arr.push({
                                    ...list.formItem,
                                });
                            }
                        });
                    }
                    return arr;
                }, []);
            }
            const obj = _formItems.reduce((obj, item) => {
                const { prop } = item;
                if (prop && Array.isArray(prop)) {
                    obj[prop.join('|')] = undefined;
                    prop.forEach((key) => {
                        obj[key] = undefined;
                    });
                } else {
                    obj[item.prop] = undefined;
                }
                return obj;
            }, {});
            const arr = Object.keys(obj).filter((k) => k.includes('|'));
            const ll = Object.keys(obj).filter((k) => !k.includes('|'));
            arr.forEach((key) => {
                Object.defineProperty(this.model, key, {
                    get() {
                        // 每一个都是就返回[],el-date 会报错
                        if (key.split('|').map((k) => this[k]).every((v) => [undefined, null].includes(v))) {
                            return [];
                        }
                        return key.split('|').map((k) => this[k]);
                    },
                    set(value) {
                        if (value && value.length === key.split('|').length) {
                            key.split('|').forEach((k, index) => {
                                this[k] = value[index];
                            });
                        } else {
                            key.split('|').forEach((k) => {
                                this[k] = undefined;
                            });
                        }
                    },
                    configurable: true,
                });
            });
            ll.forEach((key) => {
                if (this.model[key]) {
                    this.$set(this.model, key, this.model[key]);
                } else {
                    this.$set(this.model, key, undefined);
                }
            });
        },
        focus() {
            this.$nextTick(() => {
                if (this.$refs.focus) {
                    setTimeout(() => {
                        this.$refs.focus.focus();
                    }, 100);
                }
            });
        },
    },
    render(createElement) {
        // 父级slots
        // form-item label slot
        const slotLabel = (slotName) =>
            createElement(
                'template',
                {
                    slot: 'label',
                },
                this.$slots[slotName],
            );
        // formItem 组件
        const formItem = (item) => {
            if (item) {
                return createElement(
                    'el-form-item',
                    {
                        props: {
                            ...item,
                            rules: typeof item.rules === 'function' ? item.rules() : item.rules || null,
                            prop: this.hanlderFormItemProp(item.prop),
                        },
                        scopedSlots: {
                            error: (prop) =>
                                this.$scopedSlots[item.slotErrorName] && this.$scopedSlots[item.slotErrorName](prop),
                        },
                    },
                    [
                        // 加入label插槽
                        item.slotLabelName ? slotLabel(item.slotLabelName) : '',
                        // 有插槽优先
                        item.slotName
                            ? this.$scopedSlots[item.slotName] && this.$scopedSlots[item.slotName](this.model)
                            : createElement(item.type || 'el-input', {
                                //
                                ref: item.ref,
                                attrs: {
                                    disabled: typeof item.disabled === 'function' ? item.disabled() : item.disabled || false,
                                    ...item.props,
                                },
                                props: {
                                    clearable: true,
                                    disabled: typeof item.disabled === 'function' ? item.disabled() : item.disabled || false,
                                    ...item.props,
                                    value: this.hanlderValue(item.prop),
                                },
                                // 加样式
                                style: {
                                    width: this.itemWidth,
                                    ...(item.style || (item.props && item.props.style)),
                                },
                                class: item.class || (item.props && item.props.class),
                                on: {
                                    ...item.on,
                                    change: (value) => {
                                        // 重写change方法
                                        if (item.on && item.on.change) {
                                            item.on.change(value);
                                        }
                                        this.hanlderModel(item.prop, value);
                                    },
                                    // el-input 场景
                                    input: (value) => {
                                        // 重写input方法
                                        if (item.on && item.on.input) {
                                            item.on.input(value);
                                        }
                                        this.hanlderModel(item.prop, value);
                                    },
                                },
                            }),
                    ],
                );
            }
            return '';
        };
        // col 组件
        const col = (item) => {
            if (item) {
                return createElement(
                    'el-col',
                    {
                        props: {
                            ...item,
                        },
                    },
                    item.slotName ? [this.$slots[item.slotName]] : [formItem(item.formItem)],
                );
            }
            return '';
        };
        // row组件
        const row = (item) => {
            if (item) {
                return createElement(
                    'el-row',
                    {
                        props: {
                            ...item,
                        },
                    },
                    item.slotName ? [this.$slots[item.slotName]] : item.cols.filter((col) => (typeof col.hide === 'function' ? !col.hide() : !col.hide)).map((list) => col(list)),

                );
            }
            return '';
        };
        return createElement(
            'el-form',
            {
                ref: 'form',
                props: {
                    ...this.$attrs,
                    model: this.model,
                },
                nativeOn: {
                    // https://github.com/ElemeFE/element/issues/3625
                    // 多个表单选项, 输入回车后并不会自动submit, 然而出现单个 input组件的时候, 聚焦input后按回车就会触发自动提交
                    submit(event) {
                        event.preventDefault();
                    },
                },
                on: {
                    ...this.$listeners,
                },
            },
            // rowItems 优先
            this.rowItems.length
                ? this.rowItems
                    .filter((list) => (typeof list.hide === 'function' ? !list.hide() : !list.hide))
                    .map((item) => row(item))
                : this.formItems
                    .filter((list) => (typeof list.hide === 'function' ? !list.hide() : !list.hide))
                    .map((item) => formItem(item)),
        );
    },
};
</script>

<style lang="scss" module="s"></style>


关键文档,大部分复制el-form

Attributes

参数说明类型可选值默认值
model表单数据对象object
rules表单验证规则object
inline行内表单模式booleanfalse
label-position表单域标签的位置,如果值为 left 或者 right 时,则需要设置 label-widthstringright/left/topright
label-width表单域标签的宽度,例如 '50px'。作为 Form 直接子元素的 form-item 会继承该值。支持 autostring
label-suffix表单域标签的后缀string
hide-required-asterisk是否隐藏必填字段的标签旁边的红色星号booleanfalse
show-message是否显示校验错误信息booleantrue
inline-message是否以行内形式展示校验信息booleanfalse
status-icon是否在输入框中显示校验结果反馈图标booleanfalse
validate-on-rule-change是否在 rules 属性改变后立即触发一次验证booleantrue
size用于控制该表单内组件的尺寸stringmedium / small / mini
disabled是否禁用该表单内的所有组件。若设置为 true,则表单内组件上的 disabled 属性不再生效boolean/functionfalse
-分割线-------------
formItems表单内组件arrayForm-Item
rowItems表单内组件支持layout布局,优先等级高于formItemsarrayRow Attributes
hide动态控制显隐booleanfalse

Methods

方法名说明参数
validate对整个表单进行校验的方法,参数为一个回调函数。该回调函数会在校验结束后被调用,并传入两个参数:是否校验成功和未通过校验的字段。若不传入回调函数,则会返回一个 promiseFunction(callback: Function(boolean, object))
validateField对部分表单字段进行校验的方法Function(props: array | string, callback: Function(errorMessage: string))
resetFields对整个表单进行重置,将所有字段值重置为初始值并移除校验结果
clearValidate移除表单项的校验结果。传入待移除的表单项的 prop 属性或者 prop 组成的数组,如不传则移除整个表单的校验结果Function(props: array | string)
focus使 input 获取焦点-
blur使 input 失去焦点,并隐藏下拉框-

Events

事件名称说明回调参数
validate任一表单项被校验后触发被校验的表单项 prop 值,校验是否通过,错误消息(如果存在)

Slot

name说明
label标签文本的内容

Scoped Slot

name说明
error自定义表单校验信息的显示方式,参数为 { error }

Form-Item Attributes

参数说明类型可选值默认值
prop表单域 model 字段,在使用 validate、resetFields 方法的情况下,该属性是必填的string传入 Form 组件的 model 中的字段
label标签文本string
label-width表单域标签的的宽度,例如 '50px'。支持 autostring
required是否必填,如不设置,则会根据校验规则自动生成booleanfalse
rules表单验证规则object/funtion
error表单域验证错误信息, 设置该值会使表单验证状态变为error,并显示该错误信息string
show-message是否显示校验错误信息booleantrue
inline-message以行内形式展示校验信息booleanfalse
size用于控制该表单域下组件的尺寸stringmedium / small / mini-
slotName插槽名字string

Row Attributes

参数说明类型可选值默认值
gutter栅格间隔number0
type布局模式,可选 flex,现代浏览器下有效string
justifyflex 布局下的水平排列方式stringstart/end/center/space-around/space-betweenstart
alignflex 布局下的垂直排列方式stringtop/middle/bottom
tag自定义元素标签string*div
slotName插槽名字string

Col Attributes

参数说明类型可选值默认值
span栅格占据的列数number24
offset栅格左侧的间隔格数number0
push栅格向右移动格数number0
pull栅格向左移动格数number0
xs<768px 响应式栅格数或者栅格属性对象number/object (例如: {span: 4, offset: 4})
sm≥768px 响应式栅格数或者栅格属性对象number/object (例如: {span: 4, offset: 4})
md≥992px 响应式栅格数或者栅格属性对象number/object (例如: {span: 4, offset: 4})
lg≥1200px 响应式栅格数或者栅格属性对象number/object (例如: {span: 4, offset: 4})
xl≥1920px 响应式栅格数或者栅格属性对象number/object (例如: {span: 4, offset: 4})
tag自定义元素标签string*div
slotName插槽名字string

基本用法

demo 直接使用formItems属性来配置表单

<template>
  <div>
    <d-form-smart
      ref="form"
      :model="form"
      label-width="150px"
      label-position="left"
      :form-items="formItems"
    >
      <template #slotErrorName>
        <span>按钮</span>
      </template>
      <template #region-label>
        <el-badge :value="12" class="item">
          <span>自定义label</span>
        </el-badge>
      </template>
      <template #btn>
        <el-button type="primary" @click="submit">提交</el-button>
      </template>
    </d-form-smart>
  </div>
</template>
<script>
  export default {
    data() {
      return {
        form: {
          name: '',
          region: '',
          date: [],
          delivery: false,
          personnel: '',
          radio: '',
          checkbox: [],
          cascader: [],
          timeSelect: '',
          datePicker: '',
          startTime: '',
          endTime: '',
          rate: 0,
          special: '',
          desc: null,
        },
        rules: {
          name: [
            { required: true, message: '请输入活动名称', trigger: 'blur' },
            {
              min: 3,
              max: 5,
              message: '长度在 3 到 5 个字符',
              trigger: 'blur',
            },
          ],
        },
        formItems: [
          {
            label: '名字',
            prop: 'name',
            type: 'el-input',
            props: {
              placeholder: '请输入内容',
            },
          },
          {
            label: '开关',
            prop: 'delivery',
            type: 'el-switch',
          },
          {
            prop: 'region',
            type: 'd-select-smart',
            slotLabelName: 'region-label',
            props: {
              list: [],
            },
          },
          {
            label: '级联',
            prop: 'cascader',
            type: 'el-cascader',
            props: {
              options: [
                {
                  value: 'zhinan',
                  label: '指南',
                  children: [
                    {
                      value: 'shejiyuanze',
                      label: '设计原则',
                      children: [
                        {
                          value: 'yizhi',
                          label: '一致',
                        },
                        {
                          value: 'fankui',
                          label: '反馈',
                        },
                        {
                          value: 'xiaolv',
                          label: '效率',
                        },
                        {
                          value: 'kekong',
                          label: '可控',
                        },
                      ],
                    },
                  ],
                },
              ],
            },
          },
          {
            label: '时间选择',
            prop: 'timeSelect',
            type: 'el-time-select',
            props: {
              placeholder: '选择时间',
            },
          },
          {
            label: '日期选择',
            prop: 'datePicker',
            type: 'el-date-picker',
            props: {
              placeholder: '选择日期',
              valueFormat: 'yyyy-MM-dd',
            },
          },
          {
            label: '日期范围选择',
            prop: ['startTime', 'endTime'],
            type: 'el-date-picker',
            props: {
              type: 'daterange',
              startPlaceholder: '开始日期',
              endPlaceholder: '结束日期',
              format: 'yyyy-MM-dd',
              valueFormat: 'yyyy-MM-dd',
            },
          },
          {
            label: '评分',
            prop: 'rate',
            type: 'el-rate',
          },
          {
            label: '备注',
            prop: 'desc',
            type: 'el-input',
            props: {
              placeholder: '请输入内容',
              type: 'textarea',
              style: {
                width: '400px',
              },
            },
          },
          {
            slotName: 'btn',
            props: {},
          },
        ],
      };
    },
    methods: {
      change(val) {
        console.log('change', val);
      },
      submit(){
           console.log(this.form);
      }
    },
  };
</script>

使用场景

1. 通用列表搜索条件

  • 配置
  queryItems: [
        {
            label: '属性编码',
            prop: 'propertyValueCode',
            props: {
                placeholder: '属性编码',
            },
        },
        {
            label: '属性名称',
            prop: 'name',
            props: {
                placeholder: '属性名称',
            },
        },
    ],
  • 模板写法
    <d-form-smart
        @submit.native.prevent
        ref="query"
        inline
        :model="query"
        label-width="80px"
        label-position="right"
        :form-items="queryItems"
      >
      </d-form-smart>

2.弹窗写法优化

  • 简单的新增
  • form v-model绑定的值
  • rules 规则
  • formItems 表单配置
  async add() {
            try {
                // 重置表单
                Object.assign(this.form, this.$options.data().form);
                this.$msgbox({
                    customClass: ['custom-message-box'],
                    title: '新增属性',
                    message: this.$createElement('FormSmart', {
                        ref: 'form',
                        key: Math.random(), // 重新创建,不缓存
                        attrs: {
                            labelWidth: '100px',
                            labelPosition: 'right',
                            model: this.form,
                            rules: this.opRules,
                            formItems: opItems,
                        },
                    }),
                    closeOnClickModal: false,
                    showCancelButton: true,
                    confirmButtonText: '确定',
                    cancelButtonText: '取消',
                    beforeClose: async (action, instance, done) => {
                        if (action === 'confirm') {
                            await this.$refs.form.validate();
                            instance.confirmButtonLoading = true;
                            try {
                                // 请求接口编写的地方
                                instance.confirmButtonLoading = false;
                                done();
                            } catch (error) {
                                instance.confirmButtonLoading = false;
                            }
                        } else {
                            this.$refs.form.resetFields();
                            done();
                        }
                    },
                });
            } catch (error) {
                console.error(error);
            }
        },

更新个版本23/6/1