讓el-form更好用,通過配置的方式

語言: CN / TW / HK

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>

複製代碼

代碼:演示實例 - 添加表單項