在实现表单功能的时候,我们经常会选择使用el-form,但是如果表单项比较多,使用和修改起来都会比较繁琐,那可不可以通过配置项来生成表单呢,说干就干。通过二次封装el-form实现下面的需求。
一、封装布局组件
1、封装grid网格布局组件
先不要急着去实现,思考观察一下,整个页面就是一个表单,它大致分为两列,那么我们首先要考虑的是如何布局,这里我使用的是grid布局,先封装一个网格布局盒子。盒子的样式通过计算属性绑定,计算属性整合默认样式和通过配置项传入的样式。下面就实现了一个两列的网格布局。
Grid.js
<template>
<div :style="style">
<slot></slot>
</div>
</template>
<script>
export default {
name: 'Grid',
props: {
// 传递配置详情可以参考网格布局属性
gridProps: {
type: Object,
default: () => {}
}
},
computed: {
style () {
return {
display: 'grid',
gridGap: '10px',
gridTemplateColumns: `repeat(2, minmax(300px, 1fr))`,
gridAutoRows: '50px',
gridTemplateRows: '50px',
alignItems: 'center',
...this.gridProps
}
}
}
}
</script>
<style scoped>
</style>
2、封装网格布局的单元格组件
整体的组件封装完毕后,对网格的单元格也进行一次封装,逻辑大致和上面一样,样式也是可以通过配置项传入。
GridItem.js
<template>
<div :style="gridItemProps">
<slot></slot>
</div>
</template>
<script>
export default{
name: 'GridItem',
props: {
// 传递配置详情可以参考网格布局单元格属性
gridItemProps: {
type: Object,
default: () => {}
}
}
}
</script>
<style scoped>
</style>
布局到这儿就搞定了,步入正题开始写表单组件,行数有点多,请耐心观看。
二、封装表单组件
为什么最外层需要包裹一个el-form,大家应该都知道,第一是绑定表单数据,第二是绑定表单的校验规则等。当然,这里都是使用时传入的。引入上面封装好的布局组件,每个单元格组件中放入一个表单项,表单项通过遍历传入的配置项渲染。在这里除了表单项的渲染外还加入了标题,标题占独占一行,默认就是两列。表单项则通过component组件进行动态渲染,考虑到自己会定义一些组件以及el组件中有些需要选项的组件,这里把他单独拿出去封装,然后引入存放在对象里,动态渲染时先判断该对象里是否有相应的组件,没有就使用el的组件。
这里有一个重要的点,就是el组件的样式,它默认的宽度不是100%,所以我此处样式穿透,修改了它的宽度,使用时有组件宽度没有占满,可以在最下面我注释的地方设置其宽度
ProForm.js
<template>
<div>
<el-form v-bind="formProps" :model="formData">
<Grid :gridProps="gridProps">
<grid-item v-for="item in formColumns" :key="item.prop" :gridItemProps="item.gridItemProps" v-if="!item.isHide">
<!--标题-->
<div v-if="item.component==='title'" class="box-title"><span />{{item.label}}</div>
<!--表单项-->
<el-form-item v-else :label="item.label" :prop="item.prop" :formItemProps="item.formItemProps">
<component
:is='getComponent(item.component)'
v-model="formData[item.prop]"
:enums="item.enums"
v-bind="item.componentProps"
@formChange="formChange(item.prop, $event)"
/>
</el-form-item>
</grid-item>
</Grid>
</el-form>
</div>
</template>
<script>
import Grid from './Grid.vue'
import GridItem from './GridItem.vue'
// 该部分为自己封装的一些需要传递选项的el组件和自定义组件,设置静态的对象存储它们
import selectWidget from './components/select-widget.vue'
import cascaderWidget from './components/cascader-widget.vue'
import checkboxWidget from './components/checkbox-widget.vue'
import radioWidget from './components/radio-widget.vue'
import buttonWidget from './components/button-widget.vue'
const widgets = {selectWidget, cascaderWidget, checkboxWidget, radioWidget, buttonWidget}
export default {
name: 'ProForm',
components: {
Grid,
GridItem
},
props: {
// 网格布局配置
gridProps: {
type: Object,
default: () => {}
},
// 表单配置
formProps: {
type: Object,
default: () => {
return {
labelWidth: '100px', // 表单项标签宽度
rules: {} // 表单校验规则
}
}
},
// 表单项配置
formColumns: {
type: Array,
default: () => {
return {
prop: null, // 字段名
label: null, // 标签
component: null, // 组件
enums: null, // 枚举类型
isHide: false, // 是否隐藏
gridItemProps: null, // 网格单元格的配置
formItemProps: null, // 单一表单项的配置
componentProps: null // 组件的配置
}
}
},
// 表单数据
formData: {
type: Object,
default: () => {}
}
},
methods: {
// 获取对应组件
getComponent (name) {
return widgets[`${name}Widget`] || `el-${name}`
},
/**
* 向上抛表单项数据变化事件
* @param prop 字段名
* @param value 值
*/
formChange (prop, value) {
this.$emit('formChange', prop, value)
}
}
}
</script>
<style scoped>
.box-title {
display: flex;
width: 100%;
margin-bottom: 10px;
font-size: 16px;
font-weight: 600;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.box-title span{
display: inline-block;
width: 4px;
height: 18px;
margin-right: 5px;
margin-bottom: 3px;
vertical-align: middle;
background: #0f64a9;
}
/deep/ .el-form-item{
display: flex;
width: 100%;
}
/* 使表单项内容垂直居中*/
/deep/ .el-form-item__content{
display: flex;
flex: 1;
margin-left: 0 !important;
align-items: center;
}
/* 在这里修改element各个组件的宽度*/
/deep/ .el-select,
.el-date-editor{
width: 100%;
}
</style>
三、二次封装特殊的el组件
下面为el中需要选项的组件,把它拎出来二次封装,因为绑定的变量是表单组件传递过来的,所以在该组件中不能直接v-model绑定,需要代理处理一下。
<template>
<el-select v-bind="$attrs" v-model="vModelValue">
<el-option v-for="option in enums" :key="option.value" v-bind="option" />
</el-select>
</template>
<script>
import {vModelMixin} from '../../mixins'
export default {
name: 'SelectWidget',
mixins: [vModelMixin],
props: {
enums: {
type: Array,
default: () => []
}
}
}
</script>
<style></style>
四、编写代理对象的混合
export const vModelMixin = {
inheritAttrs: false,
// 更改v-model的默认属性名和事件名
model: {
prop: 'modelValue',
event: 'modelChange'
},
props: {
modelValue: {
default: ''
},
prop: {
type: String,
default: ''
}
},
computed: {
vModelValue: {
get () {
return this.modelValue
},
set (val) {
this.modelChange(val)
}
}
},
methods: {
// 值变化时触发该事件,同时向上抛出表单数据变化formChange事件,方便外面监听数据的变化
modelChange (val) {
this.$emit('modelChange', val)
this.$emit('formChange', val)
}
}
}
五、使用示例
到这里一个简单的自定义表单就写完了,如何使用呢,可以参考下面的示例
<template>
<div>
<pro-form :form-props="formProps"
:form-columns="formColumns"
:form-data="formData"></pro-form>
</div>
</template>
<script>
import ProForm from './ProForm.vue'
export default {
name: 'HelloWorld',
components: {ProForm},
data () {
return {
formColumns: [
{prop: 'title1', label: '基本信息', component: 'title', gridItemProps: {gridColumnStart: 'span 2'}},
{prop: 'name', label: '姓名', component: 'input'},
{prop: 'sex',
label: '性别',
component: 'select',
// isHide: true,
enums: [
{label: '男', value: '0'},
{label: '女', value: '1'}
]
},
{prop: 'button', label: '附件', component: 'button'},
{
prop: 'date',
label: '出生日期',
component: 'date-picker'
}
], // 表单项配置
formProps: {
labelWidth: '100px',
rules: {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' }
]
}
}, // 表单配置
formData: {
name: '帅哥',
sex: '1'
} // 表单绑定数据
}
}
}
</script>
<style scoped>
</style>
总结
最后感谢大家的观看,有不对的地方还请及时指正。本人前端菜鸟,上述内容都是在工作中所学,分享给大家,便于相互成长,相互进步。