注册

🦊【低代码相关】表单联动新思路 摆脱if-else的地狱🦄


在低代码解决方案中,表单是一大类低代码搭建解决的问题。表单作为用户信息采集的手段存在于各类应用中,无论是面向C端的手机页面,还是面向B端的运营平台,甚至低代码平台本身都会存在表单这种形式的交互。


表单本身并不复杂,各个组件库,如antd,element ui等都提供了表单组件,能够将一组输入控件组织成一个表单,并且都提供了简单的校验功能,能够检查单控件类似非空、输入长度、正则匹配之类的问题,也可以针对类似多字段情形自己定制复杂校验逻辑。然而,对于表单项之间存在联动的情形,比如一些字段的出现/消失依赖于其他字段的情形,或者一些字段填写以后其他字段的选项应当变更,这些情形通用的组件库就没有提供解决方案,而是由开发各显神通了。


表单联动最简单的方式自然是if-else了,对于联动项较少的情形,简单一个if-else就能够实现我们所需要的功能。然而,在复杂的表单上if-else层层嵌套下来代码的可读性会变差,下次开发的时候看着长长的一串if-else,每个人都会超级头痛。更重要的是,采用if-else的维护方式,在表单渲染部分需要一组对应的逻辑,在表单提交校验的时候又需要一组对应的逻辑,两边的逻辑大量都是重复的,但一组是嵌套在视图里,一组是针对表单数据。


在程序语言中,解决if-else的方法是采用模式匹配,在表单联动这个主题上,这个方式也是可行的嘛?让我们就着手试试吧!


模式定义


我们的目标是尽可能多地去掉if-else。表单联动主要是基于表单的值,那模式自然是基于值来定义的。


举个🌰:假设我们需要开发一个会议预订系统,支持单次和循环会议,那么表单的模式有那几种呢?


系统最后的效果就类似Outlook:


image.png



  1. 单次会议,需要会议日期(event_date)、开始时间(event_start)、结束时间(event_end)、主题(subject)、参与者(attenders)、地址(location)



  2. 循环会议,一样需要开始时间(event_start),结束时间(event_end),主题(subject)、参与者(attenders),地址(location),还需要循环的间隔(recurrence_interval)和循环的起始(recurrence_start)、结束日期(recurrence_end)。而循环又可以分为以下几种子模式:


    1. 按日循环
    2. 按周循环,额外需要周几举行会议(recurrence_weekdays)
    3. 按月循环,额外需要几号举行会议(recurrence_date)
    4. 按年循环,额外需要几月几号举行会议(recurrence_month,recurrence_date)


这里除了地址和循环结束日期以外的所有字段都是必选的,循环的间隔需要是一个正整数。


可以看到,这里一共是5种模式。区分模式主要是两个字段——是否循环(is_recurrence)和循环单位(recurrence_unit),并且都是值的唯一匹配,因此我们可以用简单用JSON的方式定义模式:


// 单次会议
{
"is_recurrence": false
}
// 按日循环
{
"is_recurrence": true,
"recurrence_unit": "day"
}
// 按周循环
{
"is_recurrence": true,
"recurrence_unit": "week"
}
// 按月循环
{
"is_recurrence": true,
"recurrence_unit": "month"
}
// 按年循环
{
"is_recurrence": true,
"recurrence_unit": "year"
}

对于更复杂的情况来说,模式的区分可能就不是单一值匹配了。例如我们需要做一个医院急诊管理系统,需要根据用户输入的体温来获取更多信息,体温在38.5度上下需要有不同的反馈,这样的情况就没法简单用JSON来表达,而是需要使用function,但整体的逻辑是一致的,都是将可能的情况定义为模式,并将表单状态与模式相关联。


表单定义


定义完模式后我们需要定义对应的表单。


在我们的会议预订应用中,总共有以下几个字段:


  • event_date
  • event_start
  • event_end
  • subject
  • attenders
  • location
  • is_recurrence
  • recurrence_interval
  • recurrence_unit
  • recurrence_start
  • recurrence_end
  • recurrence_weekdays
  • recurrence_month
  • recurrence_date

