前言
在日常工作中,当需要处理的form
表单很多时,我们没有必要一遍又一遍地重复写form
表单,直接封装一个组件去处理就好。其实很早之前就有涉猎通过使用类似配置json
方法写form
表单的文章,虽然当时也没怎么认真看…我们前端组
也是使用这种思想配置的。然而,这个思想和方法很早就有出现过,并不怎么新颖,还望不喜勿喷…在此我封装了一个最最最基础的form
表单,离我们前端组
封装的组件差距还很大
,有兴趣朋友们的可以继续往下完善。有封装不好或者值得改进的地方,欢迎各路大佬在评论区里指点江山。
核心思想:
- 通过配置
js
文件的变量,使用vue
的is
属性动态切换组件,默认显示的组件为el-input
- 通过
element
的分栏和栅格属性,对form
表单进行响应式布局 baseForm
在组件初始化时,需要动态添加校验规则
、请求接口
以及初始化form
的部分值- 正统思想是对
element
组件的各个组件进行二次封装,然后通过is
属性切换二次封装后的组件,在此不做过多描述,有兴趣的朋友可以自行研究 - 更好的思想是
将页面请求、搜索项、表格、分页
封装到一起,形成一个整体,这也是我们前端小组目前的处理思路
实现重点:
- 任何标签或者组件都可以通过
vue
的is
属性来动态切换组件, 本组件使用component
12.27
新增: 对于component
动态组件,我们可以通过在父动态组件上绑定v-bind="column.props"
和v-on="getEvents(column)"
。将各小子组件需要接收的props
属性及事件传递
,配合各小组件上的v-bind="$attrs"
和v-on="$listeners"
,全部挂载到最小的积木子组件上。- 当为对象添加不存在的字段属性时,需要使用
$set
实现数据的响应式 - 如果
form
表单中只有一个输入框,在输入框中按下回车会提交表单,刷新页面。为了阻止这一默认行为,需要在el-form
标签上添加@submit.native.prevent
- 使用
lodash
中的get
方法获取对象的属性值,如果属性值不存在,可以给一个默认值 baseForm
父组件可以向子组件传一个form
对象。那么添加或者编辑form
对象,就都可以在父组件中进行。
`表单双向绑定的方式有两种`:
1.使用v-model进行双向绑定
<component
v-else
style="width: 100%"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
v-model="form[column.prop]"
:label-width="get(column, 'size', column || defaultFormSize)"
:disabled="get(column, 'disabled', false)"
:is="get(column, 'type', 'el-input')"
>
2.使用v-model的语法糖(`:value以及@input`)进行双向绑定
<component
v-else
style="width: 100%"
type="daterange"
range-separator="至"
start-placeholder="开始日期"
end-placeholder="结束日期"
:value="form[column.prop]"
:label-width="get(column, 'size', column || defaultFormSize)"
:disabled="get(column, 'disabled', false)"
:is="get(column, 'type', 'el-input')"
@input="input($event,column.prop)"
>
methods: {
input(e,prop) {
this.$set(this.form, prop, e)
}
}
配置项(本组件写得比较基础,目前仅支持element的五个常用组件):
整体字段:
formSize
(表单中各element组件的整体大小)
column数组中每一个对象对应的字段(非请求接口):
label
(表单label的名称)span
(这个表单项占据的份数,一行为24
,默认为12
)labelWidth
(这个表单项的label宽度
,默认为90px
)labelHeight
(这个表单项占据的高度
,默认为50px
)slotName
(插槽名)prop
(这个表单项绑定的属性名称)size
(这个表单项组件的大小,默认为small
)disabled
(是否禁用这个表单项)type
(使用二次封装的element
组件,功能不全,有兴趣的可以完善,默认为baseInput
)dic
(非接口请求的静态表单数据,使用{label以及value字段}
表示的数组形式)placeholder
(组件显示的placeholder
内容)callback
(小积木组件change
事件的回调函数,第一个参数为表单的值,第二个参数为配置项。ps: 当时封装组件时,由于项目比较赶搁置了,后续因为自己的惰性一直没有处理。于12.17下午看到这篇文章,应广大掘友们的建议,新增回调函数)validate
(校验方法,用来校验表单是否可以正常使用)props
(用于挂载各小积木组件需要接收的props
属性和事件传递。其中,props
里面非嵌套on
属性的地方用于接收props
属性,嵌套on
属性的地方用于接收小积木组件上的事件传递。)
column数组中每一个对象对应的字段(请求接口):
url
(接口的api
地址)requestParams
(非必填项,需要额外传入的传参)requestLabel
(接口返回对应的id
)requestValue
(接口返回对应的value
)handleDic
(格式化下拉列表所展示数据的方法,有两个参数。第一个参数为接口返回的数据,第二个参数为当前配置项)
Tips: 当配置项中同时存在dic
和url、requestLabel、requestValue
这三件套时,会以三件套请求的结果为准,因为在baseForm
子组件初始化时,有重新覆盖dic
的值。
效果浏览
组件架构
源码放送
1. baseForm组件
<template>
<el-form ref="form" v-bind="$attrs" v-on="$listeners" :model="form" :rules="formRules" :size="get(option, 'formSize', defaultFormSize)" @submit.native.prevent > <el-row :gutter="20" :span="24"> <el-col v-for="column in formColumn" :key="column.label" :md="column.span || 12" :sm="12" :xs="24"> <el-form-item :label="`${column.label}:`" :prop="column.prop" :label-width="get(column, 'labelWidth', column.labelWidth || defaultLabelWidth)" :style="{ height: get(column, 'labelHeight', column.labelHeight || defaultLabelHeight) }" > <slot v-if="column.slot" :name="column.slotName" :form="form" :prop="column.prop" :value="form[column.prop]" ></slot> <component v-else v-model="form[column.prop]" v-bind="column.props" v-on="getEvents(column)" :column="column" :placeholder="column.placeholder || getPlaceholder(column.type, column.label)" :label-width="get(column, 'size', column || defaultFormSize)" :disabled="get(column, 'disabled', false)" :is="get(column, 'type', 'baseInput')" @change="changeHandler(column)" > </component> </el-form-item> </el-col> </el-row> </el-form>
</template>
<script> import get from 'lodash/get' import request from '@/service/request' import baseCheckbox from './baseCheckbox' import baseInput from './baseInput' import baseRadio from './baseRadio' import baseSelect from './baseSelect' import baseTime from './baseTime' export default { components: { baseCheckbox, baseInput, baseRadio, baseSelect, baseTime }, props: { option: { type: Object, default: () => {} }, form: { type: Object, default: () => {} } }, data() { return { formRules: {}, defaultFormSize: 'small', defaultLabelWidth: '90px', defaultLabelHeight: '50px', selectList: ['baseRadio', 'baseCheckbox', 'baseSelect'], radioList: ['baseRadio', 'baseCheckbox'], // page最好封装成常量,放在const文件中。因为要展示,所以直接定义在这里 page: { pageIndex: 1, pageSize: 0 } } }, computed: { formColumn() { return this.option?.column || [] } }, created() { this.initRules() this.initRequest() this.initCheck() }, methods: { get, async validate() { try { await this.$refs.form.validate() return true } catch (error) { return false } }, getPlaceholder(type, label) { return type == 'el-select' ? `请选择${label}` : `请输入${label}` }, getEvents(data) { return data?.props?.on }, initRequest() { if (!Array.isArray(this.formColumn)) return // 根据实际请求接口地址的前缀来判断 const urls = this.formColumn?.filter((item) => item.url) const { page } = this urls.forEach(async (item) => { const data = { page, ...item.requestParams } `注意:之所以不在解构中,const { detail = [] }, 是因为解构出来的值,为undefined时才赋初始值` `如果结构出来的值为null,是不会赋初始值的` const { detail } = await request({ url: item.url, method: 'post', data }) || [] let finalResult if (item.handleDic) finalResult = item.handleDic(detail, item) else finalResult = detail.map((result) => ({ label: result[item.requestLabel], value: result[item.requestValue] })) this.$set(item, 'dic', finalResult) }) }, initRules() { if (!Array.isArray(this.formColumn)) return this.formColumn?.forEach((item) => { if (item.rules) { item.rules.map((rule, index) => { if (rule.required) { item.rules.splice(index, 1, { message: this.selectList.includes(item.type) ? `${item.label}必选` : `${item.label}必填`, ...rule }) } }) this.$set(this.formRules, item.prop, item.rules) } }) }, initCheck() { const selectList = this.formColumn.filter((item) => this.radioList.includes(item.type)) selectList.forEach((item) => { this.$set(this.form, item.prop, item.type == 'baseRadio' ? item.dic[0].label : [item.dic[0].value]) }) }, changeHandler(data) { data.callback && data.callback(this.form[data.prop], data) } } } </script>
2. baseInput组件
<template>
<el-input v-bind="$attrs" v-on="$listeners"></el-input>
</template>
3. baseCheckbox组件
<template>
<el-checkbox-group v-bind="$attrs" v-on="$listeners"> <el-checkbox v-for="item in column.dic" :key="item.label" :label="item.value"> {{ item.label }} </el-checkbox> </el-checkbox-group>
</template>
<script> export default { props: { column: Object } } </script>
4. baseRadio组件
<template>
<el-radio-group v-bind="$attrs" v-on="$listeners"> <el-radio v-for="item in column.dic" :key="item.value" :label="item.label"> {{ item.value }} </el-radio> </el-radio-group>
</template>
<script> export default { props: { column: Object } } </script>
5. baseSelect组件
<template>
<el-select size="small" v-bind="all$Attrs" v-on="$listeners"> <el-option v-for="item in column.dic" :key="item.value" :label="item.label" :value="item.value"> </el-option> </el-select>
</template>
<script> `定义默认配置项,当然你可以传入更多` const DEFAULT_OPTION = { size: 'small', clearable: true } export default { props: { column: Object }, computed: { `重构的绑定对象,配置的覆盖默认的` all$Attrs({ $attrs }) { return { ...DEFAULT_OPTION, ...$attrs } `使用下面这种需要给定一个空对象,不然DEFAULT_OPTION会被污染成为合并的结果` `1. 当然如果要封装得更细致些,需要定义所有支持的props(而不是随便一个属性都可以传进来), 对传入的$attrs进行过滤,把支持的保留下来` `2. 甚至我们可以引入lodash中的方法,对传入的props的格式进行处理(是否驼峰)` `3. 更甚至,整个form组件所有需要传递的props字段,都可以在option对象上以组件名命名key,并用v-bind进行绑定` return Object.assign({}, DEFAULT_OPTION, $attrs) } } } </script>
6. baseTime组件
<template>
<el-date-picker v-bind="$attrs" v-on="$listeners" type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" > </el-date-picker>
</template>
7. 配置项
export const option = {
column: [
{
label: '姓名',
prop: 'name',
span: 8,
callback: (data, form) => {
console.log('data', data)
console.log('form', form)
},
props: {
on: {
blur: (e) => {
console.log('e', e.target.value)
}
}
},
rules: [
{
required: true
}
]
},
{
label: '职业',
prop: 'job',
type: 'baseSelect',
// 为el-select开启搜索功能
props: {
filterable: true
},
span: 8,
dic: [
{
label: '教师',
value: 0
},
{
label: '程序猿',
value: 1
},
{
label: '作家',
value: 2
}
],
callback: (data, form) => {
},
rules: [
{
required: true
}
]
},
{
label: '性别',
prop: 'sex',
span: 8,
type: 'baseRadio',
dic: [
{
label: 0,
value: '男'
},
{
label: 1,
value: '女'
}
],
rules: [
{
required: true
}
]
},
{
label: '城市',
prop: 'city',
type: 'baseCheckbox',
span: 8,
dic: [
{
label: '仙桃',
value: 0
},
{
label: '泉州',
value: 1
},
{
label: '武汉',
value: 2
}
],
rules: [
{
required: true
}
]
},
{
label: '出生日期',
prop: 'data',
type: 'baseTime',
span: 8,
rules: [
{
required: true
}
]
},
{
// 测试接口的调用并展示数据
label: '测试',
prop: 'test',
type: 'baseSelect',
placeholder: 'test',
span: 8,
url: '/emes/factoryOrderService/warehouse/list',
requestLabel: 'warehouseName',
requestValue: 'id',
handleDic: (data, item) => {
return data.map((result) => ({
label: `test——${result[item.requestLabel]}`,
value: result[item.requestValue]
}))
},
rules: [
{
required: true
}
]
},
{
label: '插槽使用',
prop: 'usage',
slot: true,
slotName: 'usageSlot',
span: 8,
rules: [
{
required: true
}
]
}
]
}
8. 父组件
<template>
<div class="app-container"> <baseForm :option="option" :form="form"> <template #usageSlot="{ form, prop }"> <baseInput size="small" placeholder="请输入插槽使用" v-model="form[prop]" clearable></baseInput> </template> </baseForm> </div>
</template>
<script> import baseForm from './module/baseForm.vue' import baseInput from './module/baseInput.vue' import { option } from './module/const.js' export default { components: { baseForm, baseInput }, props: { msg: String }, data() { return { option, form: {} } } } </script>
9. 添加或编辑
- 添加: 如果是添加状态,直接在父组件中引入就好。在点击确定按钮时,使用以下代码进行校验,校验通过后继续往下走逻辑,否则就
return
掉;
async clickHandler() {
const valid = await this.$refs.form.validate()
if(!valid) return
}
- 编辑: 如果是编辑状态,则需要在父组件页面初始化时,
先解构后端返回的数据
,再重新分配对象的内存空间。或者将后端返回的数据
使用$set
进行初始赋值,其余操作同添加状态。
initForm() {
if (this.type == 'add') return
const { categoryName, sortNumber } = this.selectData
this.form = {
categoryName,
sortNumber
}
}
结语
因为时间有限,封装的这个组件功能也比较有限。欢迎感兴趣的小伙伴在评论区一鸣惊人
!
今天的文章什么?都2022年了,你还在一遍又一遍重复写form表单?分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/16374.html