演示环境
Vue:3.3.4
TypeScript:5.0.2
Sass:1.79.4
ElementPlus:2.7.8
开始
1、新建自定义表单组件 -MyForm.vue
html
<template>表单组件</template>
<script setup lang="ts"></script>
<style scoped lang="scss"></style>
2、新建一个测试vue文件 -Test.vue
html
<template>
<div class="myBody">
<el-card header="自定义表单">
<my-form></my-form>
</el-card>
</div>
</template>
<script lang="ts" setup>
import MyForm from '@/components/utils/MyForm.vue';
</script>
<style lang="scss" scoped>
.myBody {
margin: 1rem;
}
</style>
3、页面效果

创建初始化数据结构
1、新建表单数据和表单项 -Test.vue
html
<template>
<div class="myBody">
<el-card header="自定义表单">
<my-form v-model="formData" :items="items"></my-form>
<el-divider />
<div>
<div>表单数据</div>
<pre>{{ formData }}</pre>
</div>
</el-card>
</div>
</template>
<script lang="ts" setup>
import MyForm from '@/components/utils/MyForm.vue';
const formData = ref({
name: null,
age: null,
sex: null,
hobbies: [],
dateOfBirth: ''
});
const items = [
{
label: '姓名',
key: 'name',
type: 'input',
placeholder: '请输入姓名'
},
{
label: '年龄',
key: 'age',
type: 'number',
placeholder: '请输入年龄'
},
{
label: '性别',
key: 'sex',
type: 'radio',
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
{ label: '保密', value: 'secrecy' }
]
},
{
label: '爱好',
key: 'hobbies',
type: 'checkbox',
options: [
{ label: '阅读', value: 'reading' },
{ label: '旅行', value: 'traveling' },
{ label: '运动', value: 'sports' }
]
},
{
label: '出生日期',
key: 'dateOfBirth',
type: 'datePicker',
placeholder: '请选择出生日期'
}
];
</script>
<style lang="scss" scoped>
.myBody {
margin: 1rem;
}
</style>
2、打印接收的数据 -MyForm.vue
html
<template>
表单组件接收的表单数据:
<pre> {{ formData }}</pre>
<el-divider />
表单组件接收的表单项:
{{ props.items }}
</template>
<script setup lang="ts">
const formData = defineModel();
const props = defineProps(['items']);
</script>
<style scoped lang="scss"></style>
3、页面效果

渲染表单项和绑定表单数据
1、让我们编写表单组件的代码,将基础的表单项和表单数据绑定完成
html
<template>
<el-form :model="formData">
<el-form-item v-for="item in props.items" :key="item.key" :prop="item.key" :label="item.label">
<component :is="getComponent(item)" v-bind="getProps(item)" v-model="formData[item.key]"></component>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ElCheckboxGroup, ElDatePicker, ElInput, ElInputNumber, ElRadioGroup } from 'element-plus';
import { omit } from 'lodash';
const formData = defineModel<Record<string, any>>({ default: {} });
const props = defineProps(['items']);
const componentMap: Record<string, Component> = {
input: ElInput,
number: ElInputNumber,
radio: ElRadioGroup,
checkbox: ElCheckboxGroup,
datePicker: ElDatePicker
};
const getComponent = (item: any) => {
return componentMap[item.type];
};
const rootProps = ['label', 'key', 'type'];
const getProps = (item: any) => {
return omit(item, rootProps);
};
</script>
<style scoped lang="scss"></style>
2、页面效果

3、测试一下表单功能,都没问题,但是包含子组件的组件我们还没有处理

处理包含子组件的组件渲染
1、写一个函数生成需要渲染的子组件,然后在页面渲染即可
html
<template>
<el-form :model="formData">
<el-form-item v-for="item in props.items" :key="item.key" :prop="item.key" :label="item.label">
<component :is="getComponent(item)" v-bind="getProps(item)" v-model="formData[item.key]">
<component
v-for="(option, index) in getOptions(item)"
:key="index"
:is="option.component"
v-bind="option.props"
></component>
</component>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ElCheckbox, ElCheckboxGroup, ElDatePicker, ElInput, ElInputNumber, ElRadio, ElRadioGroup } from 'element-plus';
import { omit } from 'lodash';
const formData = defineModel<Record<string, any>>({ default: {} });
const props = defineProps(['items']);
const componentMap: Record<string, Component> = {
input: ElInput,
number: ElInputNumber,
radio: ElRadioGroup,
checkbox: ElCheckboxGroup,
datePicker: ElDatePicker
};
const getComponent = (item: any) => {
return componentMap[item.type];
};
const rootProps = ['label', 'key', 'type'];
const getProps = (item: any) => {
return omit(item, rootProps);
};
const getOptions = (item: any) => {
if (!item.options?.length) return [];
const componentMap: Record<string, Component> = {
radio: ElRadio,
checkbox: ElCheckbox
};
const component = componentMap[item.type];
if (!component) return [];
return item.options.map((option: any) => ({
component,
props: option
}));
};
</script>
<style scoped lang="scss"></style>
2、页面效果,功能测试都没问题

