背景
日常开发中,表单项满足一定条件才显示,也是一种常见的场景。举个例子:
有个"开关",打开时显示 "性别",关闭时不显示"性别"。
"性别" 为 "男" 时,显示 "单选框"。
"性别"为 "女" 时,显示 "多选"。
上篇博客 讲了如何设计一个动态表单,不过,对于上面这种场景,还不支持。本篇文章就专门讨论“动态显示表单项”如何设计和实现。
需求分析
由需求背景可以看出,一个表单项的值改变,可以影响多个表单项。
表单项上下游之间,可能存在级联的情况。
方案设计
我们首先思考 2 个问题:表单项如何动态显示?如何联动?
表单项如何动态显示?
一说起响应式状态的动态更新,我们首先想到 Vue 的 computed 计算属性。
只要给表单项添加 show 属性,值为 computed 计算属性,即可动态更新。
Vue3 的 computed 组合式 API,可以动态创建计算属性。这就为我们在函数上下文中创建计算属性提供了可能。
e.g.
<el-form-item v-if="item.show" label="item.label"> </el-form-item>
item.show = computed(() => {
return a.value > 0;
});
表单项如何联动?
由于计算属性中,可以依赖其他变量,包括其他 计算属性,其他 计算属性 值的更新,会自动触发本 计算属性 的更新。
一个简单直接的思路就是,借助 Vue 的 计算属性 来自动跟踪依赖项的状态,把表单项的联动转化为 Vue 的响应式更新。
e.g.
item1.show = computed(() => {
return a.value > 0;
});
item2.show = computed(() => {
return item1.show && b.value > 0;
});
item3.show = computed(() => {
return item2.show && c.value > 0;
});
解决了上面的 2 个问题,我们得到了最简单的方案:
当可以动态创建计算属性时,动态表单组件传入 form-items 参数,每个 item 对象有 show 属性,值为 计算属性,在 计算属性 中写业务代码定义表单项显示的条件。
当不能动态创建 计算属性 时,可以将每个表单项显示的逻辑封装成函数,在动态表单组件外定义,通过 计算属性 返回需要显示的表单项。
Vue 会自动追踪函数依赖的变量,当变量值修改后,自动更新计算属性的值。
需要动态创建计算属性的实现方式 e.g.
<dynamic-form :formItems="formItems"></dynamic-form>
组件内的实现
<el-form-item v-if="item.show"></el-form-item>
const formItems = ref([
{
prop: "state",
label: "状态",
show: computed(() => {
return a.value > 0;
}),
},
{
prop: "sex",
label: "性别",
show: computed(() => {
return model.sex === "man";
}),
},
]);
不需要动态创建计算属性的实现方式
<dynamic-form :formItems="formItems"></dynamic-form>
const formItems = ref([
{
prop: "state",
label: "状态",
show: () => {
return a.value > 0;
},
},
{
prop: "sex",
label: "性别",
show: () => {
return model.sex === "man";
},
},
]);
const displayedFormItems = computed(() => {
return formItems.value.filter((item) => item.show);
});
这样做可行,但是不够优雅:
- 当一个条件控制多个表单项时,每个表单项的 show 属性都要传入同一个
计算属性的引用(推荐),或者把计算属性复制粘贴一遍(不推荐)。 - 不直观:不容易看出哪个条件改变后,会直接影响到哪些表单项
我们可以再优化一下,将 计算属性 抽离出 formItems 数组,单独维护表单项显示的条件。那么,如何定义表单项显示的条件?
如何定义表单项显示的条件?
我们从需求背景和需求分析中可以提取到一个逻辑关系:当 xxx 时 显示 yyy
其实就是
if(满足条件) then {显示相关表单项}
的关系。
由于条件判断逻辑是在动态表单组件外编写的,所以,我们需要将条件判断逻辑封装成一个函数,传给动态表单组件,来让动态表单组件计算出是否显示表单项:函数返回 true,就显示受影响的表单项;函数返回 false,就不显示受影响的表单项
condition: () => boolean;
当一个条件变化时,可能会影响到多个表单项,所以,我们可以用一个数组来表示受影响的表单项。
把条件记为 condition,受影响的表单项记为 action
action: Array<string>;
条件和受影响的表单项是一一对应的,所以,可以保存到同一个对象中。
{
condition: () => boolean,
action: Array<string>
}
一个表单中可能存在多个这样的关系,所以,可以用一个数组来存放所有关系。
[{
condition: () => boolean,
action: Array<string>
}]
这就是我们需要的描述表单项显示条件的数组。
condition 和 action 的作用见下表
| 属性名 | 作用 | 类型 |
|---|---|---|
| condition | 条件判断函数,无参数,返回 boolean 值 | ()=>boolean |
| action | 满足条件时需要显示的表单项的编号,组成的数组,编号用 string 表示 | Array |
把逻辑写成 condition-action 的形式,还会带来另一个问题:多个不同的条件,对应同一个表单项。
当多个不同的条件,对应同一个表单项时,如何处理?
举个例子:
a>0 时显示开关,b>0 时也显示开关,以哪个条件为准呢?
[{
condition: () => {
return a.value > 0
},
action: ['switch']
})
},{
condition: () => {
return b.value > 0
},
action: ['switch']
}]
这时的处理方式可以自行决定,我们采用的是以最后一个条件为准。
condition-action 数组设计好了,下一步就是遍历数组,生成计算属性,绑定到表单的show 属性上
如何遍历 condition-action 数组,给表单项绑定计算属性?
我们需要实现一个函数:
传入 2 个参数: condition-action 数组,formItems 表单项数组
返回:原 formItems 数组,修改了每个表单项的 show 属性。
实现思路:
既然 condition 已经是一个函数,那么,我们可以直接把 condition 传给 computed。
把 computed 赋给表单项的 show 属性,即可绑定。
要赋给哪些表单项呢?肯定是 action 中的编号对应的表单项。
为了高效查找,我们可以预先用一个 Map 存放所有的表单项,直接根据编号快速查找表单项。
示例代码
const map = formItems.value.reduce((prev, cur) => {
prev.set(cur.id, cur);
return prev;
}, new Map());
list.value.forEach(({ condition, actions }) => {
actions.forEach((ac) => {
const item = map.get(ac);
item.show = computed(condition);
});
});
初始化时显示哪些表单项?
由condition-action 数组可以发现,数组每项的 action 中的编号对应的表单项,都是满足一定条件才显示的,换句话说,不是任何时候都显示。那么,如果一个表单项的编号不在任何一个 condition-action数组项的 action 中,是不是意味着一定显示呢?
所以,我们可以确定初始化时一定显示的表单项,思路是:
- 遍历
condition-action数组,找出action中出现过的表单项编号, - 和全量表单项作差集运算,得到全量表单项中没有在
action出现过的表单项,作为初始化显示的表单项。
示例代码
const set = new Set(list.value.map((item) => item.actions).flat());
formItems.value.forEach((item) => {
item.show = !set.has(item.id);
});
总结 我们最终的方案如下
动态表单组件新增参数:
| 参数名 | 作用 | 类型 |
|---|---|---|
| logics | condition-action 数组 | Array<{ condition: () => boolean, action: string[]}> |
动态表单 的表单项,使用 show 属性来控制是否显示。
至此,所有问题都解决了,可以开发了。
开发
动态表单组件新增参数
props:{
logics: {
type: Array,
default: () => [],
},
}
写一个函数,根据 logics 来设置表单项的 show 属性
initFn() {
const set = new Set(this.logics.map((item) => item.actions).flat());
const map = this.currentFormItems.reduce((prev, cur) => {
prev.set(cur.id, cur);
return prev;
}, new Map());
this.currentFormItems.forEach((item) => {
item.show = !set.has(item.id);
});
this.logics.forEach(({ condition, actions }) => {
actions.forEach((ac) => {
const item = map.get(ac);
item.show = computed(condition);
});
});
}
在 组件初始化和 logics 数组改变时,调用该函数。
mounted() {
this.initFn();
},
watch: {
logics() {
this.initFn();
},
},
验证
验证代码如下
<dynamic-form
:data="data"
:model="model"
:isEdit="isEdit"
:formItems="formItems"
:logics="list"
></dynamic-form>
const model = ref({
name: "1",
open: true,
sex: "man",
});
const list = ref([
{
condition: () => {
return model.value.open;
},
actions: ["sex"],
},
{
condition: () => {
return map.get("sex").show && model.value.sex === "man";
},
actions: ["select"],
},
{
condition: () => {
return map.get("sex").show && model.value.sex === "woman";
},
actions: ["checkbox"],
},
]);
效果如下
开关关闭时
开关打开时
"性别"选"女"时
逻辑正确
总结
一句话概括思路
给表单项绑定 计算属性,来动态更新表单项的展示。
优点
把表单项显示的逻辑从业务代码中抽离出来,实现思路简单,扩展性好。
局限性
由于需要使用 Vue3 的 computed 组合式 API 动态创建 计算属性,所以 动态表单组件的logics属性只能在 Vue3 下使用,Vue2 下需要在组件外使用计算属性 维护表单项。