前言
在日常工作中,当需要处理的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
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>
2.使用v-model的语法糖(`:value以及@input`)进行双向绑定
<component
v-else
:value="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')"
@input="input($event,column.prop)"
@change="changeHandler(column)"
>
</component>
methods: {
input(e,prop) {
this.$set(this.form, prop, e)
}
}
配置项(本组件写得比较基础,目前仅支持element的五个常用组件):
整体字段:
formSize
(表单中各element组件的整体大小)labelWidth
(表单label
整体宽度大小,优先级:子项label
宽度 > 表单整体label
宽度 > 默认宽度)labelPosition
(表单label
整体位置)
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
属性的地方用于接收小积木组件上的事件传递。)defaultValue
(设置表单的默认值)controls
(以函数的形式,返回一个对象,用于控制其他表单项的显示与隐藏)labelSlotName
(label
插槽, 可用于配置一些自定义样式,比如tooltip
)
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"
:label-position="option.labelPosition"
: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
v-if="!column.hide"
:label="`${column.label}:`"
:prop="column.prop"
:label-width="
get(
column,
'labelWidth',
column.labelWidth || option.labelWidth || defaultLabelWidth
)
"
:style="{
height: get(
column,
'labelHeight',
column.labelHeight || option.labelHeight || defaultLabelHeight
)
}"
>
<template #label>
<slot
v-if="column.labelSlotName"
:name="column.labelSlotName"
:column="column"
></slot>
</template>
<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 baseCheckbox from './baseCheckbox'
import baseInput from './baseInput'
import baseRadio from './baseRadio'
import baseSelect from './baseSelect'
import baseTime from './baseTime'
// axios的封装,用原生axios也可以
import request from '@/service/request'
// 后文会给出定义
import { validatenull } from '@/components/avue/utils/validate'
import get from 'lodash/get'
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 || []
},
controlOption({ formColumn }) {
return formColumn.filter(({ controls }) => controls) || []
}
},
watch: {
form: {
handler(val) {
this.controlOption.forEach((item) => {
const { controls, prop } = item
let control = controls(val[prop], val) || {}
Object.keys(control).forEach((key) => {
const data = this.formColumn.find(({ prop }) => prop == key)
const { hide } = control[key]
Object.assign(data, { hide: validatenull(hide) ? true : hide })
})
})
},
immediate: true,
deep: true
}
},
created() {
this.initRules()
this.initRequest()
this.initCheck()
this.initDefaultValue()
},
methods: {
get,
async validate() {
try {
await this.$refs.form.validate()
return true
} catch (error) {
return false
}
},
getPlaceholder(type, label) {
return `${type == 'el-select' ? '请选择' : '请输入'}${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: `${item.label}${this.selectList.includes(item.type) ? '必选' : '必填'}`,
...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)
},
initDefaultValue() {
this.formColumn.map(({ prop, defaultValue }) => defaultValue && this.$set(this.form, prop, defaultValue))
}
}
}
</script>
2. baseInput组件
<template>
<el-input v-bind="all$Attrs" v-on="$listeners"></el-input>
</template>
<script>
import formUtils from '../mixins/'
export default {
mixins: [formUtils]
}
</script>
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>
import formUtils from '../mixins/'
export default {
mixins: [formUtils],
props: {
column: Object
}
}
</script>
6. baseTime组件
<template>
<el-date-picker
v-bind="all$Attrs"
v-on="$listeners"
type="daterange"
range-separator="-"
start-placeholder="开始日期"
end-placeholder="结束日期"
>
</el-date-picker>
</template>
<script>
import formUtils from '../mixins/'
export default {
mixins: [formUtils]
}
</script>
7. 混入
export default {
data() {
return {
`定义公共的默认配置项,当然你可以传入更多`
`如果需要针对某些积木组件,特殊配置一些默认项,也可以不用混入,只在积木组件内单独处理`
DEFAULT_OPTION: {
size: 'small',
clearable: true
}
}
},
props: {
column: Object
},
computed: {
`重构的绑定对象,后续的配置覆盖默认的`
all$Attrs({ $attrs, DEFAULT_OPTION }) {
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)
}
}
}
8. 配置项
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,
defaultValue: 1,
controls: value => {
return {
hair: {
hide: {
0: true,
1: false,
2: true,
3: true
}[value]
}
}
},
dic: [
{
label: '教师',
value: 0
},
{
label: '程序猿',
value: 1
},
{
label: '作家',
value: 2
},
{
label: '警察',
value: 3
}
],
callback: (data, form) => {
},
rules: [
{
required: true
}
]
},
{
label: '发量',
prop: 'hair',
type: 'baseSelect',
span: 8,
props: {
filterable: true
},
dic: [
{
label: 'duang',
value: 0
},
{
label: '地中海',
value: 1
},
{
label: '稀疏',
value: 2
},
{
label: '正常',
value: 3
}
],
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
}
]
},
{
labelSlotName: 'testLabel',
prop: 'test',
type: 'baseInput',
placeholder: 'test',
span: 8
},
{
// 测试接口的调用并展示数据
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
}
]
}
]
}
9. 父组件
<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>
10. 公共方法
/**
* 判断是否为空
*/
export function validatenull(val) {
if (val instanceof Date || ['boolean', 'number', 'function'].includes(typeof val)) return false
if (val instanceof Array) {
if (val.length === 0) return true
} else if (val instanceof Object) {
for (var o in val) {
return false
}
return true
} else {
return val === 'null' ||
val == null ||
val === 'undefined' ||
val === undefined ||
val === ''
}
return false
}
11. 添加或编辑
- 添加: 如果是添加状态,直接在父组件中引入就好。在点击确定按钮时,使用以下代码进行校验,校验通过后继续往下走逻辑,否则就
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
}
}
结语
因为时间有限,封装的这个组件功能也比较有限。欢迎感兴趣的小伙伴在评论区一鸣惊人
!