前面說了列表的低程式碼化的方法,本篇介紹一下表單的低程式碼化。
表單是很常見的需求,各種網頁、平臺、後臺管理等,都需要表單,有簡單的、也有複雜的,但是目的一致:收集使用者的資料,然後提交給後端。
表單控制元件的基礎需求:
el-form 實現了資料驗證、自定義擴充套件等功能(還有漂亮的UI),我們可以直接拿過來封裝,然後再補充點程式碼,實現多列、分欄、依賴 JSON 渲染等功能。
首先把表單控制元件需要的屬性分為兩大類:el-form 的屬性、低程式碼需要的資料。
整理一下做個腦圖:
我們轉換為介面的形式,再做個腦圖:
然後我們定義具體的 interface
/**
* 表單控制元件的屬性
*/
export interface IFromProps {
/**
* 表單的 model,物件,包含多個欄位。
*/
model: any,
/**
* 根據選項過濾後的 model,any
*/
partModel?: any,
/**
* 表單控制元件需要的 meta
*/
formMeta: IFromMeta,
/**
* 表單子控制元件的屬性,IFormItem
*/
itemMeta: IFormItemList,
/**
* 標籤的字尾,string
*/
labelSuffix: string,
/**
* 標籤的寬度,string
*/
labelWidth: string,
/**
* 控制元件的規格,ESize
*/
size: ESize,
/**
* 其他擴充套件屬性
*/
[propName: string]: any
}
本來想用這個介面約束元件的 props,但是有點小問題:
介面檔案應該可以在外部定義,然後引入元件。如果不能的話,那就尷尬了。
所以只好暫時放棄對元件的 props 進行整體約束。
/**
* 低程式碼的表單需要的 meta
*/
export interface IFromMeta {
/**
* 模組編號,綜合使用的時候需要
*/
moduleId: number | string,
/**
* 表單編號,一個模組可以有多個表單
*/
formId: number | string,
/**
* 表單欄位的排序、顯示依據,Array<number | string>,
*/
colOrder: Array<number | string>,
/**
* 表單的列數,分為幾列 number,
*/
columnsNumber: number
/**
* 分欄的設定,ISubMeta
*/
subMeta: ISubMeta,
/**
* 驗證資訊,IRuleMeta
*/
ruleMeta: IRuleMeta,
/**
* 子控制元件的聯動關係,ILinkageMeta
*/
linkageMeta: ILinkageMeta
}
/**
* 分欄表單的設定
*/
export interface ISubMeta {
type: ESubType, // 分欄型別:card、tab、step、"" (不分欄)
cols: Array<{ // 欄目資訊
title: string, // 欄目名稱
colIds: Array<number> // 欄目裡有哪些控制元件ID
}>
}
UI庫提供了 el-card、el-tab、el-step等元件,我們可以使用這幾個元件來實現多種分欄的形式。
el-form 採用 async-validator
實現資料驗證,所以我們可以去官網(https://github.com/yiminghe/async-validator)看看可以有哪些屬性,針對這些屬性指定一個介面(IRule),然後定義一個【欄位編號-驗證陣列】的介面(IRuleMeta)
/**
* 一條驗證規則,一個控制元件可以有多條驗證規則
*/
export interface IRule {
/**
* 驗證時機:blur、change、click、keyup
*/
trigger?: "blur" | "change" | "click" | "keyup",
/**
* 提示訊息
*/
message?: string,
/**
* 必填
*/
required?: boolean,
/**
* 資料型別:any、date、url等
*/
type?: string,
/**
* 長度
*/
len?: number, // 長度
/**
* 最大值
*/
max?: number,
/**
* 最小值
*/
min?: number,
/**
* 正則
*/
pattern?: string
}
/**
* 表單的驗證規則集合
*/
export interface IRuleMeta {
/**
* 控制元件的ID作為key, 一個控制元件,可以有多條驗證規則
*/
[key: string | number]: Array<IRule>
}
有時候需要根據使用者的選擇顯示對應的一組元件,那麼如何實現呢?其實也比較簡單,還是做一個key-value ,欄位值作為key,需要顯示的欄位ID集合作為value。這樣就可以了。
/**
* 顯示控制元件的聯動設定
*/
export interface ILinkageMeta {
/**
* 控制元件的ID作為key,每個控制元件值對應一個陣列,陣列裡面是需要顯示的控制元件ID。
*/
[key: string | number]: {
/**
* 控制元件的值作為key,後面的陣列裡存放需要顯示的控制元件ID
*/
[id: string | number]: Array<number>
}
}
interface 都定義好了,我們來定義元件的 props(實現介面)。
這裡採用 Option API 的方式,因為可以從外部檔案引入介面,也就是說,可以實現複用。
import type { PropType } from 'vue'
import type {
IFromMeta // 表單控制元件需要的 meta
} from '../types/30-form'
import type { IFormItem, IFormItemList } from '../types/20-form-item'
import type { ESize } from '../types/enum'
import { ESize as size } from '../types/enum'
/**
* 表單控制元件需要的屬性
*/
export const formProps = {
/**
* 表單的完整的 model
*/
model: {
type: Object as PropType<any>,
required: true
},
/**
* 根據選項過濾後的 model
*/
partModel: {
type: Object as PropType<any>,
default: () => { return {}}
},
/**
* 表單控制元件的 meta
*/
formMeta: {
type: Object as PropType<IFromMeta>,
default: () => { return {}}
},
/**
* 表單控制元件的子控制元件的 meta 集合
*/
itemMeta: {
type: Object as PropType<IFormItemList>,
default: () => { return {}}
},
/**
* 標籤的字尾
*/
labelSuffix: {
type: String,
default: ':'
},
/**
* 標籤的寬度
*/
labelWidth: {
type: String,
default: '130px'
},
/**
* 控制元件的規格
*/
size: {
type: Object as PropType<ESize>,
default: size.small
}
}
那麼如何使用呢?很簡單,用 import 匯入,然後解構即可。
// 表單控制元件的屬性
import { formProps, formController } from '../map'
export default defineComponent({
name: 'nf-el-from-div',
props: {
...formProps
// 還可以設定其他屬性
},
setup (props, context) {
略。。。
}
})
這樣元件裡的程式碼看起來也會很簡潔。
我們做一個簡單的 json 檔案:
{
"formMeta": {
"moduleId": 142,
"formId": 14210,
"columnsNumber": 2,
"colOrder": [
90, 101, 100,
110, 111
],
"linkageMeta": {
"90": {
"1": [90, 101, 100],
"2": [90, 110, 111]
}
},
"ruleMeta": {
"101": [
{ "trigger": "blur", "message": "請輸入活動名稱", "required": true },
{ "trigger": "blur", "message": "長度在 3 到 5 個字元", "min": 3, "max": 5 }
]
}
},
"itemMeta": {
"90": {
"columnId": 90,
"colName": "kind",
"label": "分類",
"controlType": 153,
"isClear": false,
"defValue": 0,
"extend": {
"placeholder": "分類",
"title": "編號"
},
"optionList": [
{"value": 1, "label": "文字類"},
{"value": 2, "label": "數位類"}
],
"colCount": 2
},
"100": {
"columnId": 100,
"colName": "area",
"label": "多行文字",
"controlType": 100,
"isClear": false,
"defValue": 1000,
"extend": {
"placeholder": "多行文字",
"title": "多行文字"
},
"colCount": 1
},
"101": {
"columnId": 101,
"colName": "text",
"label": "文字",
"controlType": 101,
"isClear": false,
"defValue": "",
"extend": {
"placeholder": "文字",
"title": "文字"
},
"colCount": 1
},
"110": {
"columnId": 110,
"colName": "number1",
"label": "數位",
"controlType": 110,
"isClear": false,
"defValue": "",
"extend": {
"placeholder": "數位",
"title": "數位"
},
"colCount": 1
},
"111": {
"columnId": 111,
"colName": "number2",
"label": "滾軸",
"controlType": 111,
"isClear": false,
"defValue": "",
"extend": {
"placeholder": "滾軸",
"title": "滾軸"
},
"colCount": 1
}
}
}
溫馨提示:JSON 檔案不需要手擼哦。
準備工作完畢,我們來二次封裝 el-table 元件。
<el-form
:model="model"
ref="formControl"
:inline="false"
class="demo-form-inline"
:label-suffix="labelSuffix"
:label-width="labelWidth"
:size="size"
v-bind="$attrs"
>
<el-row :gutter="15">
<el-col
v-for="(ctrId, index) in colOrder"
:key="'form_' + ctrId + index"
:span="formColSpan[ctrId]"
v-show="showCol[ctrId]"
><!---->
<transition name="el-zoom-in-top">
<el-form-item
:label="itemMeta[ctrId].label"
:prop="itemMeta[ctrId].colName"
:rules="ruleMeta[ctrId] ?? []"
:label-width="itemMeta[ctrId].labelWidth??''"
:size="size"
v-show="showCol[ctrId]"
>
<component
:is="formItemKey[itemMeta[ctrId].controlType]"
:model="model"
v-bind="itemMeta[ctrId]"
>
</component>
</el-form-item>
</transition>
</el-col>
</el-row>
</el-form>
通過 props 繫結 el-table 的屬性
props 裡面定義的屬性,直接繫結即可,比如 :label-suffix="labelSuffix"
。
通過 $attrs 繫結 el-table 的屬性
props 裡面沒有定義的屬性,會儲存在 $attrs 裡面,可以通過 v-bind="$attrs"
的方式繫結,既方便又支援擴充套件。
使用動態元件(component)載入表單子元件。
實現資料驗證,設定 rules 屬性即可,:rules="ruleMeta[ctrId] ?? []"
。
使用 el-row、el-col 實現多列的效果。
el-col 分為了24個格子,通過一個欄位佔用多少個格子的方式實現多列,也就是說,最多支援 24列。當然肯定用不了這麼多。
所以,我們通過各種引數計算好 span 即可。篇幅有限,具體程式碼不介紹了,感興趣的話可以看原始碼。
多列表單
因為 el-col 的 span 最大是 24,所以最多支援24列。
支援調整佈局
三列表單裡面 URL元件就佔用了一整行,這類的調整都是很方便實現的。
這裡分為多個表單控制元件,以便於實現多種分欄方式,並不是在一個元件內部通過 v-if 來做各種判斷,這也是我需要把 interface 寫在單獨檔案裡的原因。
<el-form
:model="model"
ref="formControl"
:inline="false"
class="demo-form-inline"
:label-suffix="labelSuffix"
:label-width="labelWidth"
:size="size"
v-bind="$attrs"
>
<el-tabs
v-model="tabIndex"
type="border-card"
>
<el-tab-pane
v-for="(item, index) in cardOrder"
:key="'tabs_' + index"
:label="item.title"
:name="item.title"
>
<el-row :gutter="15">
<el-col
v-for="(ctrId, index) in item.colIds"
:key="'form_' + ctrId + index"
:span="formColSpan[ctrId]"
v-show="showCol[ctrId]"
>
<transition name="el-zoom-in-top">
<el-form-item
:label="itemMeta[ctrId].label"
:prop="itemMeta[ctrId].colName"
v-show="showCol[ctrId]"
>
<component
:is="formItemKey[itemMeta[ctrId].controlType]"
:model="model"
v-bind="itemMeta[ctrId]"
>
</component>
</el-form-item>
</transition>
</el-col>
</el-row>
</el-tab-pane>
</el-tabs>
</el-form>
雖然表單控制元件可以預設一些表單子控制元件,比如文字、數位、日期、選擇等,但是客戶的需求是千變萬化的,固定的子控制元件肯定無法滿足客戶所有的需求,所以必須支援自定義擴充套件。
比較簡單的擴充套件就是使用 slot 插槽,el-table 裡面的 el-form-item 其實就是以 slot 的形式加入到 el-table 內部的。
所以我們也可以通過 slot 實現自定義的擴充套件:
<nf-form
v-form-drag="formMeta"
:model="model"
:partModel="model2"
v-bind="formMeta"
>
<template v-slot:text>
<h1>外部插槽 </h1>
<input v-model="model.text"/>
</template>
</nf-form>
nf-form 就是封裝後的表單控制元件,設定屬性和 model 後就可使用了,很方便。
如果想擴充套件的話,可以使用 <template v-slot:text>
的方式,裡面的 【text】 是欄位名稱(model 的屬性)。
也就是說,我們是依據欄位名稱來區分 slot 的。
雖然使用 slot 可以擴充套件子元件,但是對於子元件的結構複雜的情況,每次都使用 slot 的話,明顯不方便複用。
既然都定義 interface 了,那麼為何不實現介面製作元件,然後變成新的表單子元件呢?
當然可以了,具體方法下次再介紹。
為什麼要定義 interface ?
定義 interface 可以比較清晰的表明結構和意圖,然後實現介面即可。避免過段時間自己都忘記含義。
JSON 檔案匯入後會自動解析為 js 的物件,那麼還用 interface 做什麼?
這就比較尷尬了,也是我一直沒有采用 TS 的原因之一。
TS只能在編寫程式碼、打包時做檢查,但是在執行時就幫不上忙了,所以對於低程式碼的幫助有限。
core:https://gitee.com/naturefw-code/nf-rollup-ui-controller
二次封裝: https://gitee.com/naturefw-code/nf-rollup-ui-element-plus
演示: https://naturefw-code.gitee.io/nf-rollup-ui-element-plus/