在这个场景下,每个字段展示的内容和校验逻辑在5种模式下都是一致的,需要根据模式联动的点只在于每个字段是否展示,整个表单数据的校验逻辑其实所有展示字段的单字段校验逻辑。因此,我们将每个字段通过以下类型表示:


type FormField<T> = {
/**
* 表单展示
*/

render: (value: T | undefined) => ReactNode;
/**
* 校验规则
*/

rules: {
validates: (value: T | undefined) => boolean;
errorMessage: boolean;
}[];
};

所有字段根据字段key通过一个map进行存储与索引。同时,将每个模式下应该展示的字段以字段key的数组的方式进行存储:


/** 所有字段的存储,这里省略实现 */
declare const formFields: Record<keyof Schedule, FormField<any>>;
type Pattern = {
pattern_indicator: Partial<Schedule>;
fields: (keyof Schedule)[];
};
/** 每个模式下应该展示的字段映射 */
const patterns: Pattern[] = [
{
pattern_indicator: { is_recurrence: false },
fields: [
"event_date",
"event_start",
"event_end",
"subject",
"attenders",
"location",
"is_recurrence",
],
},
{
pattern_indicator: { is_recurrence: true, recurrence_unit: "day" },
fields: [
"event_start",
"event_end",
"subject",
"attenders",
"location",
"is_recurrence",
"recurrence_interval",
"recurrence_unit",
"recurrence_start",
"recurrence_end",
],
},
{
pattern_indicator: { is_recurrence: true, recurrence_unit: "week" },
fields: [
"event_start",
"event_end",
"subject",
"attenders",
"location",
"is_recurrence",
"recurrence_interval",
"recurrence_unit",
"recurrence_start",
"recurrence_end",
"recurrence_weekdays",
],
},
{
pattern_indicator: { is_recurrence: true, recurrence_unit: "month" },
fields: [
"event_start",
"event_end",
"subject",
"attenders",
"location",
"is_recurrence",
"recurrence_interval",
"recurrence_unit",
"recurrence_start",
"recurrence_end",
"recurrence_date",
],
},
{
pattern_indicator: { is_recurrence: true, recurrence_unit: "year" },
fields: [
"event_start",
"event_end",
"subject",
"attenders",
"location",
"is_recurrence",
"recurrence_interval",
"recurrence_unit",
"recurrence_start",
"recurrence_end",
"recurrence_month",
"recurrence_date",
],
},
];

展示逻辑


表单定义好后,具体应该如何展示呢?


对于刚好匹配上一个模式的情况,显而易见地,我们应当展示该模式应当展示的字段。


然而,也存在匹配不上任何模式的情况。比如初始状态下,所有字段都还没有值,自然就不可能匹配上任何模式;又比如is_recurrence选择了true,但其他字段都还没有填写的情况。这种情况下我们该展示哪些字段呢?


我们可以从初始状态这种情况开始考虑,初始情况是是所有情况的起始点,那么只要所有情况下都会展示的字段,那么初始情况也应该展示。然后,当用户将is_recurrence选择了true,那么单次会议这种可能性已经被排除了,还剩下4种循环的情况,这时就应该展示这四种剩余情况都展示的字段。


这样,整套展示逻辑就出来了:


const matchedPattern: Pattern = getMatchedPattern(patterns, answer);
if (matchedPattern) {
return matchedPattern.fields;
}
const possiblePatterns: Pattern[] = removeUnmatchedPatterns(patterns, answer);
return getIntersectionFields(possiblePatterns);

本文用一个简单的例子来阐释了我们通过模式匹配的方式定义表单的思路。其实,像类似决策树、有限状态机等的模型都可以用来帮助我们通过更灵活的方式来定义我们的表单联动逻辑,像formily之类的专业的表单库更是有完整的解决方案,欢迎各位读者一起提供思路哈哈。

 

0 个评论

要回复文章请先登录注册