3、至此组件的渲染和基础功能都完成了,下面我们只需要进行一些细节功能添加
表单函数抛出
1、这里把常用的两个函数(表单验证和重置)抛出
html
<template>
<el-form ref="formRef" :model="formData">
<el-form-item v-for="item in props.items" :key="item.key" :prop="item.key" :label="item.label">
<component :is="getComponent(item)" v-bind="getProps(item)" v-model="formData[item.key]">
<component
v-for="(option, index) in getOptions(item)"
:key="index"
:is="option.component"
v-bind="option.props"
></component>
</component>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ElCheckbox, ElCheckboxGroup, ElDatePicker, ElInput, ElInputNumber, ElRadio, ElRadioGroup } from 'element-plus';
import { omit } from 'lodash';
const formRef = ref();
const formData = defineModel<Record<string, any>>({ default: {} });
const props = defineProps(['items']);
const componentMap: Record<string, Component> = {
input: ElInput,
number: ElInputNumber,
radio: ElRadioGroup,
checkbox: ElCheckboxGroup,
datePicker: ElDatePicker
};
const getComponent = (item: any) => {
return componentMap[item.type];
};
const rootProps = ['label', 'key', 'type'];
const getProps = (item: any) => {
return omit(item, rootProps);
};
const getOptions = (item: any) => {
if (!item.options?.length) return [];
const componentMap: Record<string, Component> = {
radio: ElRadio,
checkbox: ElCheckbox
};
const component = componentMap[item.type];
if (!component) return [];
return item.options.map((option: any) => ({
component,
props: option
}));
};
defineExpose({
validate: (...args: any[]) => {
return formRef.value.validate(...args);
},
resetFields: (...args: any[]) => {
return formRef.value.resetFields(...args);
}
});
</script>
<style scoped lang="scss"></style>
2、在页面上使用,添加提交和重置两个按钮
html
<template>
<div class="myBody">
<el-card header="自定义表单">
<my-form ref="formRef" v-model="formData" :items="items"></my-form>
<el-divider />
<div>
<div>表单数据</div>
<pre>{{ formData }}</pre>
</div>
<el-divider />
<el-space>
<el-button type="primary" @click="submit">提交</el-button>
<el-button @click="reset">重置</el-button>
</el-space>
</el-card>
</div>
</template>
<script lang="ts" setup>
import MyForm from '@/components/utils/MyForm.vue';
const formRef = ref();
const formData = ref({});
const items = [
{
label: '姓名',
key: 'name',
type: 'input',
placeholder: '请输入姓名'
},
{
label: '年龄',
key: 'age',
type: 'number',
placeholder: '请输入年龄'
},
{
label: '性别',
key: 'sex',
type: 'radio',
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
{ label: '保密', value: 'secrecy' }
]
},
{
label: '爱好',
key: 'hobbies',
type: 'checkbox',
options: [
{ label: '阅读', value: 'reading' },
{ label: '旅行', value: 'traveling' },
{ label: '运动', value: 'sports' }
]
},
{
label: '出生日期',
key: 'dateOfBirth',
type: 'datePicker',
placeholder: '请选择出生日期'
}
];
const submit = () => {
formRef.value
.validate()
.then(() => {
console.log('表单验证通过,提交的数据:{}', formData.value);
})
.catch((error: Error) => {
console.error('表单验证失败:{}', error.message);
});
};
const reset = () => {
formRef.value.resetFields();
console.log('表单已重置');
};
</script>
<style lang="scss" scoped>
.myBody {
margin: 1rem;
}
</style>
3、我们来测试一下,功能都没有问题,但是我们还没有添加表单校验规则


