首頁 > 軟體

vue2實現封裝動態表單元件

2022-08-14 10:00:53

封裝元件注意點

  • 不要為了封裝而封裝
  • 只封裝不變的部分,將變化的部分通過slot或其他方式,暴露出去,交給呼叫者實現
  • 為了提供封裝元件的複用率,優先封裝為UI元件,而不是封裝為業務元件(即:封裝元件內部使用到的資料,都通過prop獲取,而不是直接通過ajax請求或vuex中獲取)

動態表單動態控制的是什麼?

已知的需要動態控制的場景:

  • 在A介面顯示:A, B, C三個表單項,B介面顯示:A,B,E,F三個表單項
  • 在A介面預設顯示:A,B兩個表單項,當A項的值為x時,C表單項才顯示出來
  • 在A介面預設顯示:A,B,C三個表單項,A,B預設可用,C預設禁用,當A項的值為x時,C表單項才可用
  • 在A介面B表單項可選擇的值,依賴於A表單項的選擇/填寫結果
  • 根據不同的解析度每行可顯示的表單項數量不同

動態表單封裝

請優先考慮風格二的封裝方式

風格一

特點簡介

將el-form, el-form-item, el-input等等完全封裝到動態表單元件內,通過for迴圈來刷表單項,外部通過傳json設定物件的方式,完成欄位的動態顯示/隱藏控制。

優點:

  • 減少了<el-form-item><el-input></el-input></el-form-item> 這類程式碼的重複編寫
  • 可以輕易的改變表單項的先後順序

缺點:

  • 如果所有用到表單的地方,都用這一個表單元件實現,那麼這個元件後期一定會變得非常龐大,充斥大量的邏輯控制程式碼,導致後期難以維護
  • 要想最大程度的保留原始表單的設定和事件,那就需要使用v-bind=attrsv-on=evts方式進行設定,這相對於template語法來說,json物件的設定方式,就沒那麼讓vue開發者習慣了。
  • 通過v-bind=attrs方式設定原元件的屬性,那麼如果想設定一些預設值就變得麻煩起來,因為vue2中,設定了v-bind=attrs之後,沒法再在模板中設定預設屬性,必須在這個封裝的元件中,在獲取到設定物件時,進行一些比較繁瑣的初始值設定。
  • 如果表單model物件是通過prop傳入,那麼表單內部修改這個表單物件需要做特殊處理,來規避eslint對單項資料流的檢查報錯

程式碼實現範例

動態表單元件

<template>
  <el-form :model="formModel" v-bind="elFormAttrs">
    <el-form-item
      v-for="(formItemConfig, index) in formItemConfigArr"
      :key="`${formItemConfig.prop}-${index}`"
      :prop="formItemConfig.prop"
      :rules="formItemConfig.rules"
    >
      <template #label>
        <div class="o-custom-label">{{ formItemConfig.label }}</div>
      </template>
      <el-input
        v-if="formItemConfig.itemType === 'input'"
        v-model="formModel[formItemConfig.prop]"
      ></el-input>
      <el-select
        v-else-if="formItemConfig.itemType === 'select'"
        v-model="formModel[formItemConfig.prop]"
      >
        <template v-if="formItemConfig.optionArr">
          <el-option
            v-for="(option, pos) in formItemConfig.optionArr"
            :key="`${option.value}-${pos}`"
            :value="option.value"
            :label="option.label"
          ></el-option>
        </template>
      </el-select>
    </el-form-item>
  </el-form>
</template>

<script>
export default {
  name: "DynForm",
  model: {
    event: "change",
    prop: "formData",
  },
  props: {
    formData: {
      type: Object,
    },
    /**
     * prop: 唯一標識
     * itemType: 表單項型別
     * rules: 表單驗證規則
     * label: 顯示標籤
     * optionArr: 下拉值
     */
    formItemConfigArr: {
      type: Array,
      default: () => [],
    },
    // el-form支援的所有屬性
    elFormAttrs: {
      type: Object,
    },
  },
  data() {
    return {
      formModel: this.formData ? this.formData : {},
    };
  },
  watch: {
    // 監聽formData改變,將formData的值設定給元件內部的formModel,規避eslint對單項資料流的規則檢測報錯
    formData: {
      handler(newVal) {
        this.formModel = newVal;
      },
    },
  },
  methods: {},
};
</script>
<style scoped></style>

