Formily取经2.0

Formily 取经之后。

今天的 2.0 取经还会再来一些比较特别的 Formily 使用场景。随后,内容会偏向用过一段时间的 Formily 后的一些感受。

废话不多说我们下面开始!

先来继续说几个相对复杂的场景

接下来主要列举几个我在写一些复合表单组件的经历中遇到的场景。

因为最近在开发银行卡组件。

我们可以想想平时在填写银行卡的时候,其实并非仅填写卡号,还会填写很多银行卡相关的内容。

这就是一个很典型的对内是一整套表单,对外是表单中一项的场景。

在上一节复合字段中介绍了这样的场景。

父子表单联动校验

上一节描述的有些粗糙,其实复合表单的难点,就是两个表单如何建立关系,校验联动起来。

因为这次银行卡的开发过程涉及到了替换,而项目中还存在远古的 antd-form,这篇就把父表单是 antd 和 formily 的情况都汇总一下。

表单联动的难点就是父表单校验的时候,如何触发子表单的校验。

父表单是 antd

最佳实现方案肯定是清一色 antd 或者 formily。下文所阐述的方案仅作为一种 hack 方法来学习,不推荐真实项目中使用。

父表单是 antd,子表单又是 formily,相信大家都可能会遇到这样的情况。

我们需要做的,就是在 antd 的 form 校验时出发 formily 的表单校验。


// ...

const formRef = useRef<FormilyForm<any>>();

const getSubFormRef = useCallback((ref: FormilyForm<any>) => {
  formRef.current = ref;
}, []);

const rules = [
  ...rules,
  {
    validator: async (_: any, __: any, callback: any) => {
      const subForm = formRef?.current;
      if (subForm) {
        try {
          // 通过ref来操作内部的子表单
          await subForm.validate();
          return callback();
        } catch (error) {
          return callback(error?.[0].messages?.[0] || "该字段是必填字段");
        }
      }
      return callback();
    },
  },
];

// ...

可以看到方法简单粗暴,就是用 ref 来操作内部的子表单。

父子表单都是 formily

父子表单都是 formily 就会简单很多。


// ...

// useField可以获取到当前子表单在父表单中所代表的那一个field
const field = useField<ObjectField>();

// 可以通过field拿到外部的form,然后通过添加effect来完成校验联动
field?.form.addEffects('subFormValidate' + field.address, () => {
  onFieldValidateStart(field.address, async () => {
    if (field.required && !disabled) {
      if (!combinedField || !combinedField.length) {
        field.selfErrors = ['该字段是必填字段'];
        return;
      }
      try {
        await bankForm.validate();
        field.selfErrors = [];
      } catch (err) {
        field.form.clearErrors(field.address);
        field.selfErrors = err[0].messages;
      }
    }
  });
});

// ...

主动设置报错

有的时候我们会遇到一种场景:

就是在完成一些表单联动后,还要校验表单里某一项的值,如果值不符合要求,是需要报错的。

这个时候就会涉及到我们主动将某一个子表单内的 field 置为报错状态。

field.setSelfWarnings([yourErrors]);

其实本身就需要一个setSelfWarnings这个 API。但是有一些坑需要强调一下:

// 主要写银行卡组件的主动设置错误

validator 妙用

还学到了一些 validator 的巧妙用法。


const schema = {
  // ...
  'x-reactions': [
    {
      dependencies: ['.' + SYSTEM_SUB_FIELD_NAME.ID_TYPE],
      when: '{{ idTypeValidate($deps[0]) }}',
      fulfill: {
        schema: {
          'x-validator': [
            {
              required,
            },
            (val: string) => {
              return idCardValidate(val);
            },
          ],
        },
      },
      otherwise: {
        schema: {
          'x-validator': [
            { required },
            {
              pattern: /^[A-Za-z0-9]+$/,
              message: '仅支持数字或英文',
            },
          ],
        },
      },
    },
  ],
}

// ...
return (
  <SchemaField
    schema={schema}
    scope={{
      idTypeValidate: (idType: ID_TYPE_OPTIONS) => {
        return idType && idType?.id === ID_CARD_ID;
      },
    }}
  />
)

可以通过配置x-reaction内容来实现validator带有数据的联动方式。

Formily 带来的一些思考