4、添加表单校验规则
html
<template>
<div class="myBody">
<el-card header="自定义表单">
<my-form ref="formRef" v-model="formData" :items="items" :rules="rules"></my-form>
<el-divider />
<div>
<div>表单数据</div>
<pre>{{ formData }}</pre>
</div>
<el-divider />
<el-space>
<el-button type="primary" @click="submit">提交</el-button>
<el-button @click="reset">重置</el-button>
</el-space>
</el-card>
</div>
</template>
<script lang="ts" setup>
import MyForm from '@/components/utils/MyForm.vue';
const formRef = ref();
const formData = ref({});
const items = [
{
label: '姓名',
key: 'name',
type: 'input',
placeholder: '请输入姓名'
},
{
label: '年龄',
key: 'age',
type: 'number',
placeholder: '请输入年龄'
},
{
label: '性别',
key: 'sex',
type: 'radio',
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
{ label: '保密', value: 'secrecy' }
]
},
{
label: '爱好',
key: 'hobbies',
type: 'checkbox',
options: [
{ label: '阅读', value: 'reading' },
{ label: '旅行', value: 'traveling' },
{ label: '运动', value: 'sports' }
]
},
{
label: '出生日期',
key: 'dateOfBirth',
type: 'datePicker',
placeholder: '请选择出生日期'
}
];
const rules = {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 2, max: 10, message: '姓名长度在2-10个字符之间', trigger: 'blur' }
],
age: [{ required: true, message: '请输入年龄', trigger: 'blur' }],
sex: [{ required: true, message: '请选择性别', trigger: 'change' }],
hobbies: [{ required: true, message: '请选择至少一个爱好', trigger: 'change' }],
dateOfBirth: [{ required: true, message: '请选择出生日期', trigger: 'change' }]
};
const submit = () => {
formRef.value
.validate()
.then(() => {
console.log('表单验证通过,提交的数据:{}', formData.value);
})
.catch(() => {
console.error('表单验证失败');
});
};
const reset = () => {
formRef.value.resetFields();
console.log('表单已重置');
};
</script>
<style lang="scss" scoped>
.myBody {
margin: 1rem;
}
</style>
5、在组件中绑定校验规则的值
html
<template>
<el-form ref="formRef" :model="formData" :rules="rules">
<el-form-item v-for="item in props.items" :key="item.key" :prop="item.key" :label="item.label">
<component :is="getComponent(item)" v-bind="getProps(item)" v-model="formData[item.key]">
<component
v-for="(option, index) in getOptions(item)"
:key="index"
:is="option.component"
v-bind="option.props"
></component>
</component>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ElCheckbox, ElCheckboxGroup, ElDatePicker, ElInput, ElInputNumber, ElRadio, ElRadioGroup } from 'element-plus';
import { omit } from 'lodash';
const formRef = ref();
const formData = defineModel<Record<string, any>>({ default: {} });
const props = defineProps(['items', 'rules']);
const componentMap: Record<string, Component> = {
input: ElInput,
number: ElInputNumber,
radio: ElRadioGroup,
checkbox: ElCheckboxGroup,
datePicker: ElDatePicker
};
const getComponent = (item: any) => {
return componentMap[item.type];
};
const rootProps = ['label', 'key', 'type'];
const getProps = (item: any) => {
return omit(item, rootProps);
};
const getOptions = (item: any) => {
if (!item.options?.length) return [];
const componentMap: Record<string, Component> = {
radio: ElRadio,
checkbox: ElCheckbox
};
const component = componentMap[item.type];
if (!component) return [];
return item.options.map((option: any) => ({
component,
props: option
}));
};
defineExpose({
validate: (...args: any[]) => {
return formRef.value.validate(...args);
},
resetFields: (...args: any[]) => {
return formRef.value.resetFields(...args);
}
});
</script>
<style scoped lang="scss"></style>
6、测试表单校验,功能都没问题



