上一篇介紹了表單控件,這一篇介紹一下表單里面的各種子控件的封裝方式。
主要內(nèi)容
- 需求分析
- 子控件的分類
- 子控件屬性的分類
- 定義 interface。
- 定義子控件的的 props。
- 定義 json 文件。
- 基于 UI庫 進(jìn)行二次封裝,實現(xiàn)依賴 json 渲染。
- 通過 slot 、 “字典”,實現(xiàn)自定義子控件。
- 做個工具維護(hù) json 文件。(下篇介紹)
需求分析
表單里面需要各種各樣的子控件,像文本、數(shù)字、選擇、日期等常見的需求,可以由內(nèi)部提供組件解決,但是其他各種“奇奇怪怪”的需求怎么辦呢?
如果還是由“內(nèi)部”提供組件的話,那肯定是行不通的,因為以往的經(jīng)驗教訓(xùn)告訴我們,內(nèi)部不斷擴(kuò)充子控件的結(jié)果,必然會導(dǎo)致內(nèi)部代碼越來越臃腫,以至后期無法維護(hù),最終崩盤!
所以必須支持自定義擴(kuò)展!感謝 Vue 和 UI庫,提供基礎(chǔ)的技術(shù)支持,讓擴(kuò)展變得非常容易。
我們先對表單子控件進(jìn)行一下分類,然后為其設(shè)計一套接口,即定義一套規(guī)則,這樣才好方便做長期維護(hù)。
子控件的分類
我們對常見的組件進(jìn)行分析,得到了下面的分類:

上圖涵蓋了一些常用控件,但是很顯然并不全面,比如沒有金額類的控件,輸入金額也是需要一些輔助的,比如金額的大小寫的切換等,不過這些應(yīng)該用擴(kuò)展的方式實現(xiàn)。
屬性的分類
組件的分類可以做的“規(guī)整”一些,但是組件的屬性的分類,就比較有難度了,我們可以把組件需要的屬性分為三個主要部分:代碼里需要的、共用的、擴(kuò)展的。
低代碼需要的屬性
需要在代碼里面使用的屬性,比如字段名稱、控件類型、默認(rèn)值、防抖延遲等,集中在一起,通過 props 的方式傳遞。共用屬性
各個組件(或者大部分組件)都需要的屬性,比如浮動提示、size、是否顯示清空按鈕等,作為一級屬性,通過 props 的方式傳遞。擴(kuò)展屬性
某個組件需要的屬性,比如數(shù)字組件需要 max、min、step等。通過 $attrs 的方式傳遞。

其中擴(kuò)展屬性最為復(fù)雜,如果按照面向?qū)ο蟮姆绞絹碓O(shè)計的話,結(jié)構(gòu)就會非常復(fù)雜,會復(fù)雜到什么程度呢?可以參考當(dāng)初 asp.net 里面 webform 的繼承結(jié)構(gòu):

(controll是控件(組件)的意思,下面分出來WebControll和 repeater 兩個子類,然后又,,,算了不說了,是不是看著就很累的樣子?)
定義接口
現(xiàn)在是 JS 環(huán)境,我們沒有必要生搬硬套,而是可以利用JS的靈活性來做簡潔設(shè)計:

