什么?都2022年了,你还在一遍又一遍重复写form表单?
前言
在日常工作中,当需要处理的form
表单很多时,我们没有必要一遍又一遍地重复写form
表单,直接封装一个组件去处理就好。其实很早之前就有涉猎通过使用类似配置json
方法写form
表单的文章,虽然当时也没怎么认真看...我们前端组
也是使用这种思想配置的。然而,这个思想和方法很早就有出现过,并不怎么新颖,还望不喜勿喷...在此我封装了一个最最最基础的form
表单,离我们前端组
封装的组件差距还很大
,有兴趣朋友们的可以继续往下完善。有封装不好或者值得改进的地方,欢迎各路大佬在评论区里指点江山。
核心思想:
- 通过配置
js
文件的变量,使用vue
的is
属性动态切换组件,默认显示的组件为el-input
- 通过
element
的分栏和栅格属性,对form
表单进行响应式布局 baseForm
在组件初始化时,需要动态添加校验规则
、请求接口
以及初始化form
的部分值- 正统思想是对
element
组件的各个组件进行二次封装,然后通过is
属性切换二次封装后的组件,在此不做过多描述,有兴趣的朋友可以自行研究 - 更好的思想是
将页面请求、搜索项、表格、分页
封装到一起,形成一个整体,这也是我们前端小组目前的处理思路
实现重点:
- 任何标签或者组件都可以通过
vue的is
属性来动态切换组件。本组件中使用div
,将它的宽度设置为100%
,使得element
组件能够完全撑开。(使用vue内置组件component
会与el-radio-group
相冲突,因为它底层就是用component
实现的) - 表单上添加
validate-on-rule-change="false"
属性,防止在表单初始化时就校验表单 - 当为对象添加不存在的字段属性时,需要使用
$set
实现数据的响应式 - 如果
form
表单中只有一个输入框,在输入框中按下回车会提交表单,刷新页面。为了阻止这一默认行为,需要在el-form
标签上添加@submit.native.prevent
- 使用
lodash
中的get
方法获取对象的属性值,如果属性值不存在,可以给一个默认值 baseForm
子组件中可以传一个form
对象给父组件,那么添加或者编辑form
对象,就都可以在父组件中进行。
`表单双向绑定的方式有两种`:
1.使用v-model进行双向绑定
<div
v-else
clearable
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`)进行双向绑定
<div
v-else
clearable
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
组件,默认为el-input
)dic
(非接口请求的静态表单数据,使用{label以及value字段}
表示的数组形式)
column数组中每一个对象对应的字段(请求接口):
url
(接口的api
地址)requestParams
(非必填项,需要额外传入的传参)requestLabel
(接口返回对应的id
)requestValue
(接口返回对应的value
)
效果浏览
源码放送
1. baseForm组件
<template>
<el-form
ref="form"
:model="form"
:rules="formRules"
:size="get(option, 'formSize', defaultFormSize)"
:validate-on-rule-change="false"
@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.slotName"
:name="column.slotName"
:form="form"
:prop="column.prop"
:value="form[column.prop]"
></slot>
<div
v-else
clearable
style="width: 100%"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
v-model="form[column.prop]"
:placeholder="getPlaceholder(column.type, column.label)"
:label-width="get(column, 'size', column || defaultFormSize)"
:disabled="get(column, 'disabled', false)"
:is="get(column, 'type', 'el-input')"
>
<template v-if="column.type == 'el-select'">
<el-option
v-for="item in column.dic"
:key="item.value"
:label="item.label"
:value="item.value"
>
</el-option>
</template>
<template v-if="column.type == 'el-radio-group'">
<el-radio
v-for="item in column.dic"
:key="item.value"
:label="item.label"
>
{{ item.value }}
</el-radio>
</template>
<template v-if="column.type == 'el-checkbox-group'">
<el-checkbox
v-for="item in column.dic"
:key="item.label"
:label="item.value"
>
{{ item.label }}
</el-checkbox>
</template>
</div>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script>
import get from 'lodash/get'
import request from '@/service/request'
export default {
props: {
option: {
type: Object,
default: () => {}
},
form: {
type: Object,
default: () => {}
}
},
data() {
return {
formRules: {},
defaultFormSize: 'small',
defaultLabelWidth: '90px',
defaultLabelHeight: '50px',
selectList: ['el-radio-group', 'el-checkbox-group','el-select'],
radioList: ['el-radio-group', 'el-checkbox-group'],
page: { pageIndex: 1, pageSize: 0 }
}
},
computed: {
formColumn() {
return this.option.column
}
},
created() {
this.initRules()
this.initRequest()
this.initCheck()
},
methods: {
get,
async validate() {
try {
return await this.$refs.form.validate()
} catch {
return false
}
},
getPlaceholder(type, label) {
return type == 'el-select' ? `请选择${label}` : `请输入${label}`
},
initRequest() {
if (!Array.isArray(this.formColumn)) return
// 根据实际请求接口地址的前缀来判断
const urls = this.formColumn?.filter((item) => item.url && item.url.indexOf('/emes') == 0) || []
const { page } = this
urls.forEach(async (item) => {
const data = { page, ...item.requestParams }
const { detail } = await request({
url: item.url,
method: 'post',
data
}) || []
const 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 == 'el-radio-group' ? item.dic[0].label : [item.dic[0].value])
})
}
}
}
</script>
2. 父组件
<template>
<div class="app-container">
<myForm :option="option" :form="form">
<template #usageSlot="{form, prop}">
<el-input
size="small"
placeholder="请输入插槽使用"
v-model="form[prop]"
clearable
>
</el-input>
</template>
</myForm>
</div>
</template>
<script>
import { option } from './const.js'
export default {
data() {
return {
option,
form: {}
}
}
}
</script>
3. 配置项
export const option = {
column: [
{
label: '姓名',
prop: 'name',
span: 8,
rules: [
{
required: true
}
]
},
{
label: '职业',
prop: 'job',
type: 'el-select',
span: 8,
dic: [
{
label: '教师',
value: 0
},
{
label: '程序猿',
value: 1
},
{
label: '作家',
value: 2
}
],
rules: [
{
required: true
}
]
},
{
label: '性别',
prop: 'sex',
span: 8,
type: 'el-radio-group',
dic: [
{
label: 0,
value: '男'
},
{
label: 1,
value: '女'
}
],
rules: [
{
required: true
}
]
},
{
label: '城市',
prop: 'city',
type: 'el-checkbox-group',
span: 8,
dic: [
{
label: '仙桃',
value: 0
},
{
label: '泉州',
value: 1
},
{
label: '武汉',
value: 2
}
],
rules: [
{
required: true
}
]
},
{
label: '出生日期',
prop: 'data',
type: 'el-date-picker',
span: 8,
rules: [
{
required: true
}
]
},
{
label: '测试',
prop: 'test',
type: 'el-select',
span: 8,
url:'/emes/factoryOrderService/warehouse/list',
requestLabel: 'warehouseName',
requestValue: 'id',
rules: [
{
required: true
}
]
},
{
label: '插槽使用',
prop: 'usage',
slotName: 'usageSlot',
span: 8,
rules: [
{
required: true
}
]
}
]
}
4. 添加或编辑
- 添加: 如果是添加状态,直接在父组件中引入就好。在点击确定按钮时,使用以下代码进行校验,校验通过后继续往下走逻辑,否则就
return
掉;
try {
await this.$refs.form.validate()
} catch (error) {
return false
}
- 编辑: 如果是编辑状态,则需要在父组件页面初始化时,
先解构后端返回的数据
,再重新分配对象的内存空间。或者将后端返回的数据
使用$set
进行初始赋值,其余操作同添加状态。
initForm() {
if (this.type == 'add') return
const { categoryName, sortNumber } = this.selectData
this.form = {
categoryName,
sortNumber
}
}
结语
因为时间有限,封装的这个组件功能也比较有限。欢迎感兴趣的小伙伴在评论区一鸣惊人
!
转载自:https://juejin.cn/post/7122344763403010055