7、现在基本功能都没问题了,后面我们再继续添加一些细节功能
表单项自定义组件、自定义插槽
1、如果我们想将表单项设置为自定义的组件,可以这样,先创建一个 HelloWorld.vue
组件
html
<template>hello world</template>
<script setup lang="ts"></script>
<style scoped lang="scss"></style>
2、页面上使用,顺便设置一个默认组件,例如我们项目中输入框用的比较多,我想在不传type时让它默认为输入框
html
<template>
<div class="myBody">
<el-card header="自定义表单">
<my-form ref="formRef" v-model="formData" :items="items" :rules="rules"></my-form>
<el-divider />
<div>
<div>表单数据</div>
<pre>{{ formData }}</pre>
</div>
<el-divider />
<el-space>
<el-button type="primary" @click="submit">提交</el-button>
<el-button @click="reset">重置</el-button>
</el-space>
</el-card>
</div>
</template>
<script lang="ts" setup>
import MyForm from '@/components/utils/MyForm.vue';
import HelloWorld from '@/pages/HelloWorld.vue';
const formRef = ref();
const formData = ref({});
const items = [
{
label: '姓名',
key: 'name',
type: 'input',
placeholder: '请输入姓名'
},
{
label: '年龄',
key: 'age',
type: 'number',
placeholder: '请输入年龄'
},
{
label: '性别',
key: 'sex',
type: 'radio',
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
{ label: '保密', value: 'secrecy' }
]
},
{
label: '爱好',
key: 'hobbies',
type: 'checkbox',
options: [
{ label: '阅读', value: 'reading' },
{ label: '旅行', value: 'traveling' },
{ label: '运动', value: 'sports' }
]
},
{
label: '出生日期',
key: 'dateOfBirth',
type: 'datePicker',
placeholder: '请选择出生日期'
},
{
label: '自定义组件',
key: 'customComponent',
type: HelloWorld
},
{
label: '默认组件',
key: 'defaultComponent',
placeholder: '默认组件...'
}
];
const rules = {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 2, max: 10, message: '姓名长度在2-10个字符之间', trigger: 'blur' }
],
age: [{ required: true, message: '请输入年龄', trigger: 'blur' }],
sex: [{ required: true, message: '请选择性别', trigger: 'change' }],
hobbies: [{ required: true, message: '请选择至少一个爱好', trigger: 'change' }],
dateOfBirth: [{ required: true, message: '请选择出生日期', trigger: 'change' }]
};
const submit = () => {
formRef.value
.validate()
.then(() => {
console.log('表单验证通过,提交的数据:{}', formData.value);
})
.catch(() => {
console.error('表单验证失败');
});
};
const reset = () => {
formRef.value.resetFields();
console.log('表单已重置');
};
</script>
<style lang="scss" scoped>
.myBody {
margin: 1rem;
}
</style>
3、我们只需要改一下获取表单项组件的函数内容即可
html
<template>
<el-form ref="formRef" :model="formData" :rules="rules">
<el-form-item v-for="item in props.items" :key="item.key" :prop="item.key" :label="item.label">
<component :is="getComponent(item)" v-bind="getProps(item)" v-model="formData[item.key]">
<component
v-for="(option, index) in getOptions(item)"
:key="index"
:is="option.component"
v-bind="option.props"
></component>
</component>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ElCheckbox, ElCheckboxGroup, ElDatePicker, ElInput, ElInputNumber, ElRadio, ElRadioGroup } from 'element-plus';
import { omit } from 'lodash';
const formRef = ref();
const formData = defineModel<Record<string, any>>({ default: {} });
const props = defineProps(['items', 'rules']);
const componentMap: Record<string, Component> = {
input: ElInput,
number: ElInputNumber,
radio: ElRadioGroup,
checkbox: ElCheckboxGroup,
datePicker: ElDatePicker
};
const getComponent = (item: any) => {
if (item.type && typeof item.type !== 'string') {
return item.type;
}
return componentMap[item.type || 'input'];
};
const rootProps = ['label', 'key', 'type'];
const getProps = (item: any) => {
return omit(item, rootProps);
};
const getOptions = (item: any) => {
if (!item.options?.length) return [];
const componentMap: Record<string, Component> = {
radio: ElRadio,
checkbox: ElCheckbox
};
const component = componentMap[item.type];
if (!component) return [];
return item.options.map((option: any) => ({
component,
props: option
}));
};
defineExpose({
validate: (...args: any[]) => {
return formRef.value.validate(...args);
},
resetFields: (...args: any[]) => {
return formRef.value.resetFields(...args);
}
});
</script>
<style scoped lang="scss"></style>
4、页面效果,功能都没问题

