让el-form更好用,通过配置的方式

2,742 阅读4分钟

element-ui虽然有el-form组件,但是仍然需要手动写el-form-item

e-el-form1

这里希望进一步抽离配置,在el-form的基础上封装个enhanced-el-form组件。

使用的时候希望这样,不再需要手动写里面的el-form-item

<enhanced-el-form :model="model" :schema="schema" ></enhanced-el-form>

这边借鉴cube-ui的form属性

  • model属性,表单数据对象,{name:'颜酱',age:18}
  • schema属性,每个表单项的配置数组,如下

e-el-form9

本文代码后期可能较复杂,需要的话,可以去看github代码

当然文末也附上了,每个小节的具体代码,有需要也可以看看

TL;DR

  • 就是写了个enhanced-el-form组件,充当原先的el-form组件
  • 唯一不一样的是,rules换成schema,其他属性、事件、方法同el-form组件
  • 哦,如果表单项不足以用schema描述的话,这边提供了slot
  • 按钮部分也不需要用schema,这里提供slot#footer
  • 想直接看enhanced-el-form组件怎么用的,包括复杂情况,直接跳到本文的演示实例的优化

组件的概况

综上,enhanced-el-form组件的大概就出来了。

注意,rules属性是el-form有的,这里通过schema得到

e-el-form2

el-form-item的处理

现在有一个很简单的表单,需要填写姓名年龄

App.vue里可以简单写下:

e-el-form3

enhanced-el-form内部,没有schema的情况下,长这样

el-form(:model="model" :rules="rules")
  el-form-item(label="姓名" prop="name")
    el-input(v-model="model.name" maxlength="20")
  el-form-item(label="年龄" prop="age")
    el-input(v-model="model.age" maxlength="20")

当然,有了schema,直接就循环了

el-form(:model="model" :rules="rules")
  el-form-item(:label="config.label" :prop="config.modelKey" v-for="config in schema" :key="config.modelKey")
    el-input(v-model="model[config.modelKey]" v-bind="config.props")

匹配除了input之外的元素

表单组件当然不止input,查看官网的侧边栏发现有以下组件,其中需要配合子组件使用的放在末尾,其他的都可以单独使用。

  • radio
  • checkbox
  • input
  • input-number
  • cascader
  • switch
  • slider
  • time-select
  • date-picker
  • rate
  • color-picker
  • transfer
  • radio-group,需要子组件
  • checkbox-group,需要子组件
  • select,需要子组件
  • upload,需要子组件

enhanced-el-form内部,可以使用component匹配不同组件,需要子组件的,单独匹配。 e-el-form5

注意:有选项的时候,options可以是['上海','北京'],也可以是[{label:'上海',value:'shanghai'},{label:'北京',value:'北京'}]
option项里没有设定labelvalue的时候,默认两者一致。

演示实例 - 添加表单项

接下来,通过改写schema来显示表单。

将最开始的官方的例子,用schema改写下:

e-el-form6

注意:多选的时候,model里面的相应项的默认值必须是数组

这是大概效果,嗯,还差点,没关系,后面还有优化~

e-el-form7

优化

  • el-form自身也有很多属性,这里通过简单的v-bind="$attrs",将enhanced-el-from上面的属性自动到el-form
  • 同理,v-on="$listeners"
  • el-form上面的方法,稍微麻烦点,通过手动赋值
  • 一般提交按钮不需要配置项,直接插入即可,这里增加slot#footer
  • 同理,表单的开始有可能有别的描述,这里增加slot#header
  • 部分表单项,需要定制,通过slotName属性,表示不参与内部循环,需要自己写逻辑
  • date系列的表单,可能需要多个组件拼接,一般有children,这种时候再需要自己定制的基础上,还需要处理childrenrules
// el-form(ref="elForm" :model="model" :rules="rules" v-bind="$attrs" v-on="$listeners")
  mounted() {
    const methods = [ "validate", "validateField", "resetFields", "clearValidate" ];
    methods.forEach(method => (this[method] = this.$refs.elForm[method]));
  }

以上逻辑将在下面的部分展示全部代码。

演示实例的优化

