渐进式的formItem(基于antd的Form)

6,156 阅读3分钟

在日常用antd写表单项时,大多数人是这样写的:

老实说,antd4Form要比antd3的好用很多,但在实际的业务场景中,还是会因为充斥着大量的Form.Item

拿上面的图来说,placeholderrequire都差不多,所以我们是不是能再抽象一层。

答案肯定是能的。那么方案有哪一些呢:

  1. 借助于业界的一些方案,如formilyform-render。它们是先自己实现了一套,然后去适配不同的UI,如antdfusion(飞冰的那一套),你也可以基于它提供的核心库去扩展自己的UI。
  2. 基于antd本身再去做封装一个大而全的Form方案
  3. 渐进式的用一个组件来代替Form.Item

个人不太喜欢2的方案,因为这样一来,将来如果要适配fusion,或者其他UI时,那么基本上之前的代码是废了,何必呢?

所以我个人的选择是3,它的意思是,我写一个Field组件,能和Form.Item共存,这样一来,老代码可以渐进式的修改,新代码可以直接使用。

那么问题来了,Field组件要如何封装?其实这个完全可以参考formily的API。

export interface FieldProps extends Omit<FormItemProps, 'children'> {
  name: string; // 后端的字段key
  label: ReactText;  // label
  required?: boolean; // 是否必须,没有考虑动态
  disabled?: boolean; // 禁用,同样这里也没有考虑动态的
  placeholder?: boolean;  // 如果是文本则显示'请输入xx',xx为上面的label值,不然就是请选择
  hidden?: boolean;  // 针对那种<input type="hidden" />
  description?: ReactElement; // 描述说明
  component?: 'input' | 'textarea' | 'select' | 'radio' | 'custom'; // 当然在实际业务中肯定会更多,因为是渐进式的,所以可以考虑一步步地封装
  enum?: Array<{ label: React.ReactNode; value: string | number | boolean }>; // 针对component类型为`select`和`radio`
  node?: ReactElement; // 针对component类型为`custom`
  componentProps?: { // 组件属性
    placeholder?: string;
    disabled?: boolean;
    [key: string]: any;
  };
}

其实这个Field组件不难实现,它的难点在于context这一块的值得特殊处理一下,不然你renderForm.Item是拿不到labelAlignlabelColwrapperCol这些值的。

import { FormContextProps, FormContext } from 'antd/lib/form/context';

<FormContext.Consumer key="label">
  {(contextProps: FormContextProps) => {
    // return ...
  }}
</FormContext.Consumer>

另外还有就是在实现description功能的时候,也着实难受,要Form.Item嵌套Form.Item,布局那一块有点无力吐嘈。

有了Field组件,SubmitReset组件自然呼之而出了。那么下一步,我们要考虑,配置式的FormRender组件。

所谓的配置,无非就是个json。但这里要考虑的东西有点多了:

  1. 简单地动态化要怎么实现?
  2. 如果将来想做可视化,那么方案是怎样的?

在上面设计的Field组件里面,我考虑了可视化的方案,但是它只能满足简单的场景,一旦复杂了,就gg了。那么有没有办法在它上面扩展呢,其实是可以的。加上两个字段(这个参考了之前同事写的代码):

  • shouldUpdate: Form.Item里面的shouldUpdate
  • render: 渲染函数,接收form属性,我们就可以根据不同的情况,渲染不同的组件了。

基本上是没啥问题了,但事实上,比如说实现Item组件显不显示,固然用render可以做,但更优雅的是通过一个表达式来做,譬如这样的:

参考来源是:form-render。它的是ui-hidden功能。核心代码是这一段:

// 计算单个表达式的hidden值
const calcHidden = (hiddenString, rootValue, formData) => {
  if (!rootValue || typeof rootValue !== 'object') {
    return false;
  }
  // 支持四种基本运算符
  const operators = ['==', '!=', '>', '<'];
  try {
    const op = operators.find(op => hiddenString.indexOf(op) > -1);
    const [key, value] = hiddenString.split(op).map(item => item.trim());
    let left = rootValue[key];
    // feature: 允许从 formData 取值
    if (key.substring(0, 9) === 'formData.' && formData) {
      const subKey = key.substring(9);
      left = getExpressionValue(formData, subKey);
    }
    const right = parseString(value);
    return parseString(`"${String(left)}"${op}"${String(right)}"`);
  } catch (e) {
    console.error(e);
  }
  return false;
};

// Remove all window valid api
// For safety jest-* variable will throw error
export function safeEval(code) {
  let safeContextStr = '';
  if (typeof window !== 'undefined') {
    const windowContextAttr = Object.getOwnPropertyNames(window).filter(
      isValidVariableName
    );
    for (let i = 0, len = windowContextAttr.length; i < len; i++) {
      safeContextStr += `var ${windowContextAttr[i]} = undefined;`;
    }
  }
  return Function(`${safeContextStr} "use strict";  ${code}`)();
}
// 代替eval的函数
export const parseString = string => safeEval(`return (${string})`);  

parse那一块的逻辑感觉有点粗糙,但可以用,于是乎给抄了过来。

简单做一下总结,就是我们在平常写代码的时候,还是要多思考,看看能否让代码变得更简洁,通过渐进式的方案来改革代码,而非一蹴而就。