5、我们在页面上添加一个自定义插槽
html
<template>
<div class="myBody">
<el-card header="自定义表单">
<my-form ref="formRef" v-model="formData" :items="items" :rules="rules">
<template #customSlot>
<div>这是自定义的插槽</div>
</template>
</my-form>
<el-divider />
<div>
<div>表单数据</div>
<pre>{{ formData }}</pre>
</div>
<el-divider />
<el-space>
<el-button type="primary" @click="submit">提交</el-button>
<el-button @click="reset">重置</el-button>
</el-space>
</el-card>
</div>
</template>
<script lang="ts" setup>
import MyForm from '@/components/utils/MyForm.vue';
import HelloWorld from '@/pages/HelloWorld.vue';
const formRef = ref();
const formData = ref({});
const items = [
{
label: '姓名',
key: 'name',
type: 'input',
placeholder: '请输入姓名'
},
{
label: '年龄',
key: 'age',
type: 'number',
placeholder: '请输入年龄'
},
{
label: '性别',
key: 'sex',
type: 'radio',
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
{ label: '保密', value: 'secrecy' }
]
},
{
label: '爱好',
key: 'hobbies',
type: 'checkbox',
options: [
{ label: '阅读', value: 'reading' },
{ label: '旅行', value: 'traveling' },
{ label: '运动', value: 'sports' }
]
},
{
label: '出生日期',
key: 'dateOfBirth',
type: 'datePicker',
placeholder: '请选择出生日期'
},
{
label: '自定义组件',
key: 'customComponent',
type: HelloWorld
},
{
label: '默认组件',
key: 'defaultComponent',
placeholder: '默认组件...'
},
{
label: '自定义插槽',
key: 'customSlot'
}
];
const rules = {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 2, max: 10, message: '姓名长度在2-10个字符之间', trigger: 'blur' }
],
age: [{ required: true, message: '请输入年龄', trigger: 'blur' }],
sex: [{ required: true, message: '请选择性别', trigger: 'change' }],
hobbies: [{ required: true, message: '请选择至少一个爱好', trigger: 'change' }],
dateOfBirth: [{ required: true, message: '请选择出生日期', trigger: 'change' }]
};
const submit = () => {
formRef.value
.validate()
.then(() => {
console.log('表单验证通过,提交的数据:{}', formData.value);
})
.catch(() => {
console.error('表单验证失败');
});
};
const reset = () => {
formRef.value.resetFields();
console.log('表单已重置');
};
</script>
<style lang="scss" scoped>
.myBody {
margin: 1rem;
}
</style>
6、在组件中处理一下插槽渲染,如果有传入自定义插槽就渲染,否则渲染表单项组件
html
<template>
<el-form ref="formRef" :model="formData" :rules="rules">
<el-form-item v-for="item in props.items" :key="item.key" :prop="item.key" :label="item.label">
<slot :name="item.key">
<component :is="getComponent(item)" v-bind="getProps(item)" v-model="formData[item.key]">
<component
v-for="(option, index) in getOptions(item)"
:key="index"
:is="option.component"
v-bind="option.props"
></component>
</component>
</slot>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { ElCheckbox, ElCheckboxGroup, ElDatePicker, ElInput, ElInputNumber, ElRadio, ElRadioGroup } from 'element-plus';
import { omit } from 'lodash';
const formRef = ref();
const formData = defineModel<Record<string, any>>({ default: {} });
const props = defineProps(['items', 'rules']);
const componentMap: Record<string, Component> = {
input: ElInput,
number: ElInputNumber,
radio: ElRadioGroup,
checkbox: ElCheckboxGroup,
datePicker: ElDatePicker
};
const getComponent = (item: any) => {
if (item.type && typeof item.type !== 'string') {
return item.type;
}
return componentMap[item.type || 'input'];
};
const rootProps = ['label', 'key', 'type'];
const getProps = (item: any) => {
return omit(item, rootProps);
};
const getOptions = (item: any) => {
if (!item.options?.length) return [];
const componentMap: Record<string, Component> = {
radio: ElRadio,
checkbox: ElCheckbox
};
const component = componentMap[item.type];
if (!component) return [];
return item.options.map((option: any) => ({
component,
props: option
}));
};
defineExpose({
validate: (...args: any[]) => {
return formRef.value.validate(...args);
},
resetFields: (...args: any[]) => {
return formRef.value.resetFields(...args);
}
});
</script>
<style scoped lang="scss"></style>
7、页面效果,可以看到我们自定义的插槽已经渲染出来了