实例的效果:

e-el-form8

EnhancedElForm的代码如下

<template lang="pug">
el-form(ref="elForm" :model="model" :rules="rules" v-bind="$attrs" v-on="$listeners")
  slot(name="header")
  
  template(v-for="config in schema" )
    slot(v-if="config.slotName" :name="config.slotName" v-bind="config")
 
    el-form-item(v-else :label="config.label" :prop="config.modelKey" :key="config.modelKey")
      el-radio-group(v-if="config.type==='radio-group'"   v-model="model[config.modelKey]" v-bind="config.props")
        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-checkbox-group(v-else-if="config.type==='checkbox-group'"   v-model="model[config.modelKey]" v-bind="config.props")
        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-select(v-else-if="config.type==='select'"   v-model="model[config.modelKey]" v-bind="config.props")
        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")

      component(v-else :is="'el-'+config.type" v-model="model[config.modelKey]" v-bind="config.props") {{config.text}}

  slot(name="footer")
</template>
<script>
export default {
  name: "enhanced-el-form",
  props: {
    model: {
      type: Object,
      default() {
        return {};
      }
    },
    schema: {
      type: Array,
      default() {
        return {};
      }
    }
  },
  computed: {
    rules() {
      return this.schema.reduce((acc, cur) => {
        acc[cur.modelKey] = cur.rules;
        // 日期组件可能有children
        const hasChildren = cur.children && cur.children.length;
        hasChildren &&
          cur.children.forEach(child => (acc[child.modelKey] = child.rules));
        return acc;
      }, {});
    }
  },
  mounted() {
    // el-form上面的方法继承过来
    const methods = [
      "validate",
      "validateField",
      "resetFields",
      "clearValidate"
    ];
    methods.forEach(method => (this[method] = this.$refs.elForm[method]));
  }
};
</script>

App.vue的代码如下