使用了一阵子Formily以后,自己产生了一些思考。无所谓对错,就是在开发过程中一些感想的提炼。

需要相对庞大的准备工作

Formily在接触前,听到的更多是一个词入门难

准备工作可能第一个要准备的就是需要学习很多的Formily的基础知识,包括用法、API、每个库里都有些什么。

在此期间最直观的一个感受就是文档有点乱。很多东西分布在了不同的文档里formily/coreformily/react等等。

但这个其实不是今天主要讨论的"准备工作"

想用好 Formily 需要有一整套的组件去支撑

Formily为我们带来了复杂表单场景的解决方案。但是在我们开始这场表单构建之旅之前,却需要准备好很多的工具,其中最重要的一个,就是构成表单的每一项要用到的组件

FormItem到具体的应用组件我们都需要一套完整的支撑。

Formily对组件是有一些要求的,因为要按照Formily的方式去工作,组件就需要支持某些特定的参数

比如: onChange \ value \ error \ disabled \ placeholder \ required 等等…

这也就走到了我们本身可能已经有的组件库里面的组件并不能直接被FormItem包裹使用在Formily中,而需要先把这些组件加工成Formily组件才可以。

export default connect(
  YourComponent,
  mapProps((props) => {
    return {
      ...props,
    };
  }),
  mapReadPretty(PreviewText.YourComponent)
);

简单的我们可能像上面这样,直接connect一下即可,复杂的,还需要完成很多字段的映射之类的操作。

每个组件都必须拥有其对应的预览态组件

小插曲

这里讲一个小插曲,就是最初在不了解Formily的时候,自己封装了一个异步获取接口的Select组件。

如果单拿出来使用这个组件,没有任何问题。但是因为Formily的组件要支持很多参数的注入,还需要有预览态。

而接口内置后,预览态仅能拿到的value就无法匹配对应的label,如果需要label,就得在预览态组件中重新调用接口。这样就不好了。

可以看出,如果最开始知道预览态 & 其工作原理,在封装组件之初就会考虑这些,也正是因为没有入门及格就去封装Formily组件,造成了不必要的无用功。

不要让规范变成束缚

// 这里要写 Formily 组件字段的统一性(举例 required 这类字段 / FormItem 这类基础的表单组件)

上一节说到了Formily有很多自己的"规矩",我们在封装组件的时候就需要遵守。

假设现在有一个场景,就是PC / mobile都有一套Formily组件,但是封装的人不是一个人,随之而来就可能存在一起潜在的风险。


// PC
export const FormSelect = connect(
  (props: connectSelectProps) => {
    let param: SelectProps = useSelectProps(props);
    return <Select {...param} />;
  },
  mapProps(
    {
      dataSource: 'options',
    },
    (props) => {
      return {
        ...props,
      };
    }
  ),
  mapReadPretty(PreviewText.Select)
);

// mobile
const FormSelect = connect(
  (props: FormSelectProps) => {
    const selectProps: ComponentProps<typeof SugarSelect> = useSelectProps(props);
    return <SugarSelect {...selectProps} />;
  },
  mapProps((props, field) => {
    if (isVoidField(field)) {
      return props;
    }

    /** Form 默认的 Select 大小为 LG */
    const size = props.size ?? SIZE.LG;

    const isRequired = field?.required;

    return {
      ...props,
      size,
      // 这是一个mobile端的独有交互规则,必选的表单项不能有清空按钮
      clearable: isRequired,
    };
  }),
  mapReadPretty(Preview.Select)
);

我模拟了上述的情况,PC / mobile都有FormSelect(Formily包裹的Select组件)。但是能看出,mobile的FormSelect在字段映射方面做了一些操作。

这就会导致PC 和 mobile同样的组件不同的用法。

Formily的条条框框是为了后续复杂场景使用时候更顺畅方便。但是在一开始构建Formily基础相关的东西时,就出现了分歧,后续肯定会影响到使用的效率。

类似的问题可能还有一些。

总结

这一篇可能感想的占比更大些,技术干货少一些。但是正好能经历Formily构建的初期,感受一下很多基础设施在搭建过程中遇到的问题也很有收获。

Formily的内容就暂时先告一段落了,后续如果有别的高端操作和复杂场景,再补充~

参考