8、现在这个组件能满足很多场景了,但是还有可以添加的功能,例如:用户想自定义布局排版呢?
自定义表单布局
1、先在表单组件中添加布局组件,如果不传占位大小(span)则默认24(占满一行)
html
<template>
<el-form ref="formRef" :model="formData" :rules="rules">
<el-row>
<el-col v-for="item in props.items" :key="item.key" :span="item.span || 24">
<el-form-item :prop="item.key" :label="item.label">
<slot :name="item.key">
<component :is="getComponent(item)" v-bind="getProps(item)" v-model="formData[item.key]">
<component
v-for="(option, index) in getOptions(item)"
:key="index"
:is="option.component"
v-bind="option.props"
></component>
</component>
</slot>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import { ElCheckbox, ElCheckboxGroup, ElDatePicker, ElInput, ElInputNumber, ElRadio, ElRadioGroup } from 'element-plus';
import { omit } from 'lodash';
const formRef = ref();
const formData = defineModel<Record<string, any>>({ default: {} });
const props = defineProps(['items', 'rules']);
const componentMap: Record<string, Component> = {
input: ElInput,
number: ElInputNumber,
radio: ElRadioGroup,
checkbox: ElCheckboxGroup,
datePicker: ElDatePicker
};
const getComponent = (item: any) => {
if (item.type && typeof item.type !== 'string') {
return item.type;
}
return componentMap[item.type || 'input'];
};
const rootProps = ['label', 'key', 'type'];
const getProps = (item: any) => {
return omit(item, rootProps);
};
const getOptions = (item: any) => {
if (!item.options?.length) return [];
const componentMap: Record<string, Component> = {
radio: ElRadio,
checkbox: ElCheckbox
};
const component = componentMap[item.type];
if (!component) return [];
return item.options.map((option: any) => ({
component,
props: option
}));
};
defineExpose({
validate: (...args: any[]) => {
return formRef.value.validate(...args);
},
resetFields: (...args: any[]) => {
return formRef.value.resetFields(...args);
}
});
</script>
<style scoped lang="scss"></style>
2、页面上使用
html
<template>
<div class="myBody">
<el-card header="自定义表单">
<my-form ref="formRef" v-model="formData" :items="items" :rules="rules">
<template #customSlot>
<div>这是自定义的插槽</div>
</template>
</my-form>
<el-divider />
<div>
<div>表单数据</div>
<pre>{{ formData }}</pre>
</div>
<el-divider />
<el-space>
<el-button type="primary" @click="submit">提交</el-button>
<el-button @click="reset">重置</el-button>
</el-space>
</el-card>
</div>
</template>
<script lang="ts" setup>
import MyForm from '@/components/utils/MyForm.vue';
import HelloWorld from '@/pages/HelloWorld.vue';
const formRef = ref();
const formData = ref({});
const items = [
{
label: '姓名',
key: 'name',
type: 'input',
placeholder: '请输入姓名',
span: 12
},
{
label: '年龄',
key: 'age',
type: 'number',
placeholder: '请输入年龄',
span: 12
},
{
label: '性别',
key: 'sex',
type: 'radio',
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
{ label: '保密', value: 'secrecy' }
],
span: 12
},
{
label: '爱好',
key: 'hobbies',
type: 'checkbox',
options: [
{ label: '阅读', value: 'reading' },
{ label: '旅行', value: 'traveling' },
{ label: '运动', value: 'sports' }
],
span: 12
},
{
label: '出生日期',
key: 'dateOfBirth',
type: 'datePicker',
placeholder: '请选择出生日期',
span: 12
},
{
label: '自定义组件',
key: 'customComponent',
type: HelloWorld,
span: 12
},
{
label: '默认组件',
key: 'defaultComponent',
placeholder: '默认组件...',
span: 12
},
{
label: '自定义插槽',
key: 'customSlot',
span: 12
}
];
const rules = {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 2, max: 10, message: '姓名长度在2-10个字符之间', trigger: 'blur' }
],
age: [{ required: true, message: '请输入年龄', trigger: 'blur' }],
sex: [{ required: true, message: '请选择性别', trigger: 'change' }],
hobbies: [{ required: true, message: '请选择至少一个爱好', trigger: 'change' }],
dateOfBirth: [{ required: true, message: '请选择出生日期', trigger: 'change' }]
};
const submit = () => {
formRef.value
.validate()
.then(() => {
console.log('表单验证通过,提交的数据:{}', formData.value);
})
.catch(() => {
console.error('表单验证失败');
});
};
const reset = () => {
formRef.value.resetFields();
console.log('表单已重置');
};
</script>
<style lang="scss" scoped>
.myBody {
margin: 1rem;
}
</style>
3、页面效果

4、同一行表单项挨在一起了,我们在表单组件中添加一个间距(gutter)即可
html
<template>
<el-form ref="formRef" :model="formData" :rules="rules">
<el-row :gutter="16">
<el-col v-for="item in props.items" :key="item.key" :span="item.span || 24">
<el-form-item :prop="item.key" :label="item.label">
<slot :name="item.key">
<component :is="getComponent(item)" v-bind="getProps(item)" v-model="formData[item.key]">
<component
v-for="(option, index) in getOptions(item)"
:key="index"
:is="option.component"
v-bind="option.props"
></component>
</component>
</slot>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import { ElCheckbox, ElCheckboxGroup, ElDatePicker, ElInput, ElInputNumber, ElRadio, ElRadioGroup } from 'element-plus';
import { omit } from 'lodash';
const formRef = ref();
const formData = defineModel<Record<string, any>>({ default: {} });
const props = defineProps(['items', 'rules']);
const componentMap: Record<string, Component> = {
input: ElInput,
number: ElInputNumber,
radio: ElRadioGroup,
checkbox: ElCheckboxGroup,
datePicker: ElDatePicker
};
const getComponent = (item: any) => {
if (item.type && typeof item.type !== 'string') {
return item.type;
}
return componentMap[item.type || 'input'];
};
const rootProps = ['label', 'key', 'type'];
const getProps = (item: any) => {
return omit(item, rootProps);
};
const getOptions = (item: any) => {
if (!item.options?.length) return [];
const componentMap: Record<string, Component> = {
radio: ElRadio,
checkbox: ElCheckbox
};
const component = componentMap[item.type];
if (!component) return [];
return item.options.map((option: any) => ({
component,
props: option
}));
};
defineExpose({
validate: (...args: any[]) => {
return formRef.value.validate(...args);
},
resetFields: (...args: any[]) => {
return formRef.value.resetFields(...args);
}
});
</script>
<style scoped lang="scss"></style>
5、页面效果,现在有了间距,看上去舒服多了

