一、为什么表单要分组处理?
- 方便表单字段的复用:例如,你的表单有十个字段会在很多的表单都会用到,那么表单则需要进行分组进行表单复用;
- 实现不同角色的表单权限控制:例如一个表单有60个字段,角色A拥有表单前30个的权限,角色B拥有其它30个字段的权限,正常想法,可能会在表单的页面直接获取角色的权限,通过v-if来控制不同字段的权限,这样的话,权限逻辑和表单逻辑就会堆砌在一起,简单一点的表单可能没有太大影响,如果表单逻辑很复杂或者表单字段过多,就会导致代码的臃肿,后期难以维护;若表单进行了分组处理,可以增加一个业务组件来处理权限的逻辑,表单的校验等逻辑则在分组表单中完成。
二、表单分组处理实现思路
- 表单字段按业务逻辑进行划分成不同的组,一个组代表一个表单子组件;
- 表单子组件实现校验字段的方法,如果有需要重置表单字段需求,则增加表单重置方法;
- 增加的校验方法(校验成功则返回子表单所有字段,否则返回null)和表单重置方法,在子组件onMounted钩子中将两个方法传递给业务组件(父组件),业务组件维护两个数组分别存储子表单组件的校验方法和重置表单方法;
- 用户点击提交表单或者重置表单时,触发对应维护的数组即可;
三、具体实现代码
3.1 将表单分为两组为例,目录结构如下:
效果图如下:
3.2 ComplexForm.vue业务组件代码如下:
<script setup lang="ts">
import FormItemA from "@/views/component/complex-form/FormItemA.vue";
import FormItemB from "@/views/component/complex-form/FormItemB.vue";
import { reactive, provide, ref, onMounted } from "vue";
defineOptions({
name: "ComplexForm"
});
const formData = ref({});
// 向子组件注入表单的初始化的值或者回填的值,一般是编辑表单的时候需要传递
provide("defaultFormData", formData);
const getFormData = () => {
formData.value = {
name: "Hello",
region: "shanghai",
count: "5",
date1: "",
date2: "",
delivery: false,
type: [],
resource: "",
desc: "hello world"
};
};
onMounted(() => {});
// 将子组件表单的参数添加到formData中汇总
const addParamsToFormData = (params: any) => {
Object.assign(formData.value, params);
};
const formEventList = reactive([]);
// 存储子组件的表单验证方法,在提交时统一调用
const addEventToFormEventList = (event: Function) => {
formEventList.push(event);
};
const submitHandler = () => {
let successFlag = true;
console.log("formEventList=", formEventList);
formEventList.forEach((func, index, list) => {
// 执行子组件的方法(表单验证+触发add-params添加参数)
func()
.then(res => {
if (!res) {
// 如果表单存在一个不满足,则设置标识为false,后续根据这个标识来确定是否可以提交表单
successFlag = false;
} else {
// 表单验证通过后,添加参数到formData
addParamsToFormData(res);
}
// 执行最后一个表单验证并且通过后,提交表单处理
if (index === list.length - 1 && successFlag) {
console.log("表单验证通过,提交表单");
}
})
.catch(err => {
console.log("返回错误", err);
});
});
};
const formResetEvent = reactive([]);
// 存储子组件的表单重置方法,在重置时统一调用
const addFormResetEvent = func => {
formResetEvent.push(func);
};
const resetForm = () => {
formResetEvent.forEach(func => {
func();
});
};
</script>
<template>
<div>{{ formData }}</div>
<FormItemA
@add-submit-event="addEventToFormEventList"
@add-reset-event="addFormResetEvent"
/>
<FormItemB
@add-submit-event="addEventToFormEventList"
@add-reset-event="addFormResetEvent"
/>
<el-button type="primary" @click="submitHandler">表单提交</el-button>
<el-button @click="resetForm">重置表单</el-button>
<el-button @click="getFormData">模拟接口请求表单回填数据</el-button>
</template>
<style scoped lang="scss"></style>
3.2 FormItemA.vue 子组件代码如下:
<script lang="ts" setup>
import {
reactive,
ref,
defineEmits,
onMounted,
inject,
watch,
nextTick
} from "vue";
import type { FormInstance, FormRules } from "element-plus";
interface RuleForm {
name: string;
region: string;
count: string;
}
interface Emits {
(e: "add-submit-event", event: Function): void;
(e: "add-reset-event", event: Function): void;
}
const emits = defineEmits<Emits>();
// 接受注入的默认表单数据(表单回填)
const defaultFormData = inject("defaultFormData");
onMounted(() => {
// 将当前表单验证方法传递给父组件维护的数组,父组件点击提交时,统一遍历数组进行表单验证
emits("add-submit-event", submitForm);
emits("add-reset-event", resetForm);
});
const setDefaultFormData = (ruleForm, sourceForm) => {
console.log("666666--sourceForm", sourceForm);
for (const key in ruleForm) {
if (Object.prototype.hasOwnProperty.call(sourceForm, key)) {
ruleForm[key] = JSON.parse(JSON.stringify(sourceForm[key]));
}
}
};
watch(
() => defaultFormData.value,
() => {
console.log("watch监听");
// 回填表单数据时,需要加nextTick,否则ruleFormRef.value.resetFields()初始化表单时,会初始化为赋值后的表单数据(无法达到真正初始化表单为空值)
nextTick(() => {
setDefaultFormData(ruleForm, defaultFormData.value);
});
},
{
immediate: true
}
);
const formSize = ref("default");
const ruleFormRef = ref<FormInstance>();
const ruleForm = reactive<RuleForm>({
name: "Hello",
region: "",
count: ""
});
const rules = reactive<FormRules<RuleForm>>({
name: [
{ required: true, message: "Please input Activity name", trigger: "blur" },
{ min: 3, max: 5, message: "Length should be 3 to 5", trigger: "blur" }
],
region: [
{
required: true,
message: "Please select Activity zone",
trigger: "change"
}
],
count: [
{
required: true,
message: "Please select Activity count",
trigger: "change"
}
]
});
// 单个表单提交,校验通过则返回表单的字段,校验失败则返回null
const submitForm = () => {
return new Promise((resolve, reject) => {
if (!ruleFormRef.value) return resolve(null);
ruleFormRef.value.validate((valid, fields) => {
if (valid) {
console.log("formItemA---submit!");
resolve(ruleForm);
} else {
resolve(null);
}
});
});
};
const resetForm = () => {
if (!ruleFormRef.value) return;
ruleFormRef.value.resetFields();
};
const options = Array.from({ length: 10000 }).map((_, idx) => ({
value: `${idx + 1}`,
label: `${idx + 1}`
}));
</script>
<template>
<el-form
ref="ruleFormRef"
:model="ruleForm"
:rules="rules"
label-width="120px"
class="demo-ruleForm"
:size="formSize"
status-icon
>
<el-form-item label="Activity name" prop="name">
<el-input v-model="ruleForm.name" />
</el-form-item>
<el-form-item label="Activity zone" prop="region">
<el-select v-model="ruleForm.region" placeholder="Activity zone">
<el-option label="Zone one" value="shanghai" />
<el-option label="Zone two" value="beijing" />
</el-select>
</el-form-item>
<el-form-item label="Activity count" prop="count">
<el-select-v2
v-model="ruleForm.count"
placeholder="Activity count"
:options="options"
/>
</el-form-item>
</el-form>
</template>
3.3 FormItemB.vue 子组件代码如下
<script lang="ts" setup>
import {
reactive,
ref,
defineEmits,
onMounted,
watch,
nextTick,
inject
} from "vue";
import type { FormInstance, FormRules } from "element-plus";
interface RuleForm {
date1: string;
date2: string;
delivery: boolean;
type: string[];
resource: string;
desc: string;
}
interface Emits {
(e: "add-submit-event", event: Function): void;
(e: "add-reset-event", event: Function): void;
}
const emits = defineEmits<Emits>();
// 接受注入的默认表单数据(表单回填)
const defaultFormData = inject("defaultFormData");
onMounted(() => {
// 将当前表单验证方法传递给父组件维护的数组,父组件点击提交时,统一遍历数组进行表单验证
emits("add-submit-event", submitForm);
emits("add-reset-event", resetForm);
});
const setDefaultFormData = (ruleForm, sourceForm) => {
console.log("666666--sourceForm", sourceForm);
for (const key in ruleForm) {
if (Object.prototype.hasOwnProperty.call(sourceForm, key)) {
ruleForm[key] = JSON.parse(JSON.stringify(sourceForm[key]));
}
}
};
watch(
() => defaultFormData.value,
() => {
console.log("watch监听");
// 回填表单数据时,需要加nextTick,否则ruleFormRef.value.resetFields()初始化表单时,会初始化为赋值后的表单数据(无法达到真正初始化表单为空值)
nextTick(() => {
setDefaultFormData(ruleForm, defaultFormData.value);
});
},
{
immediate: true
}
);
const formSize = ref("default");
const ruleFormRef = ref<FormInstance>();
const ruleForm = reactive<RuleForm>({
date1: "",
date2: "",
delivery: false,
type: [],
resource: "",
desc: ""
});
const rules = reactive<FormRules<RuleForm>>({
date1: [
{
type: "date",
required: true,
message: "Please pick a date",
trigger: "change"
}
],
date2: [
{
type: "date",
required: true,
message: "Please pick a time",
trigger: "change"
}
],
type: [
{
type: "array",
required: true,
message: "Please select at least one activity type",
trigger: "change"
}
],
resource: [
{
required: true,
message: "Please select activity resource",
trigger: "change"
}
],
desc: [
{ required: true, message: "Please input activity form", trigger: "blur" }
]
});
const submitForm = () => {
return new Promise((resolve, reject) => {
if (!ruleFormRef.value) return resolve(null);
ruleFormRef.value.validate((valid, fields) => {
if (valid) {
console.log("formItemB---submit!");
resolve(ruleForm);
} else {
// console.log("error submit!", fields);
resolve(null);
}
});
});
};
const resetForm = () => {
if (!ruleFormRef.value) return;
ruleFormRef.value.resetFields();
};
</script>
<template>
<el-form
ref="ruleFormRef"
:model="ruleForm"
:rules="rules"
label-width="120px"
class="demo-ruleForm"
:size="formSize"
status-icon
>
<el-form-item label="Activity time" required>
<el-col :span="11">
<el-form-item prop="date1">
<el-date-picker
v-model="ruleForm.date1"
type="date"
label="Pick a date"
placeholder="Pick a date"
style="width: 100%"
/>
</el-form-item>
</el-col>
<el-col class="text-center" :span="2">
<span class="text-gray-500">-</span>
</el-col>
<el-col :span="11">
<el-form-item prop="date2">
<el-time-picker
v-model="ruleForm.date2"
label="Pick a time"
placeholder="Pick a time"
style="width: 100%"
/>
</el-form-item>
</el-col>
</el-form-item>
<el-form-item label="Instant delivery" prop="delivery">
<el-switch v-model="ruleForm.delivery" />
</el-form-item>
<el-form-item label="Activity type" prop="type">
<el-checkbox-group v-model="ruleForm.type">
<el-checkbox label="Online activities" name="type" />
<el-checkbox label="Promotion activities" name="type" />
<el-checkbox label="Offline activities" name="type" />
<el-checkbox label="Simple brand exposure" name="type" />
</el-checkbox-group>
</el-form-item>
<el-form-item label="Resources" prop="resource">
<el-radio-group v-model="ruleForm.resource">
<el-radio label="Sponsorship" />
<el-radio label="Venue" />
</el-radio-group>
</el-form-item>
<el-form-item label="Activity form" prop="desc">
<el-input v-model="ruleForm.desc" type="textarea" />
</el-form-item>
</el-form>
</template>