2023-03-06 TForm组件新增继承 Element-plus 组件的事件使用及el-input 去除前后空格(type=password 除外)
一、简介
HTML 一行代码,可实现表单输入框/日期选择/下拉选择/复选框选中等及规则校验功能
<t-form
v-model="formOpts.ref"
:formOpts="formOpts"
:widthSize="2"
@handleEvent="handleEvent"
/>
//注意formOpts.ref(t-form组件实例相当于vue2 ref)需要要v-model接收
二、最终效果
三、参数配置
1、Attributes
参数 | 说明 | 类型 | 是否必须 |
---|
className | 自定义类名 | String | 否 |
labelPosition | 改变表单项 label 与输入框的布局方式(默认:right) /top | String | 否 |
widthSize | 每行显示几个输入项(默认两项) 最大值 4 | Number | 否 |
isTrim | 全局是否开启清除前后空格(comp 为 el-input 且 type 不等于’password’) | Boolean | true |
formOpts | 表单配置项 | Object | 否 |
—listTypeInfo | 下拉选择数据源(type:'select’有效) | Object | 否 |
—fieldList | form 表单每项 list | Array | 是 |
------slotName | 自定义表单某一项输入框 | slot | 否 |
------childSlotName | 自定义表单某一下拉选择项子组件插槽(el-option) | slot | 否 |
------comp | form 表单每一项组件是输入框还是下拉选择等(可使用第三方 UI 如 el-select/el-input 也可以使用自定义组件) | String | 是 |
------bind | 表单每一项属性(继承第三方 UI 的 Attributes,如 el-input 中的 clearable 清空功能)默认清空及下拉过滤 | Object/function | 否 |
------type | form 表单每一项类型 | String | 是 |
------isTrim | 是否不清除前后空格(comp 为 el-input 且 type 不等于’password’) | Boolean | false |
------eventHandle | 继承 comp 组件的事件 | Object | - |
------widthSize | form 表单某一项所占比例(如果占一整行则设置 1) | Number | 否 |
------width | form 表单某一项所占实际宽度 | String | 否 |
------arrLabel | type=select-arr 时,每个下拉显示的中文 | String | 否 |
------arrKey | type=select-arr 时,每个下拉显示的中文传后台的数字 | String | 否 |
------label | form 表单每一项 title | String | 是 |
------labelRender | 自定义某一项 title | function | 是 |
------value | form 表单每一项传给后台的参数 | String | 是 |
------rules | 每一项输入框的表单校验规则 | Object/Array | 否 |
------list | 下拉选择数据源(仅仅对 type:'select’有效) | String | 否 |
------event | 表单每一项事件标志(handleEvent 事件) | String | 否 |
—formData | 表单提交数据(对应 fieldList 每一项的 value 值) | Object | 是 |
—labelWidth | label 宽度 | String | 否 |
—rules | 规则(可依据 elementUI el-form 配置————对应 formData 的值) | Object/Array | 否 |
—operatorList | 操作按钮 list | Array | 否 |
2、Events
事件名 | 说明 | 返回值 |
---|
handleEvent | 单个查询条件触发事件 | fieldList 中 type/查询条件输入的值/fieldList 中 event 值 |
3、Methods
事件名 | 说明 | 返回值 |
---|
validate | 校验表单数据 | valid:boolean; formData:最终表单数据 |
resetFields | 重置表单 | - |
clearValidate | 清空校验 | - |
四、源码
<template>
<el-form class="t-form" ref="tform" :class="className" :model="formOpts.formData" :rules="formOpts.rules"
:label-width="formOpts.labelWidth || '100px'" :label-position="formOpts.labelPosition || 'right'" v-bind="$attrs">
<template v-for="(item, index) in formOpts.fieldList">
<el-form-item v-if="!item.isHideItem" :key="index" :prop="item.value" :label="item.label" :class="[
item.className,
{ render_label: item.labelRender },
{ slot_label: item.slotName },
{ render_laber_position_left: formOpts.labelPosition === 'left' },
]" :rules="item.rules" :style="getChildWidth(item)" v-bind="$attrs">
<template #label v-if="item.labelRender">
<render-comp :render="item.labelRender" :item="item" />
</template>
<template v-if="item.slotName">
<slot :name="item.slotName"></slot>
</template>
<template v-if="item.textShow">
<span class="text_show">{{
item.textValue || formOpts.formData[item.value]
}}</span>
</template>
<component v-if="!item.slotName && !item.textShow && item.comp.includes('date')" :is="item.comp"
v-model="formOpts.formData[item.value]" :type="item.type"
:placeholder="item.placeholder || getPlaceholder(item)"
@change="handleEvent(item.event, formOpts.formData[item.value], item)" v-bind="typeof item.bind == 'function'
? item.bind(item)
: { clearable: true, filterable: true, ...item.bind }
" :style="{ width: item.width || '100%' }" v-on="cEvent(item)" />
<component v-if="!item.slotName &&
!item.textShow &&
item.comp.includes('tree-select')
" :is="item.comp" v-model="formOpts.formData[item.value]" :type="item.type"
:placeholder="item.placeholder || getPlaceholder(item)"
@change="handleEvent(item.event, formOpts.formData[item.value], item)" v-bind="typeof item.bind == 'function'
? item.bind(item)
: { clearable: true, filterable: true, ...item.bind }
" :style="{ width: item.width || '100%' }" v-on="cEvent(item)" />
<component v-if="!item.slotName &&
!item.textShow &&
!item.comp.includes('date') &&
!item.comp.includes('tree-select')
" :is="item.comp" v-model="formOpts.formData[item.value]" :type="item.type"
:placeholder="item.placeholder || getPlaceholder(item)"
@change="handleEvent(item.event, formOpts.formData[item.value], item)" v-bind="typeof item.bind == 'function'
? item.bind(item)
: { 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 :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>
</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)"
:type="val.type || 'primary'" :icon="val.icon" :size="val.size || 'small'" :disabled="val.disabled">
{{ val.label }}
</el-button>
</template>
</div>
</el-form>
</template>
<script lang="ts">
export default {
name: 'TForm',
}
</script>
<script setup lang="ts">
import RenderComp from './renderComp.vue'
import { ElMessage } from 'element-plus'
import { computed, ref, watch, onMounted, getCurrentInstance } from 'vue'
const props = defineProps({
className: {
type: String,
},
formOpts: {
type: Object,
default: () => ({}),
},
widthSize: {
type: Number,
default: 2,
validator: (value: any) => {
return value <= 4
},
},
isTrim: {
type: Boolean,
default: true,
},
})
const cEvent = computed(() => {
return ({ eventHandle }) => {
return { ...eventHandle }
}
})
const selectListType = computed(() => {
return ({ list }) => {
if (props.formOpts.listTypeInfo) {
return props.formOpts.listTypeInfo[list]
} else {
return []
}
}
})
const compChildName = computed(() => {
return (opt: any) => {
switch (opt.type) {
case 'checkbox':
return 'el-checkbox'
case 'radio':
return 'el-radio'
case 'select-arr':
case 'select-obj':
return 'el-option'
}
}
})
const compChildLabel = computed(() => {
return (opt: any, value) => {
switch (opt.type) {
case 'radio':
case 'checkbox':
return value.value
case 'el-select-multiple':
case 'select-arr':
return value[opt.arrLabel || 'label']
case 'select-obj':
return value
}
}
})
const compChildValue = computed(() => {
return (opt: any, value, key) => {
switch (opt.type) {
case 'radio':
case 'checkbox':
return value.value
case 'el-select-multiple':
case 'select-arr':
return value[opt.arrKey || 'key']
case 'select-obj':
return key
}
}
})
const compChildShowLabel = computed(() => {
return (opt: any, value) => {
switch (opt.type) {
case 'radio':
case 'checkbox':
return value.label
case 'el-select-multiple':
case 'select-arr':
return value[opt.arrLabel || 'label']
case 'select-obj':
return value
}
}
})
const colSize = ref(props.widthSize)
const tform: any = ref<HTMLElement | null>(null)
const instance: any = getCurrentInstance()
const emits = defineEmits(['update:modelValue', 'handleEvent'])
watch(
() => props.formOpts.formData,
(val) => {
emits('update:modelValue', tform.value)
},
{ deep: true }
)
watch(
() => props.widthSize,
(val) => {
if (val > 4) {
ElMessage.warning('widthSize值不能大于4!')
colSize.value = 4
} else {
colSize.value = val
}
},
{ deep: true }
)
onMounted(() => {
const entries = Object.entries(tform.value.$.exposed)
for (const [key, value] of entries) {
instance.exposed[key] = value
}
emits('update:modelValue', tform.value)
})
const getChildWidth = (item) => {
if (props.formOpts.labelPosition === 'top') {
return `flex: 0 1 calc((${100 / (item.widthSize || colSize.value)
}% - 10px));margin-right:10px;`
} else {
return `flex: 0 1 ${100 / (item.widthSize || colSize.value)}%;`
}
}
const getPlaceholder = (row: any) => {
let placeholder
if (row.comp && typeof row.comp == 'string') {
if (row.comp.includes('input')) {
placeholder = '请输入' + row.label
} else if (row.comp.includes('select') || row.comp.includes('date')) {
placeholder = '请选择' + row.label
} else {
placeholder = row.label
}
}
return placeholder
}
const handleEvent = (type, val, item) => {
if (
props.isTrim &&
!item.isTrim &&
item.comp.includes('el-input') &&
item.type !== 'password' &&
item.type !== 'inputNumber'
) {
props.formOpts.formData[item.value] =
props.formOpts.formData[item.value].trim()
}
emits('handleEvent', type, val)
}
const selfValidate = () => {
return new Promise((resolve: any, reject: any) => {
tform.value.validate((valid: boolean) => {
if (valid) {
resolve({
valid,
formData: props.formOpts.formData,
})
} else {
reject({
valid,
formData: null,
})
}
})
})
}
defineExpose({ ...instance.exposed, selfValidate })
</script>
<style lang="scss">
.t-form {
display: flex;
flex-wrap: wrap;
.el-form-item {
align-items: center;
.el-form-item__content {
.el-input,
.el-select,
.el-date-editor,
.el-textarea {
width: 100%;
}
.el-input-number {
.el-input {
width: inherit;
}
}
}
}
// 左对齐
.asterisk-left {
.el-form-item__label {
margin-left: 5px;
}
}
.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_left {
.el-form-item__label {
justify-content: flex-start;
}
}
// 顶部对齐
&.el-form--label-top {
.render_label {
.el-form-item__label {
justify-content: flex-start;
}
}
}
.label_render {
display: flex;
align-items: center;
justify-content: flex-end;
}
.text_show {
color: var(--el-text-color-primary);
}
.slot_label {
// margin-bottom: 0 !important;
.el-form-item__content {
// margin-left: 0 !important;
label {
min-width: 108px;
color: var(--el-text-color-primary);
text-align: right;
margin-right: 12px;
}
}
}
.flex-box {
display: -webkit-box;
display: -webkit-flex;
display: flex;
}
.flex-ver {
align-items: center;
justify-content: center;
}
.footer_btn {
width: 100%;
}
}
</style>
五、组件地址
gitHub组件地址
gitee码云组件地址
vue3+ts基于Element-plus再次封装基础组件文档
六、相关文章
基于ElementUi再次封装基础组件文档
vue+element-ui的table组件二次封装