背景
项目中涉及非常多的查询表单、提交的表单,引用的组件库是 Element,使用 el-form 等组件进行二次封装成一个通用的定制表单组件,只需传入配置项即可使用。
一、考虑的点
- 表单项配置传入一个数组,使用 v-for 遍历数组添加表单项
- 页面宽度变化时可能会影响表单项的布局,引入 element 的 Layout 布局,保证每个表单项所占的宽度按比例,这样宽度变化时布局不受影响
- 表单项可能是输入框、选择器、时间选择器等多种控件,这些控件都选择 element 组件,使用 component 动态组件实现渲染不同的组件
- 表单项可能是只读的文本,并且支持传入格式化的函数
- 表单还需要保留原有的 label 插槽和默认的表单项插槽
- 表单的 label 可能会出现过长的情况,要求使用 … 省略显示,鼠标悬浮显示 el-tooltip 提示框显示完整的 label
- 表单项的校验规则支持在表单项配置中传入
- 自定义的通用表单组件的 props 除表单数据对象、表单项配置数组外,尽可能少,其他的 form 需要接收的属性和事件,可通过 $attrs、$listeners 进行传递,form-item 组件中的属性和事件则通过表单项配置中的 item.attrs 和 item.listeners 传递
- 表单项数组中传入的项可能只在某些情况下显示,所以对表单项增加 v-if = “!item.hidden” 进行条件渲染
二、组件实现
CustomForm.vue
<template>
<el-form
class="custom-form"
ref="customForm"
:model="formModel"
:label-width="labelWidth"
:label-position="labelPosition"
label-suffix=":"
v-bind="$attrs"
v-on="$listeners"
@keyup.enter.native="handleEnter"
@submit.native.prevent
>
<el-row :gutter="gutter">
<template v-for="item in formItems">
<el-col
v-if="!item.hidden"
:key="item.prop"
:span="item.span"
:offset="item.offset"
>
<el-form-item
:label="item.label || ''"
:label-width="item.labelWidth"
:prop="item.prop"
:rules="genRules(item)"
:ref="item.prop"
>
<!-- label 插槽 -->
<template #label>
<slot :name="item.labelSlot" v-if="item.labelSlot" :item="item" />
<el-tooltip
v-else
effect="light"
:content="item.label || ''"
placement="bottom"
:disabled="tooltipDisabled"
>
<span
class="ellipsis"
@mouseenter="spanMouseenter($event)">
{{ item.label || '' }}
</span>
</el-tooltip>
</template>
<!-- form-item 插槽 -->
<slot v-if="item.formItemSlot" :name="item.formItemSlot" />
<!-- 只读文本 -->
<span v-else-if="this.onlyRead || item.onlyRead">
{{ item.formatter ? item.formatter(formModel[item.prop]) : formModel[item.prop] }}
</span>
<!-- 表单元素组件 -->
<component
v-else
class="form-item-component"
:is="item.type"
v-model="formModel[item.prop]"
v-bind="item.attrs"
v-on="item.listeners"
>
<template v-if="item.type === 'el-select'">
<el-option
v-for="opt in getOptions(item.options)"
:key="opt.value"
:label="opt.label"
:value="opt.value"
:disabled="opt.disabled"
></el-option>
</template>
</component>
</el-form-item>
</el-col>
</template>
</el-row>
</el-form>
</template>
<script>
import { Form, FormItem, Row, Col, Option } from 'element-ui'
export default {
name: 'CustomForm',
props: {
gutter: {
type: Number,
default: 16
},
labelWidth: {
type: String,
default: '120px'
},
labelPosition: {
type: String,
default: 'left'
},
onlyRead: Boolean, // 只读
formItems: Array, // 表单配置
formModel: Object, // 表单数据
},
components: {
[Form.name]: Form,
[FormItem.name]: FormItem,
[Row.name]: Row,
[Col.name]: Col,
[Option.name]: Option,
},
data() {
return {
tooltipDisabled: false
}
},
computed: {},
methods: {
// 回车回调方法
handleEnter() {
this.$emit('enter')
},
// 获取当前表单项校验规则
genRules(item) {
const { rules, label, required = false } = item
return rules || [{ required, message: `${label}不能为空`, trigger: item.trigger || 'blur' }]
},
// 获取枚举
getOptions(option) {
return typeof option === 'function' ? option() : option
},
spanMouseenter(ev) {
if (ev.target.clientWidth < ev.target.scrollWidth) {
this.tooltipDisabled = false
} else {
this.tooltipDisabled = true
}
},
},
}
</script>
<style lang="scss" scoped>
.ellipsis {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
</style>
三、使用示例
template
<CustomForm
ref="form"
label-position="top"
:formModel="formModel"
:formItems="formItems"
@enter="handleQuery"
>
<template #amountSlot="{ item }">
{{ toThausands(formModel[item.prop]) }}
</template>
</CustomForm>
js - 配置项仅供参考
formModel: {
taxRate: '',
amount: '',
billStatus: ''
},
formItems: [
{
label: '推送状态',
prop: 'pushStatus',
type: 'el-select',
options: () => this.pushStatusOptions,
attrs: {
placeholder: '请选择...',
clearable: true
},
span: 6
},
{
label: '税率',
prop: 'taxRate',
type: 'text',
onlyRead: true,
formatter: val => {
if (val === 0) return 0
return val ? parseInt(val) + '%' : ''
},
span: 6
},
{
label: '金额(元)',
prop: 'amount',
formItemSlot: 'amountSlot',
span: 6
},
{
label: '隐藏',
prop: 'hide',
hidden: true
span: 6
},
// ...
]