[Vue3] 动态表单设计:链表结构的实现与优化
发布于
2025年1月29日
更新于
2025年1月30日
36
分钟阅读
Vue
在复杂业务场景中,表单项通常并非固定,而是需要根据条件动态显示或隐藏。比如,一个表单中 OtherFavorite 需要根据 Favorite 的值来决定是否显示。这类场景下,常规的实现方法是通过 if 条件语句控制显示逻辑,但当动态表单项较多时,代码复杂度会显著增加,维护成本也随之上升。
本文将分享一种更灵活的链表结构设计,帮助解决动态表单显示及校验的复杂问题。
问题背景
以下是一个简单的表单需求示例:
| 顺序 | 项 | 类型 | 值 | 显示条件 |
|---|---|---|---|---|
| 1 | Name | text | 字符串 | 无条件 |
| 2 | Address | text | 字符串 | 无条件 |
| 3 | Favorite | checkbox | [“Apple”, “Banana”, “Cherry”, “Other”] | 无条件 |
| 4 | OtherFavorite | text | 字符串 | 当favorite的选项里含有Other的时候显示 |
| 5 | Comment | textarea | 字符串 | 无条件 |
普通实现中,我们可能会用如下代码通过 v-if 动态显示 OtherFavorite;同时,利用 watch 动态调整校验规则:
<template>
<el-form
ref="ruleFormRef"
style="max-width: 600px"
:model="ruleForm"
:rules="rules"
label-width="auto"
:validate-on-rule-change="false"
status-icon
>
<el-form-item label="Name" prop="name">
<el-input v-model="ruleForm.name" />
</el-form-item>
<el-form-item label="Address" prop="address">
<el-input v-model="ruleForm.address" />
</el-form-item>
<el-form-item label="Favorite" prop="favorite">
<el-checkbox-group v-model="ruleForm.favorite">
<el-checkbox label="Apple" value="Apple" />
<el-checkbox label="Banana" value="Banana" />
<el-checkbox label="Cherry" value="Cherry" />
<el-checkbox label="Other" value="Other" />
</el-checkbox-group>
</el-form-item>
<!-- 通过判断favorite的值判断other是否显示 -->
<el-form-item v-if="ruleForm.favorite.includes('Other')" label="OtherFavorite" prop="other">
<el-input v-model="ruleForm.other" />
</el-form-item>
<el-form-item label="Comment" prop="comment">
<el-input v-model="ruleForm.comment" />
</el-form-item>
<el-form-item>
<el-button type="primary" @click="submitForm(ruleFormRef)"> Create </el-button>
<el-button @click="resetForm(ruleFormRef)">Reset</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue';
import type { FormInstance, FormRules } from 'element-plus';
import type { FormInputs } from '@/models/form.model';
const ruleFormRef = ref<FormInstance>();
const ruleForm = ref<FormInputs>({
name: '',
address: '',
favorite: [],
other: '',
comment: '',
});
const rules = ref<FormRules<FormInputs>>({
name: [{ required: true, message: 'Please input name', trigger: 'blur' }],
address: [{ required: true, message: 'Please input address', trigger: 'blur' }],
favorite: [{ required: true, message: 'Please select favorite', trigger: 'change' }],
comment: [{ required: true, message: 'Please input comment', trigger: 'blur' }],
});
// 监听favorite的值,动态设置other的校验规则
watch(
() => ruleForm.value.favorite,
(value) => {
if (value.includes('Other')) {
rules.value.other = [
{ required: true, message: 'Please input other favorite', trigger: 'blur' },
];
} else {
rules.value.other = [];
}
},
);
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid, fields) => {
if (valid) {
console.log('submit!');
} else {
console.log('error submit!', fields);
}
});
};
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.resetFields();
};
</script>
这种方法在简单场景下可以胜任,但当动态项增多或逻辑更复杂时,代码将难以维护。
解决方案:链表结构
我们可以将表单项抽象为一个链表节点,每个节点包含以下信息:
- 表单项的基本信息(如 id、label、type 等)。
- 动态显示的条件逻辑。
- 下一个节点的指向。
数据结构设计
首先定义表单项元数据类型:
import type { FormItemRule } from 'element-plus';
/**
* 表单项类型
*/
export type FormInputs = {
name: string;
address: string;
favorite: string[];
other?: string;
comment: string;
};
/**
* 表单项元数据
*/
export type FormItemMeta = {
/** 表单项ID,必须是表单项类型的key */
id: keyof FormInputs;
/** 表单项标签 */
label: string;
/** 表单项类型 */
type: 'text' | 'checkbox' | 'textarea';
/** 表单项校验规则 */
rules: Array<FormItemRule> | undefined;
/** 当表单项是checkbox的时候,显示的选项 */
options?: Array<{ label: string; value: string }>;
/** 下一个表单项 */
next?: FormItemMeta | Array<{ condition: string; item: FormItemMeta } | FormItemMeta>;
};
基于上述结构,我们可以用链表形式组织表单项:
export const formItemMeta: FormItemMeta = {
id: 'name',
label: 'Name',
type: 'text',
rules: [{ required: true, message: 'Please input name', trigger: 'blur' }],
// name的下一个表单项是address
next: {
id: 'address',
label: 'address',
type: 'text',
rules: [{ required: true, message: 'Please input address', trigger: 'blur' }],
// address的下一个表单项是favorite
next: {
id: 'favorite',
label: 'Favorite',
type: 'checkbox',
rules: [{ required: true, message: 'Please select favorite', trigger: 'change' }],
options: [
{ label: 'Apple', value: 'apple' },
{ label: 'Banana', value: 'banana' },
{ label: 'Cherry', value: 'cherry' },
{ label: 'Other', value: 'other' },
],
// favorite的下一个表单项是other和comment
next: [
// other表单项的显示条件是favorite中选择了Other
{
condition: 'other', // 显示条件
item: {
id: 'other',
label: 'Other',
type: 'text',
rules: [{ required: true, message: 'Please input other', trigger: 'blur' }],
},
},
// comment表单项是无条件显示的
{
id: 'comment',
label: 'Comment',
type: 'textarea',
rules: [{ required: true, message: 'Please input Comment', trigger: 'blur' }],
},
],
},
},
};
渲染逻辑:递归实现
定义一个递归组件 FormItemRenderer.vue,根据元数据动态渲染表单项:
<!-- FormItemRenderer.vue -->
<template>
<el-form-item :label="meta.label" :prop="meta.id">
<!-- text类型的表单项 -->
<el-input v-if="meta.type === 'text'" :id="meta.id" v-model="value" />
<!-- textarea类型的表单项 -->
<el-input v-if="meta.type === 'textarea'" type="textarea" :id="meta.id" v-model="value" />
<!-- checkboxt类型的表单项 -->
<el-checkbox-group v-if="meta.type === 'checkbox'" v-model="value">
<el-checkbox
v-for="option in meta.options"
:key="option.value"
:label="option.label"
:value="option.value"
/>
</el-checkbox-group>
</el-form-item>
<!-- 渲染下一个表单项 -->
<FormItemRenderer
v-for="nextFormItem in nextFormItemList"
:key="nextFormItem.id"
:meta="nextFormItem"
:values="values"
v-on:value-changed="onValueChanged"
v-on:rule-changed="onRuleChanged"
/>
</template>
<script setup lang="ts">
import type { FormInputs, FormItemMeta } from '@/models/form.model';
import type { FormItemRule } from 'element-plus';
import { computed, onMounted, onUnmounted, type PropType } from 'vue';
const props = defineProps({
meta: {
type: Object as PropType<FormItemMeta>,
required: true,
},
values: {
type: Object as PropType<FormInputs>,
required: true,
},
onValueChanged: {
type: Function as PropType<(id: string, value: string | string[] | undefined) => void>,
required: true,
},
onRuleChanged: {
type: Function as PropType<(id: string, rules: Array<FormItemRule> | undefined) => void>,
required: true,
},
});
const value = computed({
get: () => props.values[props.meta.id],
set: (newValue: string | string[]) => props.onValueChanged(props.meta.id, newValue),
});
/**
* 获取下一个表单项
*/
const nextFormItemList = computed(() => {
// 如果没有下一个表单项,则返回空数组
if (props.meta.next === undefined) {
return [];
}
// 如果下一个表单项是一个FormItemMeta类型的数据,则直接返回
if ('id' in props.meta.next) {
return [props.meta.next] as Array<FormItemMeta>;
}
// 如果下一个表单项是一个数组,则遍历数组,根据条件返回下一个表单项
const next = props.meta.next as Array<{ condition: string; item: FormItemMeta } | FormItemMeta>;
let nextItems: Array<FormItemMeta> = [];
for (const nextItem of next) {
if ('condition' in nextItem && 'item' in nextItem) {
// 如果下一个表单项有条件
const nextItemWithCondition = nextItem as { condition: string; item: FormItemMeta };
// 如果当前表单项的值是一个stirng类型,并且等于下一个表单项的条件,则将下一个表单项添加到nextItems数组中
if (typeof value.value === 'string' && value.value === nextItemWithCondition.condition) {
nextItems.push(nextItemWithCondition.item);
}
// 如果当前表单项的值是一个数组,并且包含下一个表单项的条件,则将下一个表单项添加到nextItems数组中
if (Array.isArray(value.value) && value.value.includes(nextItemWithCondition.condition)) {
nextItems.push(nextItemWithCondition.item);
}
} else {
// 如果下一个表单项没有条件,则直接将下一个表单项添加到nextItems数组中
nextItems.push(nextItem);
}
}
return nextItems;
});
/**
* 当组件挂载时,如果表单项有规则,则将规则传递给父组件
*/
onMounted(() => {
if (props.meta.rules) {
props.onRuleChanged(props.meta.id, props.meta.rules);
}
});
/**
* 当组件卸载时,将表单项的值和规则清空
*/
onUnmounted(() => {
props.onRuleChanged(props.meta.id, undefined);
props.onValueChanged(props.meta.id, undefined);
});
</script>
主组件渲染
在主组件中,通过 FormItemRenderer.vue 渲染整个表单链表:
<template>
<el-form
ref="ruleFormRef"
style="max-width: 600px"
:model="form"
:rules="rules"
label-width="auto"
:validate-on-rule-change="false"
status-icon
>
<FormItemRenderer
:meta="formItemMeta"
:values="form"
v-on:value-changed="onValueChanged"
v-on:rule-changed="onRuleChanged"
/>
<el-form-item>
<el-button type="primary" @click="submitForm(ruleFormRef)"> Create </el-button>
<el-button @click="resetForm(ruleFormRef)">Reset</el-button>
</el-form-item>
</el-form>
</template>
<script lang="ts" setup>
import { ref } from 'vue';
import type { FormInstance, FormItemRule, FormRules } from 'element-plus';
import type { FormInputs } from '@/models/form.model';
import FormItemRenderer from './FormItemRenderer.vue';
import { formItemMeta } from '@/models/form.model';
const ruleFormRef = ref<FormInstance>();
const form = ref<FormInputs>({
name: '',
address: '',
favorite: [],
other: '',
comment: '',
});
const rules = ref<FormRules<FormInputs>>({});
const onValueChanged = (id: string, value: string | string[] | undefined) => {
form.value = {
...form.value,
[id]: value,
};
};
const onRuleChanged = (id: string, itemRules: Array<FormItemRule> | undefined) => {
rules.value = {
...rules.value,
[id]: itemRules,
};
};
const submitForm = async (formEl: FormInstance | undefined) => {
if (!formEl) return;
await formEl.validate((valid, fields) => {
if (valid) {
console.log('submit!');
} else {
console.log('error submit!', fields);
}
});
};
const resetForm = (formEl: FormInstance | undefined) => {
if (!formEl) return;
formEl.resetFields();
};
</script>
总结
通过链表结构设计动态表单,我们不仅可以简化代码逻辑,还能提升动态表单的扩展性与可维护性。无论是条件复杂的动态显示,还是规则变更,都能通过元数据定义轻松实现。