Vue3元件(九)Vue + element-Plus + json = 動態渲染的表單控制元件

金色海洋(jyk)發表於2021-02-22

一個成熟的表單

表單表單,你已經長大了,你要學會:

  • 動態渲染
  • 支援單列、雙列、多列
  • 支援調整佈局
  • 支援表單驗證
  • 支援調整排列(顯示)順序
  • 依據元件值顯示需要的元件
  • 支援 item 擴充套件元件
  • 可以自動建立 model

這個表單控制元件是基於 element-plus 的 el-form 做的二次封裝,所以首先感謝 element-plus 提供了這麼強大的UI庫,以前用 jQuery 做過類似的,但是非常麻煩,既不好看,可維護性、擴充套件性也差,好多想法都實現不了(技術有限)。
現在好了,站在巨人的肩膀上,實現自己的想法了。

實現動態渲染

把表單需要的屬性,統統放入json裡面,然後用require(方便) 或者aioxs(可以熱更新)載入進來,這樣就可以實現動態渲染了。
比如要實現公司資訊的新增、修改,那麼只需要載入公司資訊需要的json即可。
想要實現員工資訊的新增、修改,那麼只需要載入員工資訊需要的json。

總之,載入需要的json即可,不需要再一遍一遍的手擼程式碼了。

那麼這個神奇的 json 是啥樣子的呢?檔案有點長,直接看截圖,更清晰一些。
006動態渲染需要的json.png

另外還有幾個附帶功能:

  • 支援單行下的合併。
    在單行的情況下,一些短的控制元件會比較佔空間,我們可以把多個小的合併到一行。

  • 支援多行下的擴充套件。
    多行的情況下,一些長的控制元件需要佔更多的空間,我們可以設定它多佔幾個格子。

  • 自動建立表單需要的 model。
    不需要手動寫 model了。

實現多行多列的表單

再次感謝 el-form,真的很強大,不僅好看,還提供了驗證功能,還有很多其他的功能。
只是好像只能橫著排,或者豎著排。那麼能不能多行多列呢?似乎沒有直接提供。

我們知道 el-row、el-col 可以實現多行多列的功能,那麼能不能結合一下呢?官網也不直說,害的我各種找,還好找到了。(好吧,其實折騰了一陣著的table)

二者結合一下就可以了,這裡有個小技巧,el-row 只需要一個就可以,el-col 可以有多個,這樣一行排滿後,會自動排到下一行。

    <el-form
      ref="form"
      :inline="false"
      class="demo-form-inline"
      :model="formModel"
      label-suffix=":"
      label-width="130px"
      size="mini"
    >
      <el-row>
        <!--不迴圈row,直接迴圈col,放不下會自動往下換行。-->
        <el-col
          v-for="(ctrId, index) in formColSort"
          :key="'form_'+index"
          :span="formColSpan[ctrId]"
        >
          <el-form-item :label="getCtrMeta(ctrId).label">
            <!--表單item元件,採用動態元件的方式-->
            <component
              :is="ctlList[getCtrMeta(ctrId).controlType]"
              v-model="formModel[getCtrMeta(ctrId).colName]"
              :meta="getCtrMeta(ctrId)"
              @myChange="mySubmit">
            </component>
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
  • formColSort
    存放元件ID的陣列,決定了顯示哪些元件以及顯示的先後順序。

  • v-for
    遍歷 formColSort 得到元件ID,然後獲取ID對應的span(確定佔位)以及元件需要的meta。

  • formColSpan
    存放元件佔位的陣列。依據el-col的span的24格設定。

  • getCtrMeta(ctrId)
    根據元件ID獲取元件的meta。
    為啥要寫個函式呢?因為model的屬性不允許中括號套娃,所以只好寫個函式。
    為啥不用計算屬性呢?計算屬性好像不能傳遞引數。

  • component :is="xxx"
    Vue提供的動態元件,用這個可以方便載入不同型別的子元件。

  • ctlList
    元件字典,把元件型別變成對應的元件標籤。