6、基础功能都有了,在实际应用场景中,往往表单项是需要动态渲染的,我们来将动态渲染表单项功能实现
表单项动态渲染
1、在页面上简单的添加两个输入框,分别是:男生简介、女生简介,然后添加触发条件
html
<template>
<div class="myBody">
<el-card header="自定义表单">
<my-form ref="formRef" v-model="formData" :items="items" :rules="rules">
<template #customSlot>
<div>这是自定义的插槽</div>
</template>
</my-form>
<el-divider />
<div>
<div>表单数据</div>
<pre>{{ formData }}</pre>
</div>
<el-divider />
<el-space>
<el-button type="primary" @click="submit">提交</el-button>
<el-button @click="reset">重置</el-button>
</el-space>
</el-card>
</div>
</template>
<script lang="ts" setup>
import MyForm from '@/components/utils/MyForm.vue';
import HelloWorld from '@/pages/HelloWorld.vue';
const formRef = ref();
const formData = ref({}) as any;
const items = [
{
label: '姓名',
key: 'name',
type: 'input',
placeholder: '请输入姓名',
span: 12
},
{
label: '年龄',
key: 'age',
type: 'number',
placeholder: '请输入年龄',
span: 12
},
{
label: '性别',
key: 'sex',
type: 'radio',
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
{ label: '保密', value: 'secrecy' }
],
span: 12
},
{
label: '爱好',
key: 'hobbies',
type: 'checkbox',
options: [
{ label: '阅读', value: 'reading' },
{ label: '旅行', value: 'traveling' },
{ label: '运动', value: 'sports' }
],
span: 12
},
{
label: '出生日期',
key: 'dateOfBirth',
type: 'datePicker',
placeholder: '请选择出生日期',
span: 12
},
{
label: '自定义组件',
key: 'customComponent',
type: HelloWorld,
span: 12
},
{
label: '默认组件',
key: 'defaultComponent',
placeholder: '默认组件...',
span: 12
},
{
label: '自定义插槽',
key: 'customSlot',
span: 12
},
{
label: '男生简介',
key: 'maleIntroduction',
type: 'input',
placeholder: '请输入男生的简介',
hidden: !formData.value.sex || formData.value.sex !== 'male'
},
{
label: '女生简介',
key: 'femaleIntroduction',
type: 'input',
placeholder: '请输入女生的简介',
hidden: !formData.value.sex || formData.value.sex !== 'female'
}
];
const rules = {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 2, max: 10, message: '姓名长度在2-10个字符之间', trigger: 'blur' }
],
age: [{ required: true, message: '请输入年龄', trigger: 'blur' }],
sex: [{ required: true, message: '请选择性别', trigger: 'change' }],
hobbies: [{ required: true, message: '请选择至少一个爱好', trigger: 'change' }],
dateOfBirth: [{ required: true, message: '请选择出生日期', trigger: 'change' }]
};
const submit = () => {
formRef.value
.validate()
.then(() => {
console.log('表单验证通过,提交的数据:{}', formData.value);
})
.catch(() => {
console.error('表单验证失败');
});
};
const reset = () => {
formRef.value.resetFields();
console.log('表单已重置');
};
</script>
<style lang="scss" scoped>
.myBody {
margin: 1rem;
}
</style>
2、页面效果

3、我们在组件中处理一下隐藏渲染的功能,很简单,写一个获取标点项的函数(getItems),过滤一下隐藏项即可
html
<template>
<el-form ref="formRef" :model="formData" :rules="rules">
<el-row :gutter="16">
<el-col v-for="item in getItems" :key="item.key" :span="item.span || 24">
<el-form-item :prop="item.key" :label="item.label">
<slot :name="item.key">
<component :is="getComponent(item)" v-bind="getProps(item)" v-model="formData[item.key]">
<component
v-for="(option, index) in getOptions(item)"
:key="index"
:is="option.component"
v-bind="option.props"
></component>
</component>
</slot>
</el-form-item>
</el-col>
</el-row>
</el-form>
</template>
<script setup lang="ts">
import { ElCheckbox, ElCheckboxGroup, ElDatePicker, ElInput, ElInputNumber, ElRadio, ElRadioGroup } from 'element-plus';
import { omit } from 'lodash';
const formRef = ref();
const formData = defineModel<Record<string, any>>({ default: {} });
const props = defineProps(['items', 'rules']);
const getItems = computed(() => {
return props.items.filter((item: any) => !item.hidden);
});
const componentMap: Record<string, Component> = {
input: ElInput,
number: ElInputNumber,
radio: ElRadioGroup,
checkbox: ElCheckboxGroup,
datePicker: ElDatePicker
};
const getComponent = (item: any) => {
if (item.type && typeof item.type !== 'string') {
return item.type;
}
return componentMap[item.type || 'input'];
};
const rootProps = ['label', 'key', 'type'];
const getProps = (item: any) => {
return omit(item, rootProps);
};
const getOptions = (item: any) => {
if (!item.options?.length) return [];
const componentMap: Record<string, Component> = {
radio: ElRadio,
checkbox: ElCheckbox
};
const component = componentMap[item.type];
if (!component) return [];
return item.options.map((option: any) => ({
component,
props: option
}));
};
defineExpose({
validate: (...args: any[]) => {
return formRef.value.validate(...args);
},
resetFields: (...args: any[]) => {
return formRef.value.resetFields(...args);
}
});
</script>
<style scoped lang="scss"></style>
4、页面效果,隐藏项默认会隐藏,当选择性别后为什么没有显示对应性别的简介输入框?因为我们页面上的items是写死的值,当组件数据变化后,页面上的items内的数据不会随着变化,套一层计算属性即可

