【摸魚神器】UI庫秒變低程式碼工具——表單篇(二)子控制項
上一篇介紹了表單控制項,這一篇介紹一下表單裡面的各種子控制項的封裝方式。
主要內容
- 需求分析
- 子控制項的分類
- 子控制項屬性的分類
- 定義 interface。
- 定義子控制項的的 props。
- 定義 json 文件。
- 基於 UI庫 進行二次封裝,實現依賴 json 渲染。
- 通過 slot 、 「字典」,實現自定義子控制項。
- 做個工具維護 json 文件。(下篇介紹)
需求分析
表單裡面需要各種各樣的子控制項,像文本、數字、選擇、日期等常見的需求,可以由內部提供組件解決,但是其他各種「奇奇怪怪」的需求怎麼辦呢?
如果還是由「內部」提供組件的話,那肯定是行不通的,因為以往的經驗教訓告訴我們,內部不斷擴充子控制項的結果,必然會導致內部程式碼越來越臃腫,以至後期無法維護,最終崩盤!
所以必須支援自定義擴展!感謝 Vue 和 UI庫,提供基礎的技術支援,讓擴展變得非常容易。
我們先對錶單子控制項進行一下分類,然後為其設計一套介面,即定義一套規則,這樣才好方便做長期維護。
子控制項的分類
我們對常見的組件進行分析,得到了下面的分類:
上圖涵蓋了一些常用控制項,但是很顯然並不全面,比如沒有金額類的控制項,輸入金額也是需要一些輔助的,比如金額的大小寫的切換等,不過這些應該用擴展的方式實現。
屬性的分類
組件的分類可以做的「規整」一些,但是組件的屬性的分類,就比較有難度了,我們可以把組件需要的屬性分為三個主要部分:程式碼里需要的、共用的、擴展的。
-
低程式碼需要的屬性
需要在程式碼裡面使用的屬性,比如欄位名稱、控制項類型、默認值、防抖延遲等,集中在一起,通過 props 的方式傳遞。 -
共用屬性
各個組件(或者大部分組件)都需要的屬性,比如浮動提示、size、是否顯示清空按鈕等,作為一級屬性,通過 props 的方式傳遞。 -
擴展屬性
某個組件需要的屬性,比如數字組件需要 max、min、step等。通過 $attrs 的方式傳遞。
其中擴展屬性最為複雜,如果按照面向對象的方式來設計的話,結構就會非常複雜,會複雜到什麼程度呢?可以參考當初 asp.net 裡面 webform 的繼承結構:
(controll是控制項(組件)的意思,下面分出來WebControll和 repeater 兩個子類,然後又,,,算了不說了,是不是看著就很累的樣子?)
定義介面
現在是 JS 環境,我們沒有必要生搬硬套,而是可以利用JS的靈活性來做簡潔設計:
我們給表單子控制項的 props 定義一個interface:(雖然暫時用不上)
- IFormItemProps
/**
* 表單控制項的子控制項的 props。
*/
export interface IFormItemProps {
/**
* 低程式碼需要的數據
*/
formItemMeta: IFormItemMeta,
/**
* 子控制項備選項,一級或者多級
*/
optionList: Array<IOptionList | IOptionTree>,
/**
* 表單的 model,含義多個屬性
*/
model: any,
/**
* 是否顯示清空的按鈕
*/
clearable: boolean,
/**
* 浮動提示資訊
*/
title: string,
/**
* 子控制項的擴展屬性
*/
[key: string]: any
}
- IFormItemMeta 的定義:
/**
* 子控制項的低程式碼需要的數據
*/
export interface IFormItemMeta {
/**
* -- 欄位ID、控制項ID
*/
columnId?: number | string,
/**
* -- 欄位名稱
*/
colName: string,
/**
* -- 欄位的中文名稱,標籤
*/
label?: string,
/**
* -- 子控制項類型,number,EControlType
*/
controlType: EControlType | number,
/**
* 子控制項的默認值
*/
defValue: any,
/**
* -- 一個控制項佔據的空間份數。
*/
colCount?: number,
/**
* 訪問後端API的配置資訊,有備選項的控制項需要
*/
webapi?: IWebAPI,
/**
* -- 防抖延遲時間,0:不延遲
*/
delay: number,
/**
* 防抖相關的事件
*/
events?: IEventDebounce,
}
規則定義之後呢,總會發現有特例的屬性,比如 select 的 option。程式碼裡面需要使用 option 去綁定組件,應該放在「低程式碼需要的屬性」裡面。
但是實際使用的時候發現,放在「共用屬性」裡面會更方便。
然後在做「維護JSON的小工具」的時候,發現需要放在「擴展屬性」裡面維護,這樣維護程式碼更容易實現。
綜合考慮之後,就出現了一個不符合規則的屬性 —— optionList。
定義組件的 props。
按照介面實現一下 props 的定義。
import type { PropType } from 'vue'
import type {
IOptionList,
IOptionTree,
IFormItemProps
} from '../types/20-form-item'
/**
* 基礎控制項的共用屬性,即表單子控制項的基礎屬性
*/
export const itemProps = {
formItemMeta: {
type: Object as PropType<IFormItemProps>,
default: () => {return {}}
},
/**
* optionList:IOptionList | IOptionTree,控制項的備選項,單選、多選、等控制項需要
*/
optionList: {
type: Object as PropType<Array<IOptionList | IOptionTree>>,
default: () => {return []}
},
/**
* 表單的 model,整體傳入,便於子控制項維護欄位值。
*/
model: {
type: Object
},
/**
* 是否顯示可清空的按鈕,默認顯示
*/
clearable: {
type: Boolean,
default: true
},
/**
* 浮動的提示資訊,部分控制項支援
*/
title: {
type: String,
default: ''
}
}
其他屬性以及擴展屬性,可以通過 $attrs 傳遞和綁定,這樣可以方便各種擴展。
定義 json 文件。
我們來定義一個示例用的 json文件。
{
"formItemMeta": {
"columnId": 90,
"colName": "kind",
"label": "分類",
"controlType": 107,
"isClear": false,
"defValue": 0,
"colCount": 7
},
"placeholder": "分類",
"title": "編號",
"optionList": [
{"value": 1, "label": "文本類"},
{"value": 2, "label": "數字類"},
{"value": 3, "label": "日期類"},
{"value": 4, "label": "時間類"},
{"value": 5, "label": "選擇類"},
{"value": 6, "label": "下拉類"}
]
}
基於 UI庫 封裝,實現依賴 json 渲染。
首先要感謝強大的UI庫,實現了大部分的功能,我們只需要再稍微封裝一下即可,只有少數幾個組件需要我們補充點程式碼。
文本類
- template
<el-input
v-model="value"
v-bind="$attrs"
:id="'c' + formItemMeta.columnId"
:name="'c' + formItemMeta.columnId"
:title="title"
:clearable="clearable"
@blur="run"
@change="run"
@clear="run"
@keydown="clear"
>
</el-input>
使用 v-bind="$attrs"
綁定擴展屬性
- ts
import { defineComponent } from 'vue'
import { ElInput } from 'element-plus'
// 引入組件需要的屬性、控制類
import { itemProps, itemController } from '@naturefw/ui-elp'
export default defineComponent({
name: 'nf-el-form-item-text',
inheritAttrs: false,
components: {
ElInput
},
props: {
modelValue: [String, Number],
...itemProps // 基礎屬性
},
emits: ['update:modelValue'],
setup (props, context) {
const {
value,
run,
clear
} = itemController(props, context.emit)
return {
value,
run,
clear
}
}
})
使用 ...itemProps
定義屬性。
是不是很簡單。
可能你會問了,這不是封裝了個寂寞嗎,你看看裡面空蕩蕩的,完全沒有封裝的必要嘛。
確實,對於文本這類簡單的組件,確實沒有封裝的必要,直接使用UI庫提供的組件即可。
那麼為啥好要封裝一下呢?
首先為了統一風格,不管是簡單的,還是複雜的,都按照統一方式封裝一下,這樣便於維護和擴展。
日期類
- template
<el-date-picker
ref="domDate"
v-model="value"
v-bind="$attrs"
:type="dateType"
:name="'c' + formItemMeta.columnId"
:format="format"
:value-format="valueFormat"
:title="title"
:clearable="clearable"
>
</el-date-picker>
- ts
import { defineComponent } from 'vue'
// 引入組件需要的屬性 引入表單子控制項的管理類
import { itemProps, itemController } from '@naturefw/ui-elp'
/**
* 日期
*/
export default defineComponent({
name: 'nf-el-from-item-date',
inheritAttrs: false,
props: {
...itemProps, // 基礎屬性
format: {
type: String,
default: 'YYYY-MM-DD'
},
'value-format': {
type: String,
default: 'YYYY-MM-DD'
},
modelValue: [String, Date, Number, Array]
},
emits: ['update:modelValue'],
setup (props, context) {
const { value } = itemController(props, context.emit)
// 根據類型判斷是否為數組,判斷是否 使用範圍。
let dateType = 'date'
if (props.formItemMeta.controlType == '125' ) {
dateType = 'daterange'
if (!Array.isArray(value.value)) {
value.value = []
}
} else {
if (Array.isArray(value.value)) {
value.value = ''
}
}
return {
dateType, // 控制項類型
value // 控制項值
}
}
})
可以增設屬性,然後根據需求設置默認值,這樣方便統一風格。
選擇類
- template
<el-select
v-model="value"
v-bind="$attrs"
:id="'c' + formItemMeta.columnId"
:name="'c' + formItemMeta.columnId"
:clearable="clearable"
:multiple="multiple"
:collapse-tags="collapseTags"
:collapse-tags-tooltip="collapseTagsTooltip"
>
<el-option
v-for="item in optionList"
:key="'select' + item.value"
:label="item.label"
:value="item.value"
:disabled="item.disabled"
>
</el-option>
</el-select>
- ts
import { defineComponent, computed } from 'vue'
// 引入組件需要的屬性 引入表單子控制項的管理類
import { itemProps, itemController } from '@naturefw/ui-elp'
export default defineComponent({
name: 'nf-el-from-select',
inheritAttrs: false,
props: {
...itemProps, // 基礎屬性
'collapse-tags': {
type: Boolean,
default: true
},
'collapse-tags-tooltip': {
type: Boolean,
default: true
},
modelValue: [String, Number, Array]
},
emits: ['update:modelValue'],
setup (props, context) {
const multiple = computed (() => props.formItemMeta.controlType === 161)
return {
...itemController(props, context.emit)
}
}
})
template 裡面增加了 el-option 部分,通過對 optionList 的遍歷,實現了選項的渲染。
其他組件也是一樣的方式進行封裝,就不一一介紹了。
封裝 el-form-item
el-table 通過 el-form-item 來載入子組件,所以我們也可以封裝一下:
<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].formItemMeta.label"
:prop="itemMeta[ctrId].formItemMeta.colName"
:rules="ruleMeta[ctrId] ?? []"
:label-width="itemMeta[ctrId].formItemMeta.labelWidth??''"
:size="size"
v-show="showCol[ctrId]"
>
<component
:is="formItemKey[itemMeta[ctrId].formItemMeta.controlType]"
:model="model"
v-bind="itemMeta[ctrId]"
>
</component>
</el-form-item>
</transition>
</el-col>
</el-row>
- el-row、el-col:實現多列
- transition:組件聯動的時候的動畫效果
- component:動態載入子控制項
- formItemKey 子控制項的字典,key-value形式,key就是控制項編號,value是組件。這樣就可以根據控制項的編號載入對應的子控制項了。
使用 slot 和 字典 實現擴展自定義子控制項。
這裡要感謝強大的 vue3,提供了插槽這種很靈活的擴展方式。以及組件的形成管理程式碼。
說到擴展,想必大家想到的是插槽,我們也支援使用插槽的擴展方式,不過我覺得,既然定義了介面,那麼不用的話,是不是有點浪費。
我們可以定義組件實現介面,然後併入字典(formItemKey),這樣表單控制項就可以從字典裡面載入我們自己定義的組件了,更便於管理和擴展。
源碼和演示
core://gitee.com/naturefw-code/nf-rollup-ui-controller
二次封裝: //gitee.com/naturefw-code/nf-rollup-ui-element-plus
演示: //naturefw-code.gitee.io/nf-rollup-ui-element-plus/