這樣一個v-for搞定了很多事情,比如單列、多列,元件的排序問題,元件的佔位問題,還有依據使用者的選擇顯示不同的元件的問題,其實就是修改一下 formColSort 裡的元件ID的構成和順序。

自動建立 model

我比較懶,手擼 model 是不是有點麻煩?如果能夠自動獲得該多好,於是我寫了這個函式。

  // 根據表單元素meta,建立 v-model
  const createModel = () => {
    // 依據meta,建立module
    for (const key in formItemMeta) {
      const m = formItemMeta[key]
      // 根據控制元件型別設定屬性值
      switch (m.controlType) {
        case 100: // 文字類
        case 101:
        case 102:
        case 103:
        case 104:
        case 105:
        case 106:
        case 107:
        case 130:
        case 131:
          formModel[m.colName] = ''
          break
        case 110: // 日期
        case 111: // 日期時間
        case 112: // 年月
        case 114: // 年
        case 113: // 年周
          formModel[m.colName] = null
          break
        case 115: // 任意時間
          formModel[m.colName] = '00:00:00'
          break
        case 116: // 選擇時間
          formModel[m.colName] = '00:00'
          break
        case 120: // 數字
        case 121:
          formModel[m.colName] = 0
          break
        case 150: // 勾選
        case 151: // 開關
          formModel[m.colName] = false
          break
        case 153: // 單選組
        case 160: // 下拉單選
        case 162: // 下拉聯動
          formModel[m.colName] = null
          break
        case 152: // 多選組
        case 161: // 下拉多選
          formModel[m.colName] = []
          break
      }
      // 看看有沒有設定預設值
      if (typeof m.defaultValue !== 'undefined') {
        switch (m.defaultValue) {
          case '':
            break
          case '{}':
            formModel[m.colName] = {}
            break
          case '[]':
            formModel[m.colName] = []
            break
          case 'date':
            formModel[m.colName] = new Date()
            break
          default:
            formModel[m.colName] = m.defaultValue
            break
        }
      }
    }
    // 同步父元件的v-model
    context.emit('update:modelValue', formModel)
    return formModel
  }

可以根據型別和預設值,設定 model 的屬性,這樣就方便多了。

建立使用者選擇的 model

就是使用者選了某個選項,表單的元件響應變化後的model。
在我的計劃裡面是需要一個這樣的簡單的model,所以我又寫了一個函式

  // 依據使用者選項,建立對應的 model
  const createPartModel = (array) => {
    // 先刪除屬性
    for (const key in formPartModel) {
      delete formPartModel[key]
    }
    // 建立新屬性
    for (let i = 0; i < array.length; i++) {
      const colName = formItemMeta[array[i]].colName
      formPartModel[colName] = formModel[colName]
    }
  }

這樣就可以得到一個簡潔的 model 了。

多列的表單

這個是最複雜的,分為兩種情況:單列的擠一擠、多列的搶位置。

單列

007單列表單.png

單列的表單有一個特點,一行比較寬鬆,那麼有時候就需要兩個元件在一行裡顯示,其他的還是一行一個元件,那麼要如何調整呢?

這裡做一個設定:

  • 一個元件一行的,記做1
  • 兩個元件擠一行的,記做-2
  • 三個元件擠一行的,記做-3
    以此類推,理論上最多支援 -24,當然實際上似乎沒有這麼寬的顯示器。

這樣記錄之後,我們就可以判斷,≥1的記做span=24,負數的,用24去除,得到的就是span的數字。當然記得取整數。

為啥用負數做標記呢?就是為了區分開多列的調整。

多列

008雙列表單.png

調多了之後發現一個問題,看起來和單列調整後似乎一樣的。

009三列表單.png

多列的表單有一個特點,一個格子比較小,有些元件太長放不下,這個時候這個元件就要搶後面的格子來用。

