17354
在日常工作中,当需要处理的
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组件的整体大小)
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
(以函数的形式,返回一个对象,由于控制其他表单项的显示与隐藏)
column数组中每一个对象对应的字段(请求接口):
url
(接口的api
地址)
requestParams
(非必填项,需要额外传入的传参)
requestLabel
(接口返回对应的id
)
requestValue
(接口返回对应的value
)
handleDic
(格式化下拉列表所展示数据的方法,有两个参数。第一个参数为接口返回的数据,第二个参数为当前配置项)
Tips: 当配置项中同时存在dic
和url、requestLabel、requestValue
这三件套时,会以三件套请求的结果为准,因为在baseForm
子组件初始化时,有重新覆盖dic
的值。
: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
v-if="!column.hide"
:label="`${column.label}:`"
:prop="column.prop"
:label-width="get(column, 'labelWidth', column.labelWidth || defaultLabelWidth)"
:style="{
height: get(column, 'labelHeight', column.labelHeight || defaultLabelHeight)
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'
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: { 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: {
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',
}) || []
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',
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
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
因为时间有限,封装的这个组件功能也比较有限。欢迎感兴趣的小伙伴在评论区一鸣惊人
!