呼叫動態表單元件

<template>
  <div class="demo-form">
    <DynForm
      v-model="formData"
      :form-item-config-arr="formItemConfigArr"
      :el-form-attrs="elFormAttrs"
    ></DynForm>
  </div>
</template>

<script>
import DynForm from "@/components/form/dyn/DynForm";
export default {
  name: "FormTemplate3",
  components: {
    DynForm,
  },
  props: {
    // input文字方塊數量
    count: {
      type: Number,
      default: 50,
    },
  },
  data() {
    // 表單項設定
    const formItemConfigArr = [
      {
        prop: "sex",
        label: "成語故",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "name",
        label: "姓名",
        itemType: "input",
      },
      {
        prop: "three",
        label: "成語故事",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "four",
        label: "一二三四五",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "five",
        label: "一二三四五六",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "six",
        label: "一二三四五六七",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "seven",
        label: "一二三四五六七八",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "eight",
        label: "一二三四五六七八九",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "ten",
        label: "ten",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "zero",
        label: "zero",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "a",
        label: "hello",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "b",
        label: "hello world",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "c",
        label: "good idea thank",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "d",
        label: "good configuration",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "d",
        label: "good idea thank configuration",
        itemType: "select",
        optionArr: [],
      },
    ];
    return {
      elFormAttrs: {
        inline: true,
        labelWidth: "81px",
      },
      formData: null,
      formItemConfigArr,
    };
  },
  created() {
    this.loadFormData();
    this.loadSexOptions();
  },
  methods: {
    loadFormData() {
      setTimeout(() => {
        this.formData = {
          name: "張三",
          sex: 1,
        };
      }, 1000);
    },
    loadSexOptions() {
      setTimeout(() => {
        const item = this.formItemConfigArr.find((item) => item.prop === "sex");
        if (item) {
          const sexOptionArr = [
            { value: 1, label: "選項1" },
            { value: 2, label: "選項2" },
            { value: 3, label: "選項3" },
          ];
          item.optionArr = sexOptionArr;
        }
      }, 1500);
    },
  },
};
</script>

<style scoped lang="scss">
.js-validate-form ::v-deep(.is-error .o-show-data) {
  color: red;
}
.demo-form ::v-deep(.el-form-item__label) {
  line-height: 1.6;
  display: inline-flex;
  height: 40px;
  justify-items: right;
  justify-content: flex-end;
  align-items: center;
}
.demo-form ::v-deep(.el-form-item__label .o-custom-label) {
  word-break: break-word;
  text-overflow: ellipsis;
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 2; /* 這裡是超出幾行省略 */
  overflow: hidden;
}
</style>

風格二 (推薦優先採用此風格)

特點介紹

通過函陣列件,以jsx語法對el-form-item進行封裝,僅封裝模板程式碼部分,動態部分全部由呼叫者自行實現

優點:

  • 能夠針對介面特點,僅封裝不變的模板程式碼部分,能夠最大程度保留template程式設計風格
  • 因為表單項都是通過slot實現,不會導致這個表單元件隨著應用的場景增多和變得越來越複雜,複雜度會基本保持不變

缺點:

  • 需要掌握jsx語法(jsx僅用於封裝元件,呼叫這個元件使用template語法即可)

程式碼實現範例

表單項元件封裝

<script>
export default {
  functional: true,
  name: "DynFormItemsFunction",
  props: {
    formItemConfigArr: {
      type: Array,
      required: true,
    },
  },
  render(h, context) {
    // console.log(context);
    const { props, scopedSlots } = context;
    const { formItemConfigArr } = props;
    // 獲得插槽裡的 vNodes
    const vNodes = scopedSlots.default();
    return vNodes.map((node, idx) => {
      const formItemConfig = formItemConfigArr[idx];
      return (
        <el-form-item prop={formItemConfig.prop} rules={formItemConfig.rules}>
          <span slot="label" className="o-custom-label">
            {formItemConfig.label}
          </span>
          {node}
        </el-form-item>
      );
    });
  },
};
</script>