那麼我們還是做一個設定:

  • 一個元件佔一格的,還是記做1
  • 一個元件佔兩格的,記做2
  • 一個元件佔三格的,記做3
    以此類推。

這樣記錄之後,我們可以判斷,≤1的,記做 24 / 列數,大於1的記做 24/ 列數 * n。
這樣就可以了,可以相容單列的設定,不用因為單列變多列而調整設定。
只是有個小麻煩,佔得格子太多的話,就會提取擠到下一行,而本行會出現“空缺”。
這個暫時靠人工調整吧。
畢竟哪個欄位在前面,還是需要人工設定的。

一頓分析猛如虎,一看程式碼沒幾行。

  // 根據配置裡面的colCount,設定 formColSpan
  const setFormColSpan = () => {
    const formColCount = formMeta.formColCount // 列數
    const moreColSpan = 24 / formColCount // 一個格子佔多少份

    if (formColCount === 1) {
    // 一列的情況
      for (const key in formItemMeta) {
        const m = formItemMeta[key]
        if (typeof m.colCount === 'undefined') {
          formColSpan[m.controlId] = moreColSpan
        } else {
          if (m.colCount >= 1) {
            // 單列,多佔的也只有24格
            formColSpan[m.controlId] = moreColSpan
          } else if (m.colCount < 0) {
            // 擠一擠的情況, 24 除以 佔的份數
            formColSpan[m.controlId] = moreColSpan / (0 - m.colCount)
          }
        }
      }
    } else {
      // 多列的情況
      for (const key in formItemMeta) {
        const m = formItemMeta[key]
        if (typeof m.colCount === 'undefined') {
          formColSpan[m.controlId] = moreColSpan
        } else {
          if (m.colCount < 0 || m.colCount === 1) {
            // 多列,擠一擠的佔一份
            formColSpan[m.controlId] = moreColSpan
          } else if (m.colCount > 1) {
            // 多列,佔的格子數 * 份數
            formColSpan[m.controlId] = moreColSpan * m.colCount
          }
        }
      }
    }
  }

最後看看效果,可以動態設定列數:
【視訊一】
https://www.zhihu.com/zvideo/1347091197660405760

依據使用者的選擇,顯示對應的元件

這個也是一個急需的功能,否則的話,動態渲染的表單控制元件適應性就會受到限制。
其實想想也不難,就是改一下 formColSort 裡面的元件ID就好了。
我們設定一個watch來監聽元件值的變化,然後把需要的元件ID設定給 formColSort 就可以了。

  // 監聽元件值的變化,調整元件的顯示以及顯示順序
  if (typeof formMeta.formColShow !== 'undefined') {
    for (const key in formMeta.formColShow) {
      const ctl = formMeta.formColShow[key]
      const colName = formItemMeta[key].colName
      watch(() => formModel[colName], (v1, v2) => {
        if (typeof ctl[v1] === 'undefined') {
          // 沒有設定,顯示預設元件
          setFormColSort()
        } else {
          // 按照設定顯示元件
          setFormColSort(ctl[v1])
          // 設定部分的 model
          createPartModel(ctl[v1])
        }
      })
    }
  }

因為需要監聽的元件可能不只一個,所以做了個迴圈,這樣就可以監聽所有需要的元件了。

看看效果
【視訊二】
https://www.zhihu.com/zvideo/1347099700483457024

完整程式碼

上面的程式碼比較凌亂,這裡整體介紹一下。

  • el-form-manage.js
    表單元件的管理類,單獨拿出來,這樣就可以支援其他UI庫了,比如antdv
import { reactive, watch } from 'vue'

/**
 * 表單的管理類
 * * 建立v-model
 * * 調整列數
 * * 合併
 */
