在日常用antd
写表单项时,大多数人是这样写的:
老实说,antd4
的Form
要比antd3
的好用很多,但在实际的业务场景中,还是会因为充斥着大量的Form.Item
。
拿上面的图来说,placeholder
和require
都差不多,所以我们是不是能再抽象一层。
答案肯定是能的。那么方案有哪一些呢:
- 借助于业界的一些方案,如formily、form-render。它们是先自己实现了一套,然后去适配不同的UI,如
antd
、fusion
(飞冰的那一套),你也可以基于它提供的核心库去扩展自己的UI。 - 基于
antd
本身再去做封装一个大而全的Form方案 - 渐进式的用一个组件来代替
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
这一块的值得特殊处理一下,不然你render
的Form.Item
是拿不到labelAlign
、labelCol
、wrapperCol
这些值的。
import { FormContextProps, FormContext } from 'antd/lib/form/context';
<FormContext.Consumer key="label">
{(contextProps: FormContextProps) => {
// return ...
}}
</FormContext.Consumer>
另外还有就是在实现description功能的时候,也着实难受,要Form.Item
嵌套Form.Item
,布局那一块有点无力吐嘈。
有了Field
组件,Submit
和Reset
组件自然呼之而出了。那么下一步,我们要考虑,配置式的FormRender
组件。
所谓的配置,无非就是个json
。但这里要考虑的东西有点多了:
- 简单地动态化要怎么实现?
- 如果将来想做可视化,那么方案是怎样的?
在上面设计的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
那一块的逻辑感觉有点粗糙,但可以用,于是乎给抄了过来。
简单做一下总结,就是我们在平常写代码的时候,还是要多思考,看看能否让代码变得更简洁,通过渐进式的方案来改革代码,而非一蹴而就。