一個成熟的表單
表單表單,你已經長大了,你要學會:
- 動態渲染
- 支援單列、雙列、多列
- 支援調整佈局
- 支援表單驗證
- 支援調整排列(顯示)順序
- 依據元件值顯示需要的元件
- 支援 item 擴充套件元件
- 可以自動建立 model
這個表單控制元件是基於 element-plus 的 el-form 做的二次封裝,所以首先感謝 element-plus 提供了這麼強大的UI庫,以前用 jQuery 做過類似的,但是非常麻煩,既不好看,可維護性、擴充套件性也差,好多想法都實現不了(技術有限)。
現在好了,站在巨人的肩膀上,實現自己的想法了。
實現動態渲染
把表單需要的屬性,統統放入json裡面,然後用require(方便) 或者aioxs(可以熱更新)載入進來,這樣就可以實現動態渲染了。
比如要實現公司資訊的新增、修改,那麼只需要載入公司資訊需要的json即可。
想要實現員工資訊的新增、修改,那麼只需要載入員工資訊需要的json。
總之,載入需要的json即可,不需要再一遍一遍的手擼程式碼了。
那麼這個神奇的 json 是啥樣子的呢?檔案有點長,直接看截圖,更清晰一些。
另外還有幾個附帶功能:
-
支援單行下的合併。
在單行的情況下,一些短的控制元件會比較佔空間,我們可以把多個小的合併到一行。 -
支援多行下的擴充套件。
多行的情況下,一些長的控制元件需要佔更多的空間,我們可以設定它多佔幾個格子。 -
自動建立表單需要的 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 了。
多列的表單
這個是最複雜的,分為兩種情況:單列的擠一擠、多列的搶位置。
單列
單列的表單有一個特點,一行比較寬鬆,那麼有時候就需要兩個元件在一行裡顯示,其他的還是一行一個元件,那麼要如何調整呢?
這裡做一個設定:
- 一個元件一行的,記做1
- 兩個元件擠一行的,記做-2
- 三個元件擠一行的,記做-3
以此類推,理論上最多支援 -24,當然實際上似乎沒有這麼寬的顯示器。
這樣記錄之後,我們就可以判斷,≥1的記做span=24,負數的,用24去除,得到的就是span的數字。當然記得取整數。
為啥用負數做標記呢?就是為了區分開多列的調整。
多列
調多了之後發現一個問題,看起來和單列調整後似乎一樣的。
多列的表單有一個特點,一個格子比較小,有些元件太長放不下,這個時候這個元件就要搶後面的格子來用。
那麼我們還是做一個設定:
- 一個元件佔一格的,還是記做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檔案。
如果不方便修改的話,也可以通過屬性傳遞進來。目前暫時還沒有想好細節,不過似乎不是太難。