基于Vue设计一个动态表单(二)--控制表单项的显示

612 阅读7分钟

背景

日常开发中,表单项满足一定条件才显示,也是一种常见的场景。举个例子:

有个"开关",打开时显示 "性别",关闭时不显示"性别"。

"性别" 为 "男" 时,显示 "单选框"。

"性别"为 "女" 时,显示 "多选"。

上篇博客 讲了如何设计一个动态表单,不过,对于上面这种场景,还不支持。本篇文章就专门讨论“动态显示表单项”如何设计和实现。

需求分析

由需求背景可以看出,一个表单项的值改变,可以影响多个表单项。

表单项上下游之间,可能存在级联的情况。

方案设计

我们首先思考 2 个问题:表单项如何动态显示?如何联动?

表单项如何动态显示?

一说起响应式状态的动态更新,我们首先想到 Vuecomputed 计算属性。 只要给表单项添加 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);
});

这样做可行,但是不够优雅:

  1. 当一个条件控制多个表单项时,每个表单项的 show 属性都要传入同一个 计算属性 的引用(推荐),或者把 计算属性 复制粘贴一遍(不推荐)。
  2. 不直观:不容易看出哪个条件改变后,会直接影响到哪些表单项

我们可以再优化一下,将 计算属性 抽离出 formItems 数组,单独维护表单项显示的条件。那么,如何定义表单项显示的条件?

如何定义表单项显示的条件?

我们从需求背景和需求分析中可以提取到一个逻辑关系:当 xxx 时 显示 yyy

其实就是

if(满足条件) then {显示相关表单项}

的关系。

由于条件判断逻辑是在动态表单组件外编写的,所以,我们需要将条件判断逻辑封装成一个函数,传给动态表单组件,来让动态表单组件计算出是否显示表单项:函数返回 true,就显示受影响的表单项;函数返回 false,就不显示受影响的表单项

condition: () => boolean;

当一个条件变化时,可能会影响到多个表单项,所以,我们可以用一个数组来表示受影响的表单项。

把条件记为 condition,受影响的表单项记为 action

action: Array<string>;

条件和受影响的表单项是一一对应的,所以,可以保存到同一个对象中。

{
  condition: () => boolean,
  action: Array<string>
}

一个表单中可能存在多个这样的关系,所以,可以用一个数组来存放所有关系。

[{
  condition: () => boolean,
  action: Array<string>
}]

这就是我们需要的描述表单项显示条件的数组。

conditionaction 的作用见下表

属性名作用类型
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 中,是不是意味着一定显示呢?

所以,我们可以确定初始化时一定显示的表单项,思路是:

  1. 遍历 condition-action 数组,找出 action 中出现过的表单项编号,
  2. 和全量表单项作差集运算,得到全量表单项中没有在 action 出现过的表单项,作为初始化显示的表单项。

示例代码

const set = new Set(list.value.map((item) => item.actions).flat());
formItems.value.forEach((item) => {
  item.show = !set.has(item.id);
});

总结 我们最终的方案如下

动态表单组件新增参数:

参数名作用类型
logicscondition-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"],
  },
]);

效果如下

开关关闭时

20240925_003742_image.png

开关打开时

20240925_003809_image.png

"性别"选"女"时

20240925_003836_image.png

逻辑正确

总结

一句话概括思路

给表单项绑定 计算属性,来动态更新表单项的展示。

优点

把表单项显示的逻辑从业务代码中抽离出来,实现思路简单,扩展性好。

局限性

由于需要使用 Vue3computed 组合式 API 动态创建 计算属性,所以 动态表单组件的logics属性只能在 Vue3 下使用,Vue2 下需要在组件外使用计算属性 维护表单项。