New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Antd 4.0, Form.Item 使用 normalize 和 getValueFromEvent 转换控件的赋值取值方式, onFinish 返回的值依旧是转换前的值 #19727
Comments
v3 Demo |
Any conclusion? |
Maybe we can add |
👌 Maybe |
最近需求刚遇到这种,确实是需要的,但是有点实现难度 比如接口给我一个是 Number |
现在用了一种比较脏的写法 自行封装一个 function BooleanSwitch({ value, onChange }) {
return <Switch value={Boolean(value)} onChange={checked => onChange(~~checked)} />
} |
@shaodahong, 来个 PR? |
@VoliBearCat 好思路,我准备加个 pipleline,但是现在有个问题就是首次 initialValue 的时候很难一次完美的走过 pipleline,再想想... export type Pipleline = (
value: Store,
) => StoreValue | [(value: Store) => StoreValue, (value: Store) => StoreValue];
/**
* call when initialValue or onFinish
* 1. if type eaqul pipleline only called when onFinish
* 2. if type eaqul [pipleline, pipleline]
* the first pipleline called when initialValue, last pipleline called when onFinish
*/
pipleline?: Pipleline; 使用 <Form
{...layout}
name="basic"
initialValues={{ isCheck: 1 }}
onFinish={onFinish}
onFinishFailed={onFinishFailed}
>
<Form.Item
label="Username"
name="isCheck"
pipleline={[value => Boolean(value), value => ~~value]}
>
<Switch />
</Form.Item>
<Form.Item
label="Username"
name="isCheck"
pipleline={value => value.format('YYYY-MM-DD HH:mm:ss')}
>
<DataPicker />
</Form.Item>
</Form>; 这种数据层的处理和公司的接口特性有关系,都可以抽出来 Boolean(value)
~~value
value.format('YYYY-MM-DD HH:mm:ss') |
最好还是在 |
normalize 时机不对, normalize 每次 change trigger 的, 而且 normalize 是处理给控件的,其实就是处理控件的值到 storeValue 的 |
和 normalize 概念还是有区别的,normalize 在我理解是 value 每次改变处理 change 的 value,可以 filter 或者 newValue,pipleline 只会在初始化和 onFinish 的时候触发,其他的时候没有必要,浪费性能 |
Ref: #11935 |
如果如此,是否还是应该让用户直接在 onFinish 里自行转换一下呢?比较担心不和 |
把value分为viewValue和modelValue,前者用于视图控件,后者即是模型值。两者可以通过parser和formatter来互相转换。对于双向绑定表单,无非就是从视图收集值转换成模型值,或者直接更新模型值后再转换成视图值传给控件。来自angular 1.x的ng-model做法。新版本ng是不是还是这样不清楚了 |
这个和 ng-model 或者 v-model 的概念区别还是挺大的,我们要做的只是一次 before transform 和 finish transform,值在组件中怎么转换我们不关心,现在遇到的问题和 #21816 (comment) 一样,核心点在于内聚可复用 |
我自己封装了个react-formutil,你可以看下,就是我说的这个思路。本质上,如果你的Field state里只有一个value,是无法解决视图值和表单值的转换的,或者导致重复转换。我说的modelValue就是指表单最终在onFinish中传递的。 例如 同理,如果用户主动调用setFieldValue,那么就可以认为用户设置了modelValue,那么就经过formatter转换成viewValue同样保存在state。 当然,以上是基于Field状态实时同步给Form的表单模型,所以要实时parser。但是antd4.0如果只是onFinish或者getFormValues等主动调用时才会被动同步Form,当然完全可以在这些实际再调用parser。 |
我这边处理的formatter和parser逻辑:https://github.com/qiqiboy/react-formutil/blob/master/src/fieldHelper.js#L394-L440 |
根据文档中关于
首先看现在 public getControlled = (childProps: ChildProps = {}) => {
const { trigger, validateTrigger, getValueFromEvent, normalize, valuePropName } = this.props;
const namePath = this.getNamePath();
const { getInternalHooks, getFieldsValue }: InternalFormInstance = this.context;
const { dispatch } = getInternalHooks(HOOK_MARK);
const value = this.getValue();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const originTriggerFunc: any = childProps[trigger];
const control = {
...childProps,
[valuePropName]: value,
};
// Add trigger
control[trigger] = (...args: EventArgs) => {
// Mark as touched
this.touched = true;
let newValue: StoreValue;
if (getValueFromEvent) {
newValue = getValueFromEvent(...args);
} else {
newValue = defaultGetValueFromEvent(valuePropName, ...args);
}
if (normalize) {
newValue = normalize(newValue, value, getFieldsValue(true));
}
dispatch({
type: 'updateValue',
namePath,
value: newValue,
});
if (originTriggerFunc) {
originTriggerFunc(...args);
}
};
// Add validateTrigger
const validateTriggerList: string[] = toArray(validateTrigger || []);
validateTriggerList.forEach((triggerName: string) => {
// Wrap additional function of component, so that we can get latest value from store
const originTrigger = control[triggerName];
control[triggerName] = (...args: EventArgs) => {
if (originTrigger) {
originTrigger(...args);
}
// Always use latest rules
const { rules } = this.props;
if (rules && rules.length) {
// We dispatch validate to root,
// since it will update related data with other field with same name
dispatch({
type: 'validateField',
namePath,
triggerName,
});
}
};
});
return control;
}; 根据语义, let newValue: StoreValue;
if (getValueFromEvent) {
newValue = getValueFromEvent(...args);
} else {
newValue = defaultGetValueFromEvent(valuePropName, ...args);
}
if (normalize) {
newValue = normalize(newValue, value, getFieldsValue(true));
}
dispatch({
type: 'updateValue',
namePath,
value: newValue,
}); 如果我们将 public getControlled = (childProps: ChildProps = {}) => {
const { trigger, validateTrigger, getValueFromEvent, normalize, valuePropName } = this.props;
const namePath = this.getNamePath();
const { getInternalHooks, getFieldsValue }: InternalFormInstance = this.context;
const { dispatch } = getInternalHooks(HOOK_MARK);
- const value = this.getValue();
+ let value = this.getValue();
+ if (normalize) {
+ value = normalize(value, getFieldsValue(true));
+ }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const originTriggerFunc: any = childProps[trigger];
const control = {
...childProps,
[valuePropName]: value,
};
// Add trigger
control[trigger] = (...args: EventArgs) => {
// Mark as touched
this.touched = true;
let newValue: StoreValue;
if (getValueFromEvent) {
newValue = getValueFromEvent(...args);
} else {
newValue = defaultGetValueFromEvent(valuePropName, ...args);
}
- if (normalize) {
- newValue = normalize(newValue, value, getFieldsValue(true));
- }
dispatch({
type: 'updateValue',
namePath,
value: newValue,
});
if (originTriggerFunc) {
originTriggerFunc(...args);
}
};
// Add validateTrigger
const validateTriggerList: string[] = toArray(validateTrigger || []);
validateTriggerList.forEach((triggerName: string) => {
// Wrap additional function of component, so that we can get latest value from store
const originTrigger = control[triggerName];
control[triggerName] = (...args: EventArgs) => {
if (originTrigger) {
originTrigger(...args);
}
// Always use latest rules
const { rules } = this.props;
if (rules && rules.length) {
// We dispatch validate to root,
// since it will update related data with other field with same name
dispatch({
type: 'validateField',
namePath,
triggerName,
});
}
};
});
return control;
}; |
个人觉得本质上还是要区分开从视图更新(onChange)和从模型更新(setFieldValue),并分开存储两个值。用户想获取和操作的永远是模型值。 |
@qiqiboy |
我纠结了这么久,感觉 现在处理每次单独转换是极其难受的,每个表单都有此类需求。 @kagawagao @qiqiboy 的提议我是非常认同的。 @zombieJ 想了解下 |
Form 组件在实际的业务中很少会直接写成 <Form>
<Form.Item><Input /></Form.Item>
<Form.Item><Input /></Form.Item>
</Form> 可以简单封装下 function MyForm({ columns, isViewMode /** 展示模式 */, onFinish, ...props }) {
const form = useForm();
return (
<Form
{...props}
form={form}
onFinish={values =>
onFinish(
lodash.mapValues(
values,
(value, key) => columns[key].outputer(value) || value
)
)
}
>
{columns.map(
({
isFormViewMode = isViewMode || false,
name,
render = () => <Input />,
...formProps
}) => (
<Form.Item name={name} {...formProps}>
{isFormViewMode ? form.getFieldValue(name) || "-" : render()}
</Form.Item>
)
)}
</Form>
);
} 实际使用的时候 <MyForm
onFinish={values => http.post("url.com", values)}
columns={[
{ name: "name" },
{ name: "age" },
{
name: "birthday",
render: () => <DatePicker />,
outputer: value => value?.toISOStrng()
}
]}
isViewMode={isView /** 受控 */}
/> 不建议硬着头皮自己在
PS: |
@shaodahong 非常感谢,目前还是先封装吧,硬着头皮太不优雅了 |
准备把 v3 的 |
对于自定义上传组件,需要监听上传状态,如 #2423 所描述,需要 |
@shaodahong 这个封装思路不错,可以解决简单的Form表单场景,实际的业务要复杂的多,应对Form表单各种嵌套和布局,这个封装就用不了了,最好的解决方案还是,对应的每个原子表单都可以单独在使用时提供转换的方法,api希望可以更语义化一些,比如 |
Reproduction link
Steps to reproduce
1.选择日期
2.提交表单
3.观察控制台日志
What is expected?
以DatePicker为例, 我想要让表单字段以字符串的形式对组件进行控制, 而不是moment对象, 目的是减少表单数据初始化及数据提交时需要额外对数据类型的转换;
1.onFinish事件, 返回的date字段值的是getValueFromEvent返回的值
2.initialValues中的date期望可以先通过normalize转换字段值再给控件
What is actually happening?
1.onFinish提交时, 返回值依旧是moment对象;
2.initialValues以字符串的格式给DatePicker的表单字段初始化时报类型错误
The text was updated successfully, but these errors were encountered: