Element Plus 自定义(动态)表单组件

演示环境

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、现在组件基本上能满足大多数场景了,至于其他一些小细节功能和细节优化工作就交给你们了

结语

本文旨在提供思路,代码和风格不用学我的,只要思路学会了所有组件库的动态表单封装都没问题了

如有不懂或者疑问,可在下方留言或私信我,看到必回

希望对你能有所帮助,如果觉得文章写的不错,欢迎点赞/收藏,三克油~

相关推荐
爱编程的喵12 分钟前
React Router Dom 初步:从传统路由到现代前端导航
前端·react.js
阳火锅27 分钟前
Vue 开发者的外挂工具:配置一个 JSON,自动造出一整套页面!
javascript·vue.js·面试
每天吃饭的羊28 分钟前
react中为啥使用剪头函数
前端·javascript·react.js
Nicholas681 小时前
Flutter帧定义与60-120FPS机制
前端
多啦C梦a1 小时前
【适合小白篇】什么是 SPA?前端路由到底在路由个啥?我来给你聊透!
前端·javascript·架构
薛定谔的算法1 小时前
《长安的荔枝·事件流版》——一颗荔枝引发的“冒泡惨案”
前端·javascript·编程语言
中微子1 小时前
CSS 的 position 你真的理解了吗?
前端·css
谜构1 小时前
【0编码】我使用Trae AI开发了一个【随手记账单格式化工具】
前端
G_whang1 小时前
jenkins部署前端vue项目使用Docker+Jenkinsfile方式
前端·vue.js·jenkins
ZhangApple1 小时前
微信自动化工具:让自己的微信变成智能机器人!
前端·后端