const formManage = (props, context) => {
  // 定義 完整的 v-model
  const formModel = reactive({})
  // 定義區域性的 model
  const formPartModel = reactive({})

  // 確定一個元件佔用幾個格子
  const formColSpan = reactive({})
  // 定義排序依據
  const formColSort = reactive([])
  // 獲取表單meta
  const formMeta = props.meta
  console.log('formMeta', formMeta)
  // 表單元素meta
  const formItemMeta = formMeta.itemMeta
  // 表單驗證meta,備用
  // const formRuleMeta = formMeta.ruleMeta

  // 根據表單元素meta,建立 v-model
  const createModel = () => {
    // 依據meta,建立module
    for (const key in formItemMeta) {
      const m = formItemMeta[key]
      // 根據控制元件型別設定屬性值
      switch (m.controlType) {
        case 100: // 文字類
        case 101:
        case 102:
        case 103:
        case 104:
        case 105:
        case 106:
        case 107:
        case 130:
        case 131:
          formModel[m.colName] = ''
          break
        case 110: // 日期
        case 111: // 日期時間
        case 112: // 年月
        case 114: // 年
        case 113: // 年周
          formModel[m.colName] = null
          break
        case 115: // 任意時間
          formModel[m.colName] = '00:00:00'
          break
        case 116: // 選擇時間
          formModel[m.colName] = '00:00'
          break
        case 120: // 數字
        case 121:
          formModel[m.colName] = 0
          break
        case 150: // 勾選
        case 151: // 開關
          formModel[m.colName] = false
          break
        case 153: // 單選組
        case 160: // 下拉單選
        case 162: // 下拉聯動
          formModel[m.colName] = null
          break
        case 152: // 多選組
        case 161: // 下拉多選
          formModel[m.colName] = []
          break
      }
      // 看看有沒有設定預設值
      if (typeof m.defaultValue !== 'undefined') {
        switch (m.defaultValue) {
          case '':
            break
          case '{}':
            formModel[m.colName] = {}
            break
          case '[]':
            formModel[m.colName] = []
            break
          case 'date':
            formModel[m.colName] = new Date()
            break
          default:
            formModel[m.colName] = m.defaultValue
            break
        }
      }
    }
    // 同步父元件的v-model
    context.emit('update:modelValue', formModel)
    return formModel
  }
  // 先執行一次
  createModel()

  // 向父元件提交 model
  const mySubmit = (val, controlId, colName) => {
    context.emit('update:modelValue', formModel)
    // 同步到部分model
    if (typeof formPartModel[colName] !== 'undefined') {
      formPartModel[colName] = formModel[colName]
    }
    context.emit('update:partModel', formPartModel)
  }

  // 依據使用者選項,建立對應的 model
  const createPartModel = (array) => {
    // 先刪除屬性
    for (const key in formPartModel) {
      delete formPartModel[key]
    }
    // 建立新屬性
    for (let i = 0; i < array.length; i++) {
      const colName = formItemMeta[array[i]].colName
      formPartModel[colName] = formModel[colName]
    }
  }

  // 根據配置裡面的colCount,設定 formColSpan
  const setFormColSpan = () => {
    const formColCount = formMeta.formColCount // 列數
    const moreColSpan = 24 / formColCount // 一個格子佔多少份

    if (formColCount === 1) {
    // 一列的情況
      for (const key in formItemMeta) {
        const m = formItemMeta[key]
        if (typeof m.colCount === 'undefined') {
          formColSpan[m.controlId] = moreColSpan
        } else {
          if (m.colCount >= 1) {
            // 單列,多佔的也只有24格
            formColSpan[m.controlId] = moreColSpan
          } else if (m.colCount < 0) {
            // 擠一擠的情況, 24 除以 佔的份數
            formColSpan[m.controlId] = moreColSpan / (0 - m.colCount)
          }
        }
      }
    } else {
      // 多列的情況
      for (const key in formItemMeta) {
        const m = formItemMeta[key]
        if (typeof m.colCount === 'undefined') {
          formColSpan[m.controlId] = moreColSpan
        } else {
          if (m.colCount < 0 || m.colCount === 1) {
            // 多列,擠一擠的佔一份
            formColSpan[m.controlId] = moreColSpan
          } else if (m.colCount > 1) {
            // 多列,佔的格子數 * 份數
            formColSpan[m.controlId] = moreColSpan * m.colCount
          }
        }
      }
    }
  }
  // 先執行一次
  setFormColSpan()

  // 設定元件的顯示順序
  const setFormColSort = (array = formMeta.colOrder) => {
    formColSort.length = 0
    formColSort.push(...array)
  }
  // 先執行一下
  setFormColSort()

  // 監聽元件值的變化,調整元件的顯示以及顯示順序
  if (typeof formMeta.formColShow !== 'undefined') {
    for (const key in formMeta.formColShow) {
      const ctl = formMeta.formColShow[key]
      const colName = formItemMeta[key].colName
      watch(() => formModel[colName], (v1, v2) => {
        if (typeof ctl[v1] === 'undefined') {
          // 沒有設定,顯示預設元件
          setFormColSort()
        } else {
          // 按照設定顯示元件
          setFormColSort(ctl[v1])
          // 設定部分的 model
          createPartModel(ctl[v1])
        }
      })
    }
  }

  return {
    // 物件
    formModel, // v-model createModel()
    formPartModel, // 使用者選擇的元件的 model
    formColSpan, // 確定元件佔位
    formColSort, // 確定元件排序
    // 函式
    createModel, // 建立 v-model
    setFormColSpan, // 設定元件佔位
    setFormColSort, // 設定元件排序
    mySubmit // 提交
  }
}

