1. 程式人生 > >Vue3元件(九)Vue + element-Plus + json = 動態渲染的表單控制元件

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

# 一個成熟的表單 表單表單,你已經長大了,你要學會: * 動態渲染 * 支援單列、雙列、多列 * 支援調整佈局 * 支援表單驗證 * 支援調整排列(顯示)順序 * 依據元件值顯示需要的元件 * 支援 item 擴充套件元件 * 可以自動建立 model > 這個表單控制元件是基於 **element-plus** 的 el-form 做的二次封裝,所以首先感謝 element-plus 提供了這麼強大的UI庫,以前用 jQuery 做過類似的,但是非常麻煩,既不好看,可維護性、擴充套件性也差,好多想法都實現不了(技術有限)。 現在好了,站在巨人的肩膀上,實現自己的想法了。 # 實現動態渲染 把表單需要的屬性,統統放入json裡面,然後用require(方便) 或者aioxs(可以熱更新)載入進來,這樣就可以實現動態渲染了。 比如要實現公司資訊的新增、修改,那麼只需要載入公司資訊需要的json即可。 想要實現員工資訊的新增、修改,那麼只需要載入員工資訊需要的json。 **總之,載入需要的json即可,不需要再一遍一遍的手擼程式碼了。** 那麼這個神奇的 json 是啥樣子的呢?檔案有點長,直接看截圖,更清晰一些。 ![006動態渲染需要的json.png](https://upload-images.jianshu.io/upload_images/25078225-144343abdf034cdb.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 另外還有幾個附帶功能: * 支援單行下的合併。 在單行的情況下,一些短的控制元件會比較佔空間,我們可以把多個小的合併到一行。 * 支援多行下的擴充套件。 多行的情況下,一些長的控制元件需要佔更多的空間,我們可以設定它多佔幾個格子。 * 自動建立表單需要的 model。 不需要手動寫 model了。 # 實現多行多列的表單 再次感謝 el-form,真的很強大,不僅好看,還提供了驗證功能,還有很多其他的功能。 只是好像只能橫著排,或者豎著排。那麼能不能多行多列呢?似乎沒有直接提供。 我們知道 el-row、el-col 可以實現多行多列的功能,那麼能不能結合一下呢?官網也不直說,害的我各種找,還好找到了。(好吧,其實折騰了一陣著的table) 二者結合一下就可以了,這裡有個小技巧,el-row 只需要一個就可以,el-col 可以有多個,這樣一行排滿後,會自動排到下一行。 ```js
``` * 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 是不是有點麻煩?如果能夠自動獲得該多好,於是我寫了這個函式。 ```js // 根據表單元素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,所以我又寫了一個函式 ```js // 依據使用者選項,建立對應的 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](https://upload-images.jianshu.io/upload_images/25078225-6b11f46b11eee46e.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 單列的表單有一個特點,一行比較寬鬆,那麼有時候就需要兩個元件在一行裡顯示,其他的還是一行一個元件,那麼要如何調整呢? 這裡做一個設定: * 一個元件一行的,記做1 * 兩個元件擠一行的,記做-2 * 三個元件擠一行的,記做-3 以此類推,理論上最多支援 -24,當然實際上似乎沒有這麼寬的顯示器。 這樣記錄之後,我們就可以判斷,≥1的記做span=24,負數的,用24去除,得到的就是span的數字。當然記得取整數。 為啥用負數做標記呢?就是為了區分開多列的調整。 ## 多列 ![008雙列表單.png](https://upload-images.jianshu.io/upload_images/25078225-60ce0f9770bf5732.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) *調多了之後發現一個問題,看起來和單列調整後似乎一樣的。* ![009三列表單.png](https://upload-images.jianshu.io/upload_images/25078225-fc0a55bcdaf2edc5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240) 多列的表單有一個特點,一個格子比較小,有些元件太長放不下,這個時候這個元件就要搶後面的格子來用。 那麼我們還是做一個設定: * 一個元件佔一格的,還是記做1 * 一個元件佔兩格的,記做2 * 一個元件佔三格的,記做3 以此類推。 這樣記錄之後,我們可以判斷,≤1的,記做 24 / 列數,大於1的記做 24/ 列數 * n。 這樣就可以了,可以相容單列的設定,不用因為單列變多列而調整設定。 只是有個小麻煩,佔得格子太多的話,就會提取擠到下一行,而本行會出現“空缺”。 這個暫時靠人工調整吧。 畢竟哪個欄位在前面,還是需要人工設定的。 一頓分析猛如虎,一看程式碼沒幾行。 ```js // 根據配置裡面的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](https://www.zhihu.com/zvideo/1347091197660405760) # 依據使用者的選擇,顯示對應的元件 這個也是一個急需的功能,否則的話,動態渲染的表單控制元件適應性就會受到限制。 其實想想也不難,就是改一下 formColSort 裡面的元件ID就好了。 我們設定一個watch來監聽元件值的變化,然後把需要的元件ID設定給 formColSort 就可以了。 ```js // 監聽元件值的變化,調整元件的顯示以及顯示順序 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](https://www.zhihu.com/zvideo/1347099700483457024) # 完整程式碼 上面的程式碼比較凌亂,這裡整體介紹一下。 * el-form-manage.js 表單元件的管理類,單獨拿出來,這樣就可以支援其他UI庫了,比如antdv ```js 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 動態元件需要的字典 ```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 表單控制元件的程式碼 模板 ```html ``` js ```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-