前言
在公司一直從事基於Vue框架後臺應用的前端研發,而該類應用的頁面有較多的通過表單交互來增刪改查的操作,爲了進行優雅的開發體驗,也有感於項目當前的代碼,遂封裝一個更合適的表單生成器form-generator.vue。
稍有從業經驗的人都曉得,這類生成器在基於iview
,ant-design
這樣的組件庫下實現不算複雜,因此這裏主要是闡述表單生成器的設計思路。
當前項目的表單生成操作有以下問題:
-
實現form生成功能的組件代碼僵硬,是通過
v-if
指令根據type
字段來判斷要渲染哪個組件,相應的schema如下:filterList: { list: { mallName: { name: 'mallId', displayName: '商場', type: 'SELECTION', notNull: true, value: '', options: { option: [] } }, billDate: { name: 'billDate', displayName: '賬單期', type: 'MONTH', notNull: false, value: '', options: { option: [] } } // ...... } }
如果有其他類型的組件要渲染,則要更改當前的form結構數據和生成器實現組件中的v-if
相關代碼。
-
可以看到,當前的form結構數據,將form的schema和相應的值
value
寫在同一個對象中,這樣,在將form結構數據寫在Vuex
類似的狀態庫中時,造成form結構數據和實際使用的代碼組件的上下文的不統一。 -
form結構的數據的構造較複雜,有很深的嵌套,代碼的可讀性不友好。
思路
Vue框架有內置組件component
,鏈接如下。用法是:渲染一個“元組件”爲動態組件。依 is
的值,來決定哪個組件被渲染。通過component
內置組件和is
特性,我們可以很優雅的實現不同類型組件的分發渲染。這樣我們只需要通過一個特定的字段componentType
指明當前要渲染的組件名,然後賦值給is
。而不需要冗長的v-if
相關代碼。
其次,我們將表單生成器的schema和表單最終存放的值一起存放到model中。
那麼,我們的表單生成器的結構就出來了。
{
model: {
mallId: '',
billDate: ''
},
schema: {
fields: [
{
name: 'mallId',
displayName: '商場',
componentType: 'Select',
placeholder: '不限',
required: true,
trigger: 'change',
propObj: {
transfer: true,
filterable: true,
clearable: true
},
options: []
},
{
name: 'billDate',
displayName: '賬單期',
componentType: 'DatePicker',
placeholder: '請選擇時間',
propObj: {
transfer: true,
type: 'month',
class: 'datePicker'
}
}
]
}
}
在實際生產使用中,schema部分存放在Vuex
狀態庫中,而model部分則存放在具體組件的data
中。這樣做的考慮在於:類似Select
組件的枚舉值是從服務端獲取到的,在接口請求到數據中當即將其設置到Vuex
狀態庫中,而具體頁面中存放的model對象可能會用於頁面組件其他部分代碼的操作。當然,也可以將這個form數據結構一起存放到Vuex
狀態庫中或者直接存放到具體組件的data
中。
結構解讀
整個對象存放兩個屬性:model和schema。
-
model對象用於存放最終和form交互到的值,form-generator.vue組件的model屬性即傳遞該值,並且使用了
sync
修飾符進行了數據的雙向綁定。 -
schema對象主要的屬性是fields,該字段是一個數組,用來存放具體待渲染爲form組件的數據結構。
-
屬性clearVerifyMsg用來驅動表單進行全部組件DOM層次的重置。
-
屬性colProp用於每個表單組件外層包裹的
ICol
組件的柵格寬度控制,默認值爲{ lg: 8, md: 12 }
,及其其他ICol
組件相關屬性。 -
fields數組下的每一項的屬性如下:
-
name:其值對應model中的字段名,在渲染的時候將model中的值雙向綁定到當前渲染的組件中。
-
componentType:
is
屬性來使用,用來確定當前要渲染的組件名 -
displayName:表單當前組件的label,會賦值給包裹着組件的
FormItem
外層組件的label
屬性中。 -
hide:使用
v-if
指令控制當前組件是否渲染 -
placeholder:佔位符
-
required:用在
FormItem
組件中,控制當前組件是否需要驗證 -
propObj:使用
v-bind="propObj"
語法來設置當前組件的props -
options:當前待渲染組件爲
Select
時,該組件的子組件Option
要渲染的列表 -
optPropObj:當前待渲染組件爲
Select
時,子組件Option
的屬性 -
append:當前待渲染組件爲
Input
時,對應slot
的值 -
handler:在組件觸發事件時,對當前組件值的特定的處理
-
trigger:控制當前組件在以什麼方式觸發值的校驗操作,默認爲blur
-
validator:使用了組件庫對應的驗證功能,當值爲
string
時,爲該類型,當值爲object
時,直接使用該值進行驗證,當值爲function
時,爲自定義的驗證器
-
-
其他
該組件基於iview
組件進行開發,會用到相應的組件和表單驗證功能。所以,該組件也可用於ant-design-vue
類似的組件庫。
缺點
- 由於使用了
component
內置組件進行相應組件的渲染分發,所以,對組件的屬性定製不友好,每個item
都需要傳入需要的props
,一種解決方案是:對該組件做一次包裹,然後componentType
屬性傳入包裹後的組件名。 - 每個
item
的on-change
事件的回調函數的入參個數不一,且還需傳入原生的event
對象,如何處理不同組件傳遞參數的問題還未很好處理。
具體代碼
<template>
<IForm
slot="from"
ref="formValidate"
:model="model"
:rules="ruleValidate"
:label-width="labelWidth"
:inline="inline"
>
<template v-for="(item, idx) in fieldList">
<ICol v-if="!item.hide" :key="idx" v-bind="colProp">
<FormItem
:label="item.displayName"
:prop="item.name"
:required="item.required"
>
<component
:is="item.componentType"
:ref="item.name"
v-model.trim="model[item.name]"
:placeholder="item.placeholder"
v-bind="item.propObj"
@on-change="(value, $event) => handleChange(value, $event, item)"
>
<template v-if="item.options">
<Option
v-for="(opt, i) in item.options"
:key="i"
:value="opt.value"
:disabled="opt.disabled"
v-bind="item.optPropObj"
>{{ opt.label }}</Option
>
</template>
<template v-if="item.append">
<span slot="append" :style="item.appendStyle">{{
item.append
}}</span>
</template>
</component>
</FormItem>
</ICol>
</template>
<slot name="bottom">
<div class="bottomBar" :style="bottomBarStyle">
<Button type="primary" @click="handleSubmit('formValidate')">{{
okText
}}</Button>
<Button
v-if="hasReset"
class="custom_btn"
@click="handleReset('formValidate')"
>重置</Button
>
<slot name="bottom-extra-btns" />
</div>
</slot>
</IForm>
</template>
<script>
const isObject = obj =>
Object.prototype.toString.call(obj).slice(8, -1) === 'Object'
export default {
name: 'FormGenerator',
props: {
model: {
type: Object,
default() {
return {}
}
},
schema: {
type: Object,
default() {
return {
fields: []
}
},
validator: function(obj) {
return obj.hasOwnProperty('fields') && Array.isArray(obj.fields)
}
},
inline: {
type: Boolean,
default: true
},
labelWidth: {
type: [Number, String],
default: 110
},
okText: {
type: String,
default: '查詢'
},
hasReset: {
type: Boolean,
default: true
},
bottomBarStyle: {
type: Object,
default() {
return {}
}
},
resetToDefaultValue: {
type: [Boolean, Object],
default: false
}
},
data() {
return {
ruleValidate: {}
}
},
computed: {
fieldList() {
return this.schema.fields
},
colProp() {
const obj = isObject(this.schema.colProp) ? this.schema.colProp : {}
return {
lg: 8,
md: 12,
...obj
}
}
},
watch: {
'schema.clearVerifyMsg': function(newVal) {
if (newVal) {
this.$refs['formValidate'].resetFields()
}
}
},
created() {
this.ruleInit()
},
methods: {
handleSubmit(name) {
this.$refs[name].validate(valid => {
if (valid) {
const result = JSON.parse(JSON.stringify(this.model))
this.$emit('on-submit', result)
}
})
},
handleReset(name) {
if (this.resetToDefaultValue) {
const newValue = isObject(this.resetToDefaultValue)
? this.resetToDefaultValue
: this.model
this.$emit('update:model', newValue)
} else {
this.$refs[name].resetFields()
}
this.$emit('on-reset')
},
handleChange(value, evt, item) {
const { handler } = item
let verifiedValue = ''
if (typeof handler === 'function') {
verifiedValue = handler(value, item, evt)
} else if (value instanceof InputEvent) {
verifiedValue = value.target.value
} else {
verifiedValue = value
}
if (verifiedValue !== undefined) {
this.$nextTick(() => {
this.$set(this.model, item.name, verifiedValue)
this.$emit('update:model', this.model)
})
}
},
ruleInit() {
this.fieldList.forEach(
({ required, validator, name, trigger = 'blur' }) => {
if (required) {
this.$set(this.ruleValidate, name, [
{
trigger,
required: true,
message: '必填項,不能爲空'
}
])
} else if (validator) {
if (typeof validator === 'string') {
this.$set(this.ruleValidate, name, [
{
trigger,
required: true,
type: validators,
message: `只能輸入${validator}類型`
}
])
} else if (typeof validator === 'function') {
this.$set(this.ruleValidate, name, [{ trigger, validator }])
} else if (
isObject(validator) &&
validator.hasOwnProperty('type')
) {
this.$set(this.ruleValidate, name, [validator])
} else {
throw new Error(`字段${name}的屬性type的類型錯誤`)
}
} else {
this.$set(this.ruleValidate, name, [])
}
}
)
}
}
}
</script>
<style lang="less" scoped>
.bottomBar {
text-align: right;
margin-left: 108px;
}
</style>