虽然之前对业务框架的常用组件做了一些介绍,也在实践中与大家沟通过组件的使用方法,但还没有体系化地介绍常见的使用场景,这样就难免会出现一些误用,也对后期的维护工作带来了不少困难。

默认值

默认值就是一个很简单的值,它不能依赖formModel相关的值。

// 简单值
{
  defaultValue: 1
}

// 来源于一个其他的变量,可以是普通变量,也可以是 ref 变量。
const a = 1;
{
  defaultValue: a
}
// 或者
const a = ref(1);
{
  defaultValue: a.value
}

// 不可以依赖formModel相关的值
{
  defaultValue: proFormRef.value.formModel.b
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

总之,就是简单点,别搞花里胡哨的。

这个默认值也将作为dataIndex所关联的字段的初始值。

表单回写

表单一旦初始化完毕后,它可以自行运转,不需要人为干预。

但是有时候,我们会在一些操作后,希望改变某个表单控件的值,这个时候,需要用到 writeBackForm 来控制,不要尝试用其他自己想出来的方法去更新表单,不一定产生合理的效果。

// 可以实现效果,但不是很建议
proFormRef.value.formModel.someField = 1;
1
2

writeBackForm 可以一开始就有值,也可以在异步后得到值。可以是ref,也可以是reactive

// 使用 ref
const writeBackForm = ref();
// 异步后整体赋值
writeBackForm.value = res.result

// 使用 reactive
const writeBackForm = reactive({});
// 异步后更新
// 只更新一个字段
writeBackForm.a = res.result.a
// 批量更新
Object.assign(writeBackForm, res.result)
1
2
3
4
5
6
7
8
9
10
11
12

通过 writeBackForm 回写表单,能保证正确地处理一些特殊值,比如范围值。

对于日期范围,我们从 detail 接口拿到的字段可能是 startTime, endTime,而实际上 daterange 组件需要的是一个数组值,可理解为[startTime, endTime]

借助 writeBackForm,我们不需要特意处理范围数据,因为组件内部可以通过transformPropsForRange自动检测和更新数据,保证数据和 UI 的一致性。

你还可以通过setField(field, value)或者setFields(model)进行单一或者批量的表单更新,但是它不会特意去处理特殊类型的表单项,比如前面提到的日期范围。

表单之外...

实际业务中,除了表单 UI 上体现的字段,还有一些字段我们也需要处理,最后传给后端。

对于这些,我们分两个视角来看。

  1. 固定不变,雷打不动的,见fixedForm
  2. 可能有逻辑,随逻辑变化的,见beforeSearch

fixedForm

对于一些确定下来的参数,并且又不需要展示在表单上的,我们可以通过 fixedForm指定。

fixedForm一定是简单的,它就像默认值一般,是可以直接确定下来的。如果你写的 fixedForm很复杂,有很多依赖,那你可能要思考一下了。

比如后端固定要求,type传 5,xxx传 6。不用想了,放在fixedForm很合适。

beforeSearch

对于一些存在逻辑关系的数据,我们需要用到beforeSearch来处理,最后返回一个完整的对象,交由page接口使用。

比如行政区域组件,组件绑定的值是一个数组,然而后端可能是需要provinceIdcityId之类的字段。这个时候放在 beforeSearch是最合适的。

const beforeSearch = (formModel) => {
  return {
    ...formModel,
    provinceId: formModel.cantonIds[0],
    cityId: formModel.cantonIds[1]
  }
}
1
2
3
4
5
6
7

很多人会习惯,在onChange马上把数据处理好。实际上,onChange可能会发生很多次,是不是每次onChange后,你都需要得到处理完后的数据呢?大部分情况下,不需要。

你需要这些处理完的数据的时机,往往都是在提交表单,或者提交查询条件前。所以,只需要在提交之前通过beforeSearch处理即可。

提前处理数据还会让你的代码定义很多不必要的变量,维护起来非常麻烦。

总之,让表单自己运转吧,不要去干涉它。你想要结果的时候,再去问它要。

表单提交

正如表格场景的beforeSearch所述,表单ProForm也可能在提交前需要处理数据,这个可以通在onSubmit中处理,算是比较常见的。

const onSubmit = async (formModel) => {
  const params = {
  	...formModel,
    otherField: formModel.a + formModel.b
  }
  await xxxService.yyyMethod(params);
}
1
2
3
4
5
6
7

自定义渲染

渲染的范围

首先,使用customRenderFormItem可以自定义渲染某个表单项,配合renderFormItemLevel可以实现不同颗粒度的渲染。 renderFormItemLevel有两个备选值,分别是formItemwidget,默认值是widget。两者的渲染范围不同,具体理解见下面图示。

渲染的内容

自定义渲染的意思就是不限制你要画什么组件,按钮,输入框,div,随你便。

但是如果你要绘制与表单数据有关的东西,比如由两个输入框组成的一个范围输入。从数据层面看,实际上它应该是formModel的一部分,只不过是你自己自定义渲染的,所以它的输入(修改)依然应该反馈到formModel上,那么怎么做呢?

有人可能会想到自己定义两个 ref ,配合 onChange 存一下这两个输入框的值。最后提交表单的时候再把这两个值传过去。

方式没有问题,但不够优雅。实际上,组件设计已经考虑了这个问题,我们可以通过customRenderFormItem拿到formModel对象,对formModel直接进行操作。我这里直接拿示例组件说一下,一个输入框和两个输入框本质上没有区别。同理,下拉框,其他的都是一个道理,理论上不必定义额外的变量来临时存储(很复杂的除外)。

这种做法有点类似于 React 的受控组件/非受控组件。受控的意思是框架内部已经完成了数据逻辑(绑定值和更新值)的流转。非受控的意思是你放弃了框架内部的逻辑,准备自己动手接管,这个时候你就要自行完成绑定值和更新值的操作。

或者说跟驾车也类似,自定义渲染的默认行为相当于司机已经松开了方向盘,如果你不接管,车开到哪里就不好说了(也就是:这个数据和表单不一定是正确的),所以你必须接管!

怎么取表单对象?