export default formManage

  • el-form-map.js
    動態元件需要的字典
import { defineAsyncComponent } from 'vue'

/**
 * 元件裡面註冊控制元件用
 * * 文字
 * ** eltext 單行文字、電話、郵件、搜尋
 * ** elarea 多行文字
 * ** elurl
 * * 數字
 * ** elnumber 數字
 * ** elrange 滑塊
 * * 日期
 * ** eldate 日期、年月、年周、年
 * ** eltime 時間
 * * 選擇
 * ** elcheckbox 勾選
 * ** elswitch 開關
 * ** elcheckboxs 多選組
 * ** elradios 單選組
 * ** elselect 下拉選擇
 */
const formItemList = {
  // 文字類 defineComponent
  eltext: defineAsyncComponent(() => import('./t-text.vue')),
  elarea: defineAsyncComponent(() => import('./t-area.vue')),
  elurl: defineAsyncComponent(() => import('./t-url.vue')),
  // 數字
  elnumber: defineAsyncComponent(() => import('./n-number.vue')),
  elrange: defineAsyncComponent(() => import('./n-range.vue')),
  // 日期、時間
  eldate: defineAsyncComponent(() => import('./d-date.vue')),
  eltime: defineAsyncComponent(() => import('./d-time.vue')),
  // 選擇、開關
  elcheckbox: defineAsyncComponent(() => import('./s-checkbox.vue')),
  elswitch: defineAsyncComponent(() => import('./s-switch.vue')),
  elcheckboxs: defineAsyncComponent(() => import('./s-checkboxs.vue')),
  elradios: defineAsyncComponent(() => import('./s-radios.vue')),
  elselect: defineAsyncComponent(() => import('./s-select.vue')),
  elselwrite: defineAsyncComponent(() => import('./s-selwrite.vue'))
}

/**
 * 動態元件的字典,便於v-for迴圈裡面設定控制元件
 */
const formItemListKey = {
  // 文字類
  100: formItemList.elarea, // 多行文字
  101: formItemList.eltext, // 單行文字
  102: formItemList.eltext, // 密碼
  103: formItemList.eltext, // 電話
  104: formItemList.eltext, // 郵件
  105: formItemList.elurl, // url
  106: formItemList.eltext, // 搜尋
  // 數字
  120: formItemList.elnumber, // 陣列
  121: formItemList.elrange, // 滑塊
  // 日期、時間
  110: formItemList.eldate, // 日期
  111: formItemList.eldate, // 日期 + 時間
  112: formItemList.eldate, // 年月
  113: formItemList.eldate, // 年周
  114: formItemList.eldate, // 年
  115: formItemList.eltime, // 任意時間
  116: formItemList.eltime, // 選擇固定時間
  // 選擇、開關
  150: formItemList.elcheckbox, // 勾選
  151: formItemList.elswitch, // 開關
  152: formItemList.elcheckboxs, // 多選組
  153: formItemList.elradios, // 單選組
  160: formItemList.elselect, // 下拉
  161: formItemList.elselwrite, // 下拉多選
  162: formItemList.elselect // 下拉聯動

}