我們給表單子控件的 props 定義一個interface:(雖然暫時用不上)
- IFormItemProps
/**
* 表單控件的子控件的 props。
*/
export interface IFormItemProps {
/**
* 低代碼需要的數(shù)據(jù)
*/
formItemMeta: IFormItemMeta,
/**
* 子控件備選項,一級或者多級
*/
optionList: Array<IOptionList | IOptionTree>,
/**
* 表單的 model,含義多個屬性
*/
model: any,
/**
* 是否顯示清空的按鈕
*/
clearable: boolean,
/**
* 浮動提示信息
*/
title: string,
/**
* 子控件的擴(kuò)展屬性
*/
[key: string]: any
}
- IFormItemMeta 的定義:
/**
* 子控件的低代碼需要的數(shù)據(jù)
*/
export interface IFormItemMeta {
/**
* -- 字段ID、控件ID
*/
columnId?: number | string,
/**
* -- 字段名稱
*/
colName: string,
/**
* -- 字段的中文名稱,標(biāo)簽
*/
label?: string,
/**
* -- 子控件類型,number,EControlType
*/
controlType: EControlType | number,
/**
* 子控件的默認(rèn)值
*/
defValue: any,
/**
* -- 一個控件占據(jù)的空間份數(shù)。
*/
colCount?: number,
/**
* 訪問后端API的配置信息,有備選項的控件需要
*/
webapi?: IWebAPI,
/**
* -- 防抖延遲時間,0:不延遲
*/
delay: number,
/**
* 防抖相關(guān)的事件
*/
events?: IEventDebounce,
}
規(guī)則定義之后呢,總會發(fā)現(xiàn)有特例的屬性,比如 select 的 option。代碼里面需要使用 option 去綁定組件,應(yīng)該放在“低代碼需要的屬性”里面。
但是實際使用的時候發(fā)現(xiàn),放在“共用屬性”里面會更方便。
然后在做“維護(hù)JSON的小工具”的時候,發(fā)現(xiàn)需要放在“擴(kuò)展屬性”里面維護(hù),這樣維護(hù)代碼更容易實現(xiàn)。
綜合考慮之后,就出現(xiàn)了一個不符合規(guī)則的屬性 —— optionList。
定義組件的 props。
按照接口實現(xiàn)一下 props 的定義。
import type { PropType } from 'vue'
import type {
IOptionList,
IOptionTree,
IFormItemProps
} from '../types/20-form-item'
/**
* 基礎(chǔ)控件的共用屬性,即表單子控件的基礎(chǔ)屬性
*/
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,整體傳入,便于子控件維護(hù)字段值。
*/
model: {
type: Object
},
/**
* 是否顯示可清空的按鈕,默認(rèn)顯示
*/
clearable: {
type: Boolean,
default: true
},
/**
* 浮動的提示信息,部分控件支持
*/
title: {
type: String,
default: ''
}
}
其他屬性以及擴(kuò)展屬性,可以通過 $attrs 傳遞和綁定,這樣可以方便各種擴(kuò)展。
定義 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": "數(shù)字類"},
{"value": 3, "label": "日期類"},
{"value": 4, "label": "時間類"},
{"value": 5, "label": "選擇類"},
{"value": 6, "label": "下拉類"}
]
}
基于 UI庫 封裝,實現(xiàn)依賴 json 渲染。
首先要感謝強(qiáng)大的UI庫,實現(xiàn)了大部分的功能,我們只需要再稍微封裝一下即可,只有少數(shù)幾個組件需要我們補(bǔ)充點代碼。
文本類
- 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" 綁定擴(kuò)展屬性
- 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 // 基礎(chǔ)屬性
},
emits: ['update:modelValue'],
setup (props, context) {
const {
value,
run,
clear
} = itemController(props, context.emit)
return {
value,
run,
clear
}
}
})
使用 ...itemProps 定義屬性。
是不是很簡單。
可能你會問了,這不是封裝了個寂寞嗎,你看看里面空蕩蕩的,完全沒有封裝的必要嘛。
確實,對于文本這類簡單的組件,確實沒有封裝的必要,直接使用UI庫提供的組件即可。
那么為啥好要封裝一下呢?
首先為了統(tǒng)一風(fēng)格,不管是簡單的,還是復(fù)雜的,都按照統(tǒng)一方式封裝一下,這樣便于維護(hù)和擴(kuò)展。
日期類
- 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, // 基礎(chǔ)屬性
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)
// 根據(jù)類型判斷是否為數(shù)組,判斷是否 使用范圍。
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 // 控件值
}
}
})
可以增設(shè)屬性,然后根據(jù)需求設(shè)置默認(rèn)值,這樣方便統(tǒng)一風(fēng)格。
選擇類
- 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, // 基礎(chǔ)屬性
'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 的遍歷,實現(xiàn)了選項的渲染。
其他組件也是一樣的方式進(jìn)行封裝,就不一一介紹了。
封裝 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:實現(xiàn)多列
- transition:組件聯(lián)動的時候的動畫效果
- component:動態(tài)加載子控件
- formItemKey 子控件的字典,key-value形式,key就是控件編號,value是組件。這樣就可以根據(jù)控件的編號加載對應(yīng)的子控件了。
使用 slot 和 字典 實現(xiàn)擴(kuò)展自定義子控件。
這里要感謝強(qiáng)大的 vue3,提供了插槽這種很靈活的擴(kuò)展方式。以及組件的形成管理代碼。
說到擴(kuò)展,想必大家想到的是插槽,我們也支持使用插槽的擴(kuò)展方式,不過我覺得,既然定義了接口,那么不用的話,是不是有點浪費。
我們可以定義組件實現(xiàn)接口,然后并入字典(formItemKey),這樣表單控件就可以從字典里面加載我們自己定義的組件了,更便于管理和擴(kuò)展。
源碼和演示
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/