html
<template>
<div class="myBody">
<el-card header="自定义表单">
<my-form ref="formRef" v-model="formData" :items="items" :rules="rules">
<template #customSlot>
<div>这是自定义的插槽</div>
</template>
</my-form>
<el-divider />
<div>
<div>表单数据</div>
<pre>{{ formData }}</pre>
</div>
<el-divider />
<el-space>
<el-button type="primary" @click="submit">提交</el-button>
<el-button @click="reset">重置</el-button>
</el-space>
</el-card>
</div>
</template>
<script lang="ts" setup>
import MyForm from '@/components/utils/MyForm.vue';
import HelloWorld from '@/pages/HelloWorld.vue';
const formRef = ref();
const formData = ref({}) as any;
const items = computed(() => [
{
label: '姓名',
key: 'name',
type: 'input',
placeholder: '请输入姓名',
span: 12
},
{
label: '年龄',
key: 'age',
type: 'number',
placeholder: '请输入年龄',
span: 12
},
{
label: '性别',
key: 'sex',
type: 'radio',
options: [
{ label: '男', value: 'male' },
{ label: '女', value: 'female' },
{ label: '保密', value: 'secrecy' }
],
span: 12
},
{
label: '爱好',
key: 'hobbies',
type: 'checkbox',
options: [
{ label: '阅读', value: 'reading' },
{ label: '旅行', value: 'traveling' },
{ label: '运动', value: 'sports' }
],
span: 12
},
{
label: '出生日期',
key: 'dateOfBirth',
type: 'datePicker',
placeholder: '请选择出生日期',
span: 12
},
{
label: '自定义组件',
key: 'customComponent',
type: HelloWorld,
span: 12
},
{
label: '默认组件',
key: 'defaultComponent',
placeholder: '默认组件...',
span: 12
},
{
label: '自定义插槽',
key: 'customSlot',
span: 12
},
{
label: '男生简介',
key: 'maleIntroduction',
type: 'input',
placeholder: '请输入男生的简介',
hidden: !formData.value.sex || formData.value.sex !== 'male'
},
{
label: '女生简介',
key: 'femaleIntroduction',
type: 'input',
placeholder: '请输入女生的简介',
hidden: !formData.value.sex || formData.value.sex !== 'female'
}
]);
const rules = {
name: [
{ required: true, message: '请输入姓名', trigger: 'blur' },
{ min: 2, max: 10, message: '姓名长度在2-10个字符之间', trigger: 'blur' }
],
age: [{ required: true, message: '请输入年龄', trigger: 'blur' }],
sex: [{ required: true, message: '请选择性别', trigger: 'change' }],
hobbies: [{ required: true, message: '请选择至少一个爱好', trigger: 'change' }],
dateOfBirth: [{ required: true, message: '请选择出生日期', trigger: 'change' }]
};
const submit = () => {
formRef.value
.validate()
.then(() => {
console.log('表单验证通过,提交的数据:{}', formData.value);
})
.catch(() => {
console.error('表单验证失败');
});
};
const reset = () => {
formRef.value.resetFields();
console.log('表单已重置');
};
</script>
<style lang="scss" scoped>
.myBody {
margin: 1rem;
}
</style>
5、页面效果,功能都正常


6、现在组件基本上能满足大多数场景了,至于其他一些小细节功能和细节优化工作就交给你们了
结语
本文旨在提供思路,代码和风格不用学我的,只要思路学会了所有组件库的动态表单封装都没问题了
如有不懂或者疑问,可在下方留言或私信我,看到必回
希望对你能有所帮助,如果觉得文章写的不错,欢迎点赞/收藏,三克油~