<style scoped>
.o-custom-label {
  color: blue;
}
</style>

程式碼呼叫範例

<template>
  <div>
    <el-form :model="formData" inline :validate-on-rule-change="false">
      <!-- DynFormItemsFunction 這個元件,僅封裝了el-form-item的邏輯 -->
      <DynFormItemsFunction :form-item-config-arr="formItemConfigArrComp">
        <!-- 
        注意元件的迴圈是在slot中進行的,el-form-item的包裝邏輯,
        交給了DynFormItemsFunction元件實現
        -->
        <template v-for="(formItemConfig, idx) in formItemConfigArrComp">
          <el-input
            v-if="formItemConfig.itemType === 'input'"
            v-model="formData[formItemConfig.prop]"
            :key="idx"
          ></el-input>
          <el-select
            v-else-if="formItemConfig.itemType === 'select'"
            v-model="formData.sex"
            :key="idx"
          >
            <template v-if="formItemConfig.optionArr">
              <el-option
                v-for="option in formItemConfig.optionArr"
                :key="option.value"
                :value="option.value"
                :label="option.label"
              ></el-option>
            </template>
          </el-select>
        </template>
      </DynFormItemsFunction>
    </el-form>
  </div>
</template>

<script>
import DynFormItemsFunction from "@/components/form/dyn/DynFormItemsFunction";
export default {
  name: "FormTemplate",
  components: {
    DynFormItemsFunction,
  },
  props: {
    count: {
      type: Number,
      default: 50,
    },
  },
  data() {
    /*
    完整的表單設定放在data中,資料中的元素個數不會變,
    動態值也填充到formItemConfigArr對應元素中(如性別的下拉選項值),
    formItemConfigArr只需要關注完整表單需要哪些表單項欄位,
    以及下拉選項資料的填充,無需考慮表單項欄位的顯示/隱藏邏輯控制
    */
    const formItemConfigArr = [
      {
        prop: "sex",
        label: "性別",
        itemType: "select",
        optionArr: [],
      },
      {
        prop: "name",
        label: "姓名",
        itemType: "input",
        rules: [{ required: true, message: "該項必填", trigger: "change" }],
      },
      {
        prop: "school",
        label: "學校",
        itemType: "input",
        rules: [],
      },
    ];
    // for (let i = 0; i < this.count; i++) {
    //   formItemConfigArr.push({
    //     prop: "name" + i,
    //     label: "姓名" + i,
    //     itemType: "input",
    //   });
    // }
    return {
      formData: {
        name: null,
        sex: null,
        school: null,
      },
      formItemConfigArr,
    };
  },
  computed: {
    /*
    表單項的顯示/隱藏通過計算屬性實現,可以認為計算屬性就只關注控制哪些表單項需要顯示,
    哪些需要隱藏,就可以了。算是一種職責分離
    */
    formItemConfigArrComp() {
      return this.formItemConfigArr.filter((item) => {
        if (this.formData.sex === 3 && item.prop === "name") {
          return null;
        }
        return item;
      });
    },
  },
  created() {
    this.loadOptions(1500);
  },
  methods: {
    loadOptions(timeout) {
      setTimeout(() => {
        const item = this.formItemConfigArr.find((item) => item.prop === "sex");
        if (item) {
          const sexOptionArr = new Array(10)
            .fill(true)
            .map((item, idx) => ({ value: idx, label: "選項:" + idx }));
          item.optionArr = sexOptionArr;
        }
      }, timeout);
    },
  },
};
</script>

<style scoped lang="scss">
.js-validate-form ::v-deep(.is-error .o-show-data) {
  color: red;
}
</style>

到此這篇關於vue2實現封裝動態表單元件的文章就介紹到這了,更多相關vue封裝表單元件內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com