上一篇介紹了表單控制元件,這一篇介紹一下表單裡面的各種子控制元件的封裝方式。
表單裡面需要各種各樣的子控制元件,像文字、數位、選擇、日期等常見的需求,可以由內部提供元件解決,但是其他各種「奇奇怪怪」的需求怎麼辦呢?
如果還是由「內部」提供元件的話,那肯定是行不通的,因為以往的經驗教訓告訴我們,內部不斷擴充子控制元件的結果,必然會導致內部程式碼越來越臃腫,以至後期無法維護,最終崩盤!
所以必須支援自定義擴充套件!感謝 Vue 和 UI庫,提供基礎的技術支援,讓擴充套件變得非常容易。
我們先對錶單子控制元件進行一下分類,然後為其設計一套介面,即定義一套規則,這樣才好方便做長期維護。
我們對常見的元件進行分析,得到了下面的分類:
上圖涵蓋了一些常用控制元件,但是很顯然並不全面,比如沒有金額類的控制元件,輸入金額也是需要一些輔助的,比如金額的大小寫的切換等,不過這些應該用擴充套件的方式實現。
元件的分類可以做的「規整」一些,但是元件的屬性的分類,就比較有難度了,我們可以把元件需要的屬性分為三個主要部分:程式碼裡需要的、共用的、擴充套件的。
低程式碼需要的屬性
需要在程式碼裡面使用的屬性,比如欄位名稱、控制元件型別、預設值、防抖延遲等,集中在一起,通過 props 的方式傳遞。
共用屬性
各個元件(或者大部分元件)都需要的屬性,比如浮動提示、size、是否顯示清空按鈕等,作為一級屬性,通過 props 的方式傳遞。
擴充套件屬性
某個元件需要的屬性,比如數位元件需要 max、min、step等。通過 $attrs 的方式傳遞。
其中擴充套件屬性最為複雜,如果按照物件導向的方式來設計的話,結構就會非常複雜,會複雜到什麼程度呢?可以參考當初 asp.net 裡面 webform 的繼承結構:
(controll是控制元件(元件)的意思,下面分出來WebControll和 repeater 兩個子類,然後又,,,算了不說了,是不是看著就很累的樣子?)
現在是 JS 環境,我們沒有必要生搬硬套,而是可以利用JS的靈活性來做簡潔設計:
我們給表單子控制元件的 props 定義一個interface:(雖然暫時用不上)
/**
* 表單控制元件的子控制元件的 props。
*/
export interface IFormItemProps {
/**
* 低程式碼需要的資料
*/
formItemMeta: IFormItemMeta,
/**
* 子控制元件備選項,一級或者多級
*/
optionList: Array<IOptionList | IOptionTree>,
/**
* 表單的 model,含義多個屬性
*/
model: any,
/**
* 是否顯示清空的按鈕
*/
clearable: boolean,
/**
* 浮動提示資訊
*/
title: string,
/**
* 子控制元件的擴充套件屬性
*/
[key: string]: any
}
/**
* 子控制元件的低程式碼需要的資料
*/
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 的定義。
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檔案。
{
"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庫,實現了大部分的功能,我們只需要再稍微封裝一下即可,只有少數幾個元件需要我們補充點程式碼。
<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"
繫結擴充套件屬性
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庫提供的元件即可。
那麼為啥好要封裝一下呢?
首先為了統一風格,不管是簡單的,還是複雜的,都按照統一方式封裝一下,這樣便於維護和擴充套件。
<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>
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 // 控制元件值
}
}
})
可以增設屬性,然後根據需求設定預設值,這樣方便統一風格。
<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>
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-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>
這裡要感謝強大的 vue3,提供了插槽這種很靈活的擴充套件方式。以及元件的形成管理程式碼。
說到擴充套件,想必大家想到的是插槽,我們也支援使用插槽的擴充套件方式,不過我覺得,既然定義了介面,那麼不用的話,是不是有點浪費。
我們可以定義元件實現介面,然後併入字典(formItemKey),這樣表單控制元件就可以從字典裡面載入我們自己定義的元件了,更便於管理和擴充套件。
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/