一个基于Vue.js和Element UI框架的自定义表单Form组件。
这个组件非常灵活,支持自定义类名、表单配置项、动态新增表单项、自定义事件绑定等高级功能。
<template>
<el-form
class="t-form"
ref="form"
:class="className"
:model="formOpts.formData"
:rules="formOpts.rules"
:label-width="formOpts.labelWidth||'120px'"
:label-position="formOpts.labelPosition||'right'"
v-bind="$attrs"
v-on="$listeners"
>
<template v-for="(item, index) in fieldList">
<el-form-item
v-if="typeof item.isHideItem == 'function' ? item.isHideItem(formOpts.formData) : !item.isHideItem"
:key="index"
:prop="item.value"
:label="item.label"
:class="[item.className,{'render_label':item.labelRender},{'slot_label':item.slotName},{'render_laber_position':formOpts.labelPosition},{'is_dynamic':isDynamic}]"
:rules="item.rules"
:style="getChildWidth(item)"
v-bind="{...item.formItemBind}"
>
<!-- 自定义label -->
<template #label v-if="item.labelRender">
<render-comp :createElementFunc="item.labelRender" />
</template>
<!-- 自定义输入框插槽 -->
<template v-if="item.slotName">
<slot :name="item.slotName"></slot>
</template>
<!-- 文本展示值 -->
<template v-if="item.textShow">
<span>{{item.textValue||formOpts.formData[item.value]}}</span>
</template>
<template v-if="item.isSelfCom">
<component
v-if="item.comp==='t-select-table'"
:is="item.comp"
:placeholder="item.placeholder||getPlaceholder(item)"
v-bind="typeof item.bind == 'function' ? item.bind(formOpts.formData) : {clearable:true,filterable:true,...item.bind}"
:style="{width: item.width||'100%'}"
v-on="cEvent(item,'t-select-table')"
/>
<component
v-else
:is="item.comp"
v-model="formOpts.formData[item.value]"
:placeholder="item.placeholder||getPlaceholder(item)"
v-bind="typeof item.bind == 'function' ? item.bind(formOpts.formData) : {clearable:true,filterable:true,...item.bind}"
:style="{width: item.width||'100%'}"
v-on="cEvent(item)"
/>
</template>
<!-- <child-component
v-if="!item.slotName&&!item.textShow&&!item.isSelfCom"
v-bind="item"
:item="item"
:form="formOpts"
:value="formOpts.formData[item.value]"
@handleEvent="handleEvent"
>
<template v-for="(index, name) in $slots" v-slot:[name]>
<slot :name="name" />
</template>
<template v-for="(index, name) in $scopedSlots" v-slot:[name]="data">
<slot :name="name" v-bind="data"></slot>
</template>
</child-component>-->
<component
v-if="!item.slotName&&!item.textShow&&!item.isSelfCom"
:is="item.comp"
v-model="formOpts.formData[item.value]"
:type="item.comp==='el-input'?item.type||'input':item.type||item.bind.type"
:placeholder="item.placeholder||getPlaceholder(item)"
@change="handleEvent(item.event, formOpts.formData[item.value],item)"
v-bind="typeof item.bind == 'function' ? item.bind(formOpts.formData) : {clearable:true,filterable:true,...item.bind}"
:style="{width: item.width||'100%'}"
v-on="cEvent(item)"
>
<template #prepend v-if="item.prepend">{{ item.prepend }}</template>
<template #append v-if="item.append">{{ item.append }}</template>
<template v-if="item.childSlotName">
<slot :name="item.childSlotName"></slot>
</template>
<component
v-else
:is="compChildName(item)"
v-for="(value, key, index) in selectListType(item)"
:key="index"
:disabled="value.disabled"
:label="compChildLabel(item,value)"
:value="compChildValue(item,value,key)"
>{{compChildShowLabel(item,value)}}</component>
</component>
<i :key="index+'icon'" v-if="isDynamic" class="el-icon-delete" @click="dynamicDel(index)"></i>
</el-form-item>
</template>
<!-- 按钮 -->
<div class="footer_btn flex-box flex-ver t-margin-top-5">
<template v-if="formOpts.btnSlotName">
<slot :name="formOpts.btnSlotName"></slot>
</template>
<template v-if="!formOpts.btnSlotName&&formOpts.operatorList&&formOpts.operatorList.length>0">
<el-button
v-for="(val,index) in formOpts.operatorList"
:key="index"
@click="val.fun(val)"
v-bind="{
type:'primary',
size:'small',
...val.bind
}"
>{{ val.label }}</el-button>
</template>
</div>
</el-form>
</template>
<script>
import RenderComp from './renderComp'
// import ChildComponent from './ChildComponent'
export default {
name: 'TForm',
components: {
RenderComp
// ChildComponent
},
props: {
// 自定义类名
className: {
type: String
},
/** 表单配置项说明
* formData object 表单提交数据
* rules object 验证规则
* fieldList Array 表单渲染数据
* operatorList Array 操作按钮list
* listTypeInfo object 下拉选项数据
* labelWidth String label宽度
*/
formOpts: {
type: Object,
default: () => ({})
},
// 一行显示几个输入项;最大值4
widthSize: {
type: Number,
default: 2,
validator: (value) => {
return value <= 4
}
},
// 是否开启动态新增表单项
isDynamic: {
type: Boolean,
default: false
},
// 全局是否开启清除前后空格
isTrim: {
type: Boolean,
default: true
},
// ref
refObj: {
type: Object
}
},
computed: {
cEvent() {
return ({ eventHandle }, type) => {
let event = { ...eventHandle }
let changeEvent = {}
Object.keys(event).forEach(v => {
changeEvent[v] = (e, ids) => {
if (type === 't-select-table') {
event[v] && event[v](e, ids, arguments)
} else {
if ((typeof e === 'number' && e === 0) || e) {
event[v] && event[v](e, this.formOpts, arguments)
} else {
event[v] && event[v](this.formOpts, arguments)
}
}
}
})
return { ...changeEvent }
}
},
selectListType() {
return ({ list }) => {
if (this.formOpts.listTypeInfo) {
return this.formOpts.listTypeInfo[list]
} else {
return []
}
}
},
// 子组件名称
compChildName() {
return ({ type }) => {
switch (type) {
case 'checkbox':
return 'el-checkbox'
case 'radio':
return 'el-radio'
case 'select-arr':
case 'select-obj':
return 'el-option'
}
}
},
// 子子组件label
compChildLabel() {
return ({ type, arrLabel }, value) => {
switch (type) {
case 'radio':
case 'checkbox':
return value.value
case 'el-select-multiple':
case 'select-arr':
return value[arrLabel || 'dictLabel']
case 'select-obj':
return value
}
}
},
// 子子组件value
compChildValue() {
return ({ type, arrKey }, value, key) => {
switch (type) {
case 'radio':
case 'checkbox':
return value.value
case 'el-select-multiple':
case 'select-arr':
return value[arrKey || 'dictValue']
case 'select-obj':
return key
}
}
},
// 子子组件文字展示
compChildShowLabel() {
return ({ type, arrLabel }, value) => {
switch (type) {
case 'radio':
case 'checkbox':
return value.label
case 'el-select-multiple':
case 'select-arr':
return value[arrLabel || 'dictLabel']
case 'select-obj':
return value
}
}
}
},
data() {
return {
colSize: this.widthSize,
fieldList: this.formOpts.fieldList
}
},
watch: {
'formOpts.formData': {
handler(val) {
// 将form实例返回到父级
this.$emit('update:refObj', this.$refs.form)
},
deep: true // 深度监听
},
widthSize(val) {
if (val > 4) {
this.$message.warning('widthSize值不能大于4!')
this.colSize = 4
} else {
this.colSize = val
}
}
},
mounted() {
// 将form实例返回到父级
this.$emit('update:refObj', this.$refs.form)
},
methods: {
// label与输入框的布局方式
getChildWidth(item) {
if (this.formOpts.labelPosition === 'top') {
return `flex: 0 1 calc((${100 / (item.widthSize || this.colSize)}% - 10px));margin-right:10px;`
} else {
return `flex: 0 1 ${100 / (item.widthSize || this.colSize)}%;`
}
},
// 得到placeholder的显示
getPlaceholder(row) {
let placeholder
if (typeof row.comp === 'string' && row.comp) {
if (row.comp.includes('input')) {
placeholder = '请输入' + row.label
} else if (row.comp.includes('select') || row.comp.includes('date') || row.comp.includes('cascader')) {
placeholder = '请选择' + row.label
} else {
placeholder = row.label
}
} else {
placeholder = row.label
}
return placeholder
},
dynamicDel(index) {
this.$emit('del', index)
},
// 绑定的相关事件
handleEvent(type, val, item) {
// console.log('组件', type, val, item)
// 去除前后空格
if (this.isTrim && !item.isTrim && item.comp.includes('el-input') && item.type !== 'password' && item.type !== 'inputNumber') {
this.formOpts.formData[item.value] = this.formOpts.formData[item.value].trim()
}
this.$emit('handleEvent', type, val)
},
// 校验
validate() {
return new Promise((resolve, reject) => {
this.$refs.form.validate(valid => {
if (valid) {
resolve({
valid,
formData: this.formOpts.formData
})
} else {
// eslint-disable-next-line prefer-promise-reject-errors
reject({
valid,
formData: null
})
}
})
})
},
// 重置表单
resetFields() {
return this.$refs.form.resetFields()
},
// 清空校验
clearValidate() {
return this.$refs.form.clearValidate()
}
}
}
</script>
<style lang="scss">
.t-form {
display: flex;
flex-wrap: wrap;
.el-form-item {
display: inline-block;
width: 50%;
.el-form-item__content {
.el-input,
.el-select,
.el-date-editor,
.el-textarea {
width: 100%;
}
.el-input-number {
.el-input {
width: inherit;
}
}
}
}
.is_dynamic {
.el-form-item__content {
display: flex;
align-items: center;
.el-icon-delete {
margin-left: 5px;
cursor: pointer;
}
}
}
.t-margin-top-5 {
margin-top: calc(5px);
}
.el-input-number {
.el-input {
.el-input__inner {
text-align: left;
}
}
}
.render_label {
.el-form-item__label {
display: flex;
align-items: center;
justify-content: flex-end;
&::before {
margin-top: 1px;
}
}
}
.render_laber_position {
.el-form-item__label {
justify-content: flex-start;
}
}
.label_render {
display: flex;
align-items: center;
justify-content: flex-end;
}
.slot_label {
// margin-bottom: 0 !important;
.el-form-item__content {
// margin-left: 0 !important;
label {
min-width: 108px;
color: #606266;
text-align: right;
margin-right: 12px;
}
}
}
.footer_btn {
width: 100%;
}
}
</style>
模板部分(<template>
):
- 使用
el-form
组件创建表单,并绑定了表单数据模型(formOpts.formData
)、验证规则(formOpts.rules
)、标签宽度(formOpts.labelWidth
)和标签位置(formOpts.labelPosition
)。 - 通过
v-for
循环渲染fieldList
中的每个表单项,创建el-form-item
组件。 - 对于每个表单项,根据条件渲染不同的模板,例如自定义标签(
#label
)、自定义输入框插槽(slot
)、文本展示值(span
)或自定义组件。 - 支持动态删除表单项,通过
dynamicDel
方法触发。 - 底部操作按钮区域根据配置渲染操作按钮,可以是插槽(
slot
)或静态按钮(el-button
)。
脚本部分(<script>
):
- 定义了组件
TForm
,导入了RenderComp
组件。 - 组件接收多个props,包括自定义类名、表单配置项、动态新增表单项的开关、全局是否开启清除前后空格等。
- 计算属性(
computed
)用于生成事件处理函数、下拉选项数据、子组件名称、子组件的label和value等。 - 数据(
data
)和watch
用于处理表单字段列表和宽度大小。 mounted
钩子用于将表单实例返回给父组件。- 方法(
methods
)包括获取子项宽度、获取placeholder、动态删除表单项、处理事件、表单验证、重置表单和清除校验。
样式部分(<style lang="scss">
):
- 定义了
.t-form
的基本样式,包括flex布局和表单项的宽度。 - 定义了动态表单项的样式,包括删除图标的样式。
- 设置了底部操作按钮区域的样式。
- 自定义了标签和输入框的布局样式。
这个组件的设计非常灵活,可以根据不同的需求配置表单项,支持自定义验证规则和操作按钮,适用于多种表单场景。
<template>
<component
:is="item.comp"
v-bind="typeof item.bind == 'function' ? bind(form.formData) : {clearable:true,filterable:true,...item.bind}"
:placeholder="item.placeholder||getPlaceholder(item)"
:type="item.comp==='el-input'?item.type||'input':item.type||item.bind.type"
@change="handleEvent(item.event, form.formData[item.value],item)"
v-on="cEvent(item)"
v-model="form.formData[item.value]"
>
<!-- 前置文本 -->
<template #prepend v-if="item.prepend">{{ item.prepend }}</template>
<!-- 后置文本 -->
<template #append v-if="item.append">{{ item.append }}</template>
<!-- 子组件自定义插槽 -->
<template v-if="item.childSlotName">
<slot :name="item.childSlotName"></slot>
</template>
<!-- <child-component v-for="(cOpt, i) in child" :key="i" v-bind="cOpt" :value="cOpt.value" /> -->
<child-component
v-else
:is="compChildName(item)"
v-for="(value, key, index) in selectListType(item)"
:key="index"
:disabled="value.disabled"
:label="compChildLabel(item,value)"
:value="compChildValue(item,value,key)"
>{{compChildShowLabel(item,value)}}</child-component>
</component>
</template>
<script>
export default {
name: 'ChildComponent',
props: {
dataIndex: {
type: String,
default: ''
},
form: {
type: Object,
default: () => ({})
},
item: {
type: Object,
default: () => ({})
},
bind: {
type: [Object, Function]
},
comp: {
type: [String, Object],
default: ''
},
childList: {
type: Array,
default: () => ([])
},
placeholder: {
type: String
},
value: {
type: [String, Number, Array, Date, Boolean],
default: ''
},
changeEvent: {
type: String,
default: 'input'
}
},
computed: {
cEvent() {
return ({ eventHandle }) => {
let event = { ...eventHandle }
let changeEvent = {}
Object.keys(event).forEach(v => {
changeEvent[v] = (e) => {
if (e) {
event[v] && event[v](e, this.form, arguments)
} else {
event[v] && event[v](this.form, arguments)
}
}
})
return { ...changeEvent }
}
},
selectListType() {
return ({ list }) => {
if (this.form.listTypeInfo) {
return this.form.listTypeInfo[list]
} else {
return []
}
}
},
// 子组件名称
compChildName() {
return ({ type }) => {
switch (type) {
case 'checkbox':
return 'el-checkbox'
case 'radio':
return 'el-radio'
case 'select-arr':
case 'select-obj':
return 'el-option'
}
}
},
// 子子组件label
compChildLabel() {
return ({ type, arrLabel }, value) => {
switch (type) {
case 'radio':
case 'checkbox':
return value.value
case 'el-select-multiple':
case 'select-arr':
return value[arrLabel || 'dictLabel']
case 'select-obj':
return value
}
}
},
// 子子组件value
compChildValue() {
return ({ type, arrKey }, value, key) => {
switch (type) {
case 'radio':
case 'checkbox':
return value.value
case 'el-select-multiple':
case 'select-arr':
return value[arrKey || 'dictValue']
case 'select-obj':
return key
}
}
},
// 子子组件文字展示
compChildShowLabel() {
return ({ type, arrLabel }, value) => {
switch (type) {
case 'radio':
case 'checkbox':
return value.label
case 'el-select-multiple':
case 'select-arr':
return value[arrLabel || 'dictLabel']
case 'select-obj':
return value
}
}
}
// cEvent() {
// let event = { ...this.event }
// let changeEvent = {}
// if (this.changeEvent) {
// changeEvent[this.changeEvent] = (v) => {
// event[this.changeEvent] && event[this.changeEvent](v, arguments)
// this.$emit('change', v, this.dataIndex, arguments)
// }
// }
// return { ...event, ...changeEvent }
// }
},
methods: {
// 得到placeholder的显示
getPlaceholder(row) {
// console.log(222, row, form)
let placeholder
if (typeof row.comp === 'string' && row.comp) {
if (row.comp.includes('input')) {
placeholder = '请输入' + row.label
} else if (row.comp.includes('select') || row.comp.includes('date') || row.comp.includes('cascader')) {
placeholder = '请选择' + row.label
} else {
placeholder = row.label
}
}
return placeholder
},
// 绑定的相关事件
handleEvent(type, val, item) {
// console.log('组件', type, val, item)
this.$emit('handleEvent', type, val, item)
}
}
}
</script>
这段代码是一个Vue组件,名为ChildComponent
,它是一个用于渲染表单项的通用组件。这个组件的设计允许它根据传入的item
对象动态地渲染不同类型的表单元素,如输入框、选择框、复选框等。下面是对代码的详细解释:
模板部分(<template>
):
- 使用
<component>
标签动态地绑定组件,根据item.comp
的值来决定渲染哪个Element UI组件。 - 绑定了多个属性,包括自定义绑定(
bind
)、占位符(placeholder
)、类型(type
)等。 - 监听了
change
事件,并触发handleEvent
方法,这个方法将事件类型、值和当前的item
对象发送给父组件。 - 使用了插槽(
slot
)来支持前置文本(#prepend
)和后置文本(#append
)。 - 如果
item.childSlotName
存在,则渲染一个具有该名称的插槽,允许父组件在当前组件内部插入自定义内容。 - 通过
v-for
循环渲染子组件,这些子组件是根据item
对象中的selectListType
属性动态生成的。
脚本部分(<script>
):
- 定义了
ChildComponent
组件,并设置了一系列的props
,包括数据索引(dataIndex
)、表单数据(form
)、当前项(item
)、自定义绑定(bind
)、组件名称(comp
)、子列表(childList
)、占位符(placeholder
)、值(value
)和变更事件(changeEvent
)。 - 计算属性(
computed
)用于生成事件处理函数、下拉选项数据、子组件名称、子组件的label和value等。 getPlaceholder
方法用于生成表单元素的占位符文本。handleEvent
方法用于处理组件的事件,并将事件类型、值和当前的item
对象发送给父组件。
样式部分(<style>
):
样式部分在这段代码中没有给出,但通常会包含对组件内部元素的样式定义。
这个组件的设计使得它可以很容易地集成到表单中,并且可以根据不同的配置渲染出不同的表单元素。它的灵活性和可重用性使其成为构建动态表单的有力工具。
<script>
export default {
name: 'RenderComp',
props: {
createElementFunc: Function
},
render (h) {
return this.createElementFunc(h)
}
}
</script>
这段代码定义了一个名为 RenderComp
的Vue组件,它的主要作用是接收一个函数作为属性,并在组件的渲染过程中调用这个函数来创建和返回虚拟DOM节点。
组件特点:
-
单一职责:
RenderComp
组件专注于根据传入的函数来渲染内容,不做其他操作,这符合单一职责原则。 -
高度可定制:由于组件的渲染完全取决于传入的
createElementFunc
函数,这使得它可以非常灵活地用于各种不同的场景。 -
使用虚拟DOM:组件使用了Vue的
render
函数和虚拟DOM节点创建函数h
来构建内容,这是Vue框架的核心特性之一。
组件用法:
在父组件中,你可以这样使用 RenderComp
:
<template>
<render-comp :create-element-func="customRenderFunction"></render-comp>
</template>
<script>
import RenderComp from './RenderComp.vue';
export default {
components: { RenderComp },
methods: {
customRenderFunction(createElement) {
// 使用 createElement 创建虚拟节点
return createElement('div', 'Hello, World!');
}
}
}
</script>
在这个例子中,customRenderFunction
是传递给 RenderComp
的函数,它使用 createElement
函数来创建一个包含文本 “Hello, World!” 的 div
节点。
组件的 props
:
createElementFunc
:这是一个函数类型的属性,它期望接收一个函数,这个函数将接收Vue的虚拟DOM创建函数h
作为参数,并返回一个虚拟DOM节点。
组件的 render
函数:
render
函数是Vue组件生命周期的一部分,它定义了组件如何被渲染到页面上。在这个例子中,render
函数直接调用传入的createElementFunc
函数,并将其返回值作为最终的虚拟DOM节点。
这种组件设计模式在Vue中被称为“函数式组件”,它通常用于创建高度可复用和可定制的UI组件。