细节控 - 复杂场景中的健壮的表单引擎,居然还要考虑这些?
“属性”还是“状态”
visible
和touched
分别该归类为“属性”还是“状态”?
我们先确定“属性”和“状态”的含义。“属性”和“状态”的区别在于以下几点:
- “属性”是来自外部的控制,组件自身无法改变;“状态”是来自内部的变化,外部不应该能够修改;
- “属性”是用来控制那个组件的工作方式;“状态”是用来保存交互时的动态数据;
- “属性”是相对确定的,父组件的属性不变,子组件的属性也不会变;“状态”会通过组件自身的逻辑随时变换。
我们来看visible
(是否显示)和touched
(用户是否点击过),这两个我们就容易区分了,visible
属于属性,touched
属于状态。
“属性”和“状态”并不是绝对的,这里visible
相对于touched
更像是“属性”,而某种程度visible
也是动态的,下面会细讲。
Form Array表单列表
我们举一个简单的例子,假设有两个input:fName
用来输入名字,lName
用来输入姓氏,那么对这个form,我们可以用以下JSON Schema来定义:
{ // <--- 用key-value表示,而不是array
fName: {
name: "fName",
label: "First Name",
type: "text",
...
},
lName: {
name: "fName",
label: "First Name",
type: "text",
...
}
}
随着用户的输入,我们的框架可以自动获取整个form值,比如:{ fName: "Jenny", lName: "Han" }
。
这里可能会有人有疑问为什么定义form不是用的array,而是用key-value对,一个是因为form值本身就是key-value的,所以这里用key-value来定义让格式更一致;更重要的原因是要考虑到有一种并列数据输入的情况,比如说这个表单是个可以添加多个不同人名的表单,于是会有很多个fName
和lName
,每对fName
和lName
是并列的,用户可以输入一对,也可以输入多对,这种情况,我们再用array来表示。
available
回到正题,控制类属性中,假如此时我将fName
设置为{hidden: true}
,就意味着fName
这个input看不见了,但实际上这个form值还是{ fName: "Jenny", lName: "Han" }
,因为hidden
只控制着组件的显示或隐藏,并不会让值有变化。可是在实际使用场景中,当一个组件隐藏了,大多数时候是需要将那个字段踢出去的,这里是很多低代码表单设计器没有考虑到的地方,因此,我引入了available
这个属性。 当available = true
时,字段是可以包括在内的,而当available = false
时,字段是会被踢出去的,当然也是不可见的。这样当fName
的设置为{available: false}
时,这个form的值会自动变为{ lName: "Han" }
,这样让前端的值变得更准确和自动化。
注意:这里不要用valid
或invalid
这类词汇,因为这些在校验类状态中会用到。
语义方向一致
- 对于显隐,可以用
hidden
,也可以用visible
; - 对于必填还是选填,可以用
mandatory
或required
,也可以用optional
; - 对于可用还是禁用,可以用
disabled
,也可以用enabled
。
这些语义不统一的话,用起来总要考虑是否要取非,很容易弄错。尤其是在还有嵌套表单时,父表单disabled
,子表单里的某个组件enabled
,这类计算稍不注意就会出bug。
因此这里我将语义全部统一为正向的,即:visible
,required
,enabled
,available
。
动态化
由于表单联动是很常见的,比如这里当fName = "Jenny"
时,lName
才显示,否则不显示,这种情况我们需要向用户开放一个属性visibleOn
,让用户来定义什么时候visible = true
的,其他时候visible = false
。对应的还有requiredOn
, enabledOn
, availableOn
。
要注意的是这些属性是开放给用户的,而在表单中,我们还需要一些逻辑来计算出boolean值。也就是动态地,通过visibleOn
计算当前visible
值,通过requiredOn
计算当前required
值,通过enabledOn
计算当前enabled
值,通过availableOn
计算当前available
值,再将这些boolean属性传给对应的子组件。
上面说过“属性”和“状态”并不是绝对的,这里便可以看出,visible
和visibleOn
一起看,visible
更像是状态,而visibleOn
更像是属性。
为什么disable了?
当动态表单触发到某些条件,这个field就被禁用了,这种场景我遇到过用户抱怨不知道为什么就不能用了。
所以为了更好的用户体验,这里准备一个disableReason
,用来存放禁用原因,告诉用户为什么不能用,这个原因也是动态的。
这个功能不是form组件专属的,事实上,UI上任何组件被禁用了,都可能需要disableReason
,比如一个按钮被禁用了,hover上去会有个气泡框提示用户为什么不能点。所以这个特性和disabled/enabled
属性是相伴的,任何能被禁用的组件都可以带上disableReason
,至于如何动态赋值,以后我们再具体探讨。
required和校验类属性
当一个field的required = true
,就意味着这是个必填项,如果用户没有填,在提交的时候会报错,比如红色框出来,旁边有行红色小字:“请输入...”,而这个错误提示是校验类属性来控制的,刚刚我说了valid
和inValid
,此时,form的校验状态就是invalid
,并且我们还需要动态存储错误信息,包括有哪些field,是哪些错误,这里的错误可以这样表示:
// Field Error
{
type: 'required',
label: '...' // <=== 也可以省略这个字段,所有错误为'required'的都统一为: "xxx为必填字段。"
}
校验功能这里不再展开了,这一点说的是required = true
是会给这个field自带一个“必填校验器的”。
封装
我们在封装单个组件时,react中可以写成这样:
const FreeField = ({ visibleOn, requiredOn, enabledOn, availableOn }) => {
return (
<ControllingField
visibleOn={visibleOn}
requiredOn={requiredOn}
enabledOn={enabledOn}
availableOn={availableOn}
>
{({ visible, required, enabled, available }) => {
return (
<Text
visible={visible}
required={required}
enabled={enabled}
available={available}
...
/>
);
}}
</ControllingField>
);
}
接下来我们的工作重心就在于ControllingField
中将xxxOn属性计算成boolean属性了。
总结
控制类属性归纳:
- 显隐
visibleOn
->visible
- 可用/禁用
enabledOn
->enabled
enabledOn
->disableReason
- 是否是有效字段
availableOn
->available
- 必填/选填
requiredOn
->required
->validator
(校验器)
转载自:https://juejin.cn/post/7345108417113178153