export default {
  formItemList,
  formItemListKey
}

  • el-form-div.vue
    表單控制元件的程式碼
    模板
  <div >
    <el-form
      ref="form"
      :inline="false"
      class="demo-form-inline"
      :model="formModel"
      label-suffix=":"
      label-width="130px"
      size="mini"
    >
      <el-row>
        <!--不迴圈row,直接迴圈col,放不下會自動往下換行。-->
        <el-col
          v-for="(ctrId, index) in formColSort"
          :key="'form_'+index"
          :span="formColSpan[ctrId]"
        >
          <el-form-item :label="getCtrMeta(ctrId).label">
            <!--表單item元件,採用動態元件的方式-->
            <component
              :is="ctlList[getCtrMeta(ctrId).controlType]"
              v-model="formModel[getCtrMeta(ctrId).colName]"
              :meta="getCtrMeta(ctrId)"
              @myChange="mySubmit">
            </component>
          </el-form-item>
        </el-col>
      </el-row>
    </el-form>
  </div>

js

import { watch } from 'vue'
import elFormConfig from '@/components/nf-el-form/el-form-map.js'
import formManage from '@/components/nf-el-form/el-form-manage.js'

export default {
  name: 'el-form-div',
  components: {
    ...elFormConfig.formItemList
  },
  props: {
    modelValue: Object,
    partModel: Object,
    meta: Object
  },
  setup (props, context) {
    // 控制元件字典
    const ctlList = elFormConfig.formItemListKey

    // 表單管理類
    const {
      formModel, // 依據meta,建立 Model
      formColSpan, // 依據meta,建立 span
      formColSort,
      setFormColSpan,
      setFormColSort, // 設定元件排序
      mySubmit
    } = formManage(props, context)

    // 監聽列數的變化
    watch(() => props.meta.formColCount, (v1, v2) => {
      setFormColSpan()
    })
    // 監聽reload
    watch(() => props.meta.reload, (v1, v2) => {
      setFormColSpan()
      setFormColSort()
    })

    // 監聽元件值的變化,
    // 依據ID獲取元件的meta,因為model不支援【】巢狀
    const getCtrMeta = (id) => {
      return props.meta.itemMeta[id] || {}
    }

    return {
      formModel,
      formColSpan,
      formColSort,
      ctlList,
      getCtrMeta,
      mySubmit
    }
  }
}

這裡就簡單多了,因為實現具體功能的js程式碼都分離出去了。要麼做成子元件,要麼組成獨立的js檔案。
這裡主要就是負責重新渲染表單元件。

表單驗證

這個使用 el-form 提供的驗證功能。
目前暫時還沒有歸納好 el-form 的驗證,因為需要把這個驗證用的資料寫入到json裡面,然後讀取出來設定好即可。
所以肯定沒難度,只是需要點時間。

支援 擴充套件元件

自帶的元件肯定是不夠的,因為使用者的需求總是千變萬化的,那麼新元件如何加入到表單控制元件裡面呢?可以按照介面定義封裝成符合要求的元件,然後做一個map字典,就可以設定進去了。

因為介面統一,所以可以適應表單控制元件的呼叫。

簡單的方法是,直接修改兩個js檔案。
如果不方便修改的話,也可以通過屬性傳遞進來。目前暫時還沒有想好細節,不過似乎不是太難。

原始碼

https://github.com/naturefwvue/nf-vue-element

相關文章