<template lang="pug">
div#app
  enhanced-el-form(ref='ruleForm' :model="model" :schema="schema"  label-width="100px" @validate="validate")
    template(#date="config")
      el-form-item(:label="config.label")
        el-col(:span="11")
          el-form-item(:prop="config.children[0].modelKey")
            el-date-picker(v-model="model[config.children[0].modelKey]" v-bind="config.children[0].props")
        el-col(:span="2") --
        el-col(:span="11")
          el-form-item(:prop="config.children[1].modelKey")
            el-time-picker(v-model="model[config.children[1].modelKey]" v-bind="config.children[1].props")
    template(#footer)
      el-form-item
        el-button(type="primary" @click="submitForm('ruleForm')") 立即创建
        el-button(@click="resetForm('ruleForm')") 重置
  div {{model}}
</template>

<script>
import EnhancedElForm from "./components/EnhancedElForm.vue";
export default {
  name: "App",
  components: { EnhancedElForm },
  data() {
    return {
      model: { type: [] },
      schema: [
        {
          type: "input",
          modelKey: "name",
          label: "活动名称",
          rules: [
            { required: true, message: "请输入活动名称", trigger: "blur" },
            { min: 3, max: 5, message: "长度在 3 到 5 个字符", trigger: "blur" }
          ]
        },
        {
          type: "select",
          modelKey: "region",
          label: "活动区域",
          props: {
            placeholder: "请选择活动区域",
            options: [
              { label: "区域一", value: "shanghai" },
              { label: "区域二", value: "beijing" }
            ]
          },
          rules: [
            { required: true, message: "请选择活动区域", trigger: "change" }
          ]
        },
        {
          slotName: "date",
          label: "活动时间",
          children: [
            {
              modelKey: "date1",
              props: {
                type: "date",
                placeholder: "选择日期",
                style: "width:100%"
              },
              rules: [
                {
                  type: "date",
                  required: true,
                  message: "请选择日期",
                  trigger: "change"
                }
              ]
            },
            {
              modelKey: "date2",
              props: {
                placeholder: "选择时间",
                style: "width:100%"
              },
              rules: [
                {
                  type: "date",
                  required: true,
                  message: "请选择时间",
                  trigger: "change"
                }
              ]
            }
          ]
        },
        {
          type: "switch",
          modelKey: "delivery",
          label: "即时配送",
          props: {}
        },

        {
          type: "checkbox-group",
          modelKey: "type",
          label: "活动性质",
          props: {
            options: [
              "美食/餐厅线上活动",
              "地推活动",
              "线下主题活动",
              "单纯品牌曝光"
            ]
          },
          rules: [
            {
              type: "array",
              required: true,
              message: "请至少选择一个活动性质",
              trigger: "change"
            }
          ]
        },

        {
          type: "radio-group",
          modelKey: "resource",
          label: "特殊资源",
          props: {
            options: [
              { label: "线上品牌商赞助", value: "xianshang" },
              { label: "线下场地免费", value: "xianxia" }
            ]
          },
          rules: [
            { required: true, message: "请选择活动资源", trigger: "change" }
          ]
        },
        {
          type: "input",
          modelKey: "desc",
          label: "活动形式",
          props: {
            type: "textarea"
          },
          rules: [
            { required: true, message: "请填写活动形式", trigger: "blur" }
          ]
        }
      ]
    };
  },
  methods: {
    validate(...args) {
      console.log(...args);
    },
    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>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>

不足

为了方便,这里的组件EnhancedElForm,直接修改了父组件的model

但这是违反规范的,回头有空再研究下,读者有思路也可以指导下~

表单组件可能有少量遗漏,实际使用中如果发现el-undefined未注册之类的错误信息,可以自己匹配下

代码

代码:组件概况

<template lang="pug">
  el-form(:model="model" :rules="rules")
</template>
<script>
export default {
  name: "enhanced-el-form",
  props: {
    model: {
      type: Object, default() { return {}; }
    },
    schema: {
      type: Array, default() { return {}; }
    }
  },
  computed: {
    rules() {
      return this.schema.reduce((acc, cur) => {
        acc[cur.modelKey] = cur.rules;
        return acc;
      }, {});
    }
  }
};
</script>

代码:el-form-item的处理

App.vue


<template lang="pug">
div#app
  enhanced-el-form(:model="model" :schema="schema")
  div {{model}}
</template>

<script>
import EnhancedElForm from "./components/EnhancedElForm.vue";
export default {
  name: "App",
  components: { EnhancedElForm },
  data() {
    return {
      model: { name: "", age: "" },
      schema: [
        {
          type: "input", modelKey: "name", label: "姓名",
          props: { maxlength: 20 },
          rules: [{ required: true, message: "请输入姓名", trigger: "blur" }]
        },
        {
          type: "input", modelKey: "age", label: "年龄",
          props: { maxlength: 5 },
          rules: [{ required: true, message: "请输入年龄", trigger: "blur" }]
        }
      ]
    };
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
</style>

代码:除input之外的元素

EnhancedElForm.vue

<template lang="pug">
el-form(:model="model" :rules="rules")
  el-form-item(v-for="config in schema" :label="config.label" :prop="config.modelKey" :key="config.modelKey")
  
    el-radio-group(v-if="config.type==='radio-group'"   v-model="model[config.modelKey]" v-bind="config.props")
      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-checkbox-group(v-else-if="config.type==='checkbox-group'"   v-model="model[config.modelKey]" v-bind="config.props")
      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-select(v-else-if="config.type==='select'"   v-model="model[config.modelKey]" v-bind="config.props")
      el-option(v-for="(item,index) in config.props.options" :key="index" :value="typeof item==='object'?item.label:item" :label="typeof item==='object'?item.value:item")

    component(v-else :is="'el-'+config.type" v-model="model[config.modelKey]" v-bind="config.props") {{config.text}}
    
</template>
<script>
export default {
  name: "enhanced-el-form",
  props: {
    model: {
      type: Object,
      default() {
        return {};
      }
    },
    schema: {
      type: Array,
      default() {
        return {};
      }
    }
  },
  computed: {
    rules() {
      return this.schema.reduce((acc, cur) => {
        acc[cur.modelKey] = cur.rules;
        return acc;
      }, {});
    }
  }
};
</script>

</script>

代码:演示实例 - 添加表单项