Skip to content
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

Closed
1 task
VoliBearCat opened this issue Nov 14, 2019 · 28 comments · Fixed by react-component/field-form#110

Comments

@VoliBearCat
Copy link
Contributor

  • I have searched the issues of this repository and believe that this is not a duplicate.

Reproduction link

Edit on CodeSandbox

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的表单字段初始化时报类型错误

Environment Info
antd 4.0.0-alpha.8
React 16.11.0
System macOS 10.14.2
Browser Chrom 78
@VoliBearCat VoliBearCat changed the title Antd 4.0, Form.Item 使用 normalize 和 getValueFromEvent 转换控件的赋值取值方式, onFinish 返回的值依旧是转换前的方式 Antd 4.0, Form.Item 使用 normalize 和 getValueFromEvent 转换控件的赋值取值方式, onFinish 返回的值依旧是转换前的值 Nov 14, 2019
@shaodahong shaodahong added the 4.x In Ant Design 4.0 label Nov 14, 2019
@zombieJ
Copy link
Member

zombieJ commented Dec 12, 2019

normalize 只负责将值转换给受控组件,本身不参与最终值的转化。

v3 Demo

https://codesandbox.io/s/flamboyant-dijkstra-olob1

@afc163
Copy link
Member

afc163 commented Jan 3, 2020

Any conclusion?

@zombieJ
Copy link
Member

zombieJ commented Jan 3, 2020

Maybe we can add normalizeValue={true} to auto transfer value when normalize is set?

@afc163
Copy link
Member

afc163 commented Jan 3, 2020

👌 Maybe

@shaodahong
Copy link
Member

最近需求刚遇到这种,确实是需要的,但是有点实现难度

比如接口给我一个是 Number 1 | 0 的字段,控件的展现是 Switch 或者 CheckBox 这种,接受的是 Boolean,那么我需要在展现的时候转成 Boolean,Submit 的时候转成 Number

@shaodahong
Copy link
Member

现在用了一种比较脏的写法

自行封装一个

function BooleanSwitch({ value, onChange }) {
  return <Switch value={Boolean(value)} onChange={checked => onChange(~~checked)} />
}

@zombieJ
Copy link
Member

zombieJ commented Jan 13, 2020

@shaodahong, 来个 PR?

@VoliBearCat
Copy link
Contributor Author

我基于antd3.0版本封装了一个表单工具, 里面有两个方案解决这个问题

  1. 我新增了attach属性, 支持把表单值在onFinish时转换为自定义的格式, 优点是, 支持一个组件输出多个结果值
    image
    image
    image

2.新增了parse和format属性, 和normalize与getValueFromEvent相似, 补充支持了在初始化和提交时也能够转换
image

4.0我也尝试封装了这样的表单工具, 但是还没有用于实际项目

@shaodahong
Copy link
Member

shaodahong commented Jan 14, 2020

@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')

@zombieJ
Copy link
Member

zombieJ commented Jan 14, 2020

最好还是在 normalize 上拓展,这里其实只要处理提交的 value 和 normalize 一样会转换一下。

@shaodahong
Copy link
Member

normalize 时机不对, normalize 每次 change trigger 的, 而且 normalize 是处理给控件的,其实就是处理控件的值到 storeValue 的

@shaodahong
Copy link
Member

和 normalize 概念还是有区别的,normalize 在我理解是 value 每次改变处理 change 的 value,可以 filter 或者 newValue,pipleline 只会在初始化和 onFinish 的时候触发,其他的时候没有必要,浪费性能

@shaodahong
Copy link
Member

Ref: #11935

@zombieJ
Copy link
Member

zombieJ commented Jan 14, 2020

如果如此,是否还是应该让用户直接在 onFinish 里自行转换一下呢?比较担心不和 normalize 配套的话,用户直接设置一个转换后的值会出问题。

@qiqiboy
Copy link
Contributor

qiqiboy commented Mar 9, 2020

把value分为viewValue和modelValue,前者用于视图控件,后者即是模型值。两者可以通过parser和formatter来互相转换。对于双向绑定表单,无非就是从视图收集值转换成模型值,或者直接更新模型值后再转换成视图值传给控件。来自angular 1.x的ng-model做法。新版本ng是不是还是这样不清楚了

@shaodahong
Copy link
Member

把value分为viewValue和modelValue,前者用于视图控件,后者即是模型值。两者可以通过parser和formatter来互相转换。对于双向绑定表单,无非就是从视图收集值转换成模型值,或者直接更新模型值后再转换成视图值传给控件。来自angular 1.x的ng-model做法。新版本ng是不是还是这样不清楚了

这个和 ng-model 或者 v-model 的概念区别还是挺大的,我们要做的只是一次 before transform 和 finish transform,值在组件中怎么转换我们不关心,现在遇到的问题和 #21816 (comment) 一样,核心点在于内聚可复用

@qiqiboy
Copy link
Contributor

qiqiboy commented Mar 10, 2020

我自己封装了个react-formutil,你可以看下,就是我说的这个思路。本质上,如果你的Field state里只有一个value,是无法解决视图值和表单值的转换的,或者导致重复转换。我说的modelValue就是指表单最终在onFinish中传递的。

例如 <DatePicker /> 本身处理的moment对象,但是表单最终提供的往往是格式化后的时间字符串,Form.Item提供parser的话,DatePicker的onChange把原始的moment对象存入state.viewValue,经过parser转换的值放入state.value或者state.modelVaue。渲染视图时,传递viewValue给DatePicker即可,无需再次转换。需要获取表单值时直接取出modelValue即可。

同理,如果用户主动调用setFieldValue,那么就可以认为用户设置了modelValue,那么就经过formatter转换成viewValue同样保存在state。

当然,以上是基于Field状态实时同步给Form的表单模型,所以要实时parser。但是antd4.0如果只是onFinish或者getFormValues等主动调用时才会被动同步Form,当然完全可以在这些实际再调用parser。

@qiqiboy
Copy link
Contributor

qiqiboy commented Mar 10, 2020

我这边处理的formatter和parser逻辑:https://github.com/qiqiboy/react-formutil/blob/master/src/fieldHelper.js#L394-L440

@kagawagao
Copy link
Contributor

根据文档中关于 normalizegetValueFromEvent 的语义,那么现在 normalize 是存在歧义的。

  • normalize: 转换字段值给控件
  • getValueFromEvent : 设置如何将 event 的值转换成字段值

首先看现在 rc-field-form 中的实现

  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;
  };

根据语义, normailize 的时机应该是从 Store 给到控件的时候,然而实际却是在 trigger 中做了这一操作

      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,
      });

如果我们将 normalize 执行的时机改到赋值给控件之前,那么 Store 中存储的字段值永远是我们需要提交的值,同时也能解决某一特定 Form.IteminitialValue 的问题

  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;
  };

@qiqiboy
Copy link
Contributor

qiqiboy commented Mar 11, 2020

normalize这么调整,会变成Field更新normalize都会被重复调用。

个人觉得本质上还是要区分开从视图更新(onChange)和从模型更新(setFieldValue),并分开存储两个值。用户想获取和操作的永远是模型值。

@kagawagao
Copy link
Contributor

kagawagao commented Mar 11, 2020

@qiqiboy setFieldValue 本来就要反映到视图上,Form.Item 下的组件本来就是受控组件。而且现在 normalize 也会被重复调用,这个问题是不存在的。

@MinJieLiu
Copy link

我纠结了这么久,感觉 Form.Item 上 就缺少 parserformatter 两种类似的 API,只在初始化和提交时做转换,中间怎样改变都无需关心,至于 normalize 使用场景倒还变得非常少了。

现在处理每次单独转换是极其难受的,每个表单都有此类需求。

@kagawagao @qiqiboy 的提议我是非常认同的。 @zombieJ 想了解下 rc-form 接下来是怎么规划的?

@shaodahong
Copy link
Member

shaodahong commented Mar 22, 2020

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>
  );
}

实际使用的时候 outputer 都可以抽出去,viewMode 的时候也可以穿一个 renderView 进去

<MyForm
  onFinish={values => http.post("url.com", values)}
  columns={[
    { name: "name" },
    { name: "age" },
    {
      name: "birthday",
      render: () => <DatePicker />,
      outputer: value => value?.toISOStrng()
    }
  ]}
  isViewMode={isView /** 受控 */}
/>

不建议硬着头皮自己在 onFinish 中处理,冗余代码太多很难看,大多数的时候和后台交互格式都是不变的,解决大多数的问题

  1. 大多数的表单都是 Input,可以省略不写
  2. 大多数都需要 只编辑只读 或者混合,用一个参数搞定就行
  3. 大多数的 outputer 都可以抽到 utils 去

PS: isViewMode 受控的的好处可以是为了权限,比如有些角色只能看

@MinJieLiu
Copy link

@shaodahong 非常感谢,目前还是先封装吧,硬着头皮太不优雅了

@zombieJ
Copy link
Member

zombieJ commented Apr 7, 2020

准备把 v3 的 getValueProps 加回来,它和 getValueFromEvent 才是配对的。

@crazyair
Copy link
Member

#30686

@hardmanhong
Copy link

准备把 v3 的 getValueProps 加回来,它和 getValueFromEvent 才是配对的。

对于自定义上传组件,需要监听上传状态,如 #2423 所描述,需要 setState ,好像配置 getValueProps getValueFromEvent 无法达到转换效果?因为需要在 submit 阶段转换而不是 change 阶段

@m430
Copy link

m430 commented Dec 13, 2021

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>
  );
}

实际使用的时候 outputer 都可以抽出去,viewMode 的时候也可以穿一个 renderView 进去

<MyForm
  onFinish={values => http.post("url.com", values)}
  columns={[
    { name: "name" },
    { name: "age" },
    {
      name: "birthday",
      render: () => <DatePicker />,
      outputer: value => value?.toISOStrng()
    }
  ]}
  isViewMode={isView /** 受控 */}
/>

不建议硬着头皮自己在 onFinish 中处理,冗余代码太多很难看,大多数的时候和后台交互格式都是不变的,解决大多数的问题

  1. 大多数的表单都是 Input,可以省略不写
  2. 大多数都需要 只编辑只读 或者混合,用一个参数搞定就行
  3. 大多数的 outputer 都可以抽到 utils 去

PS: isViewMode 受控的的好处可以是为了权限,比如有些角色只能看

@shaodahong 这个封装思路不错,可以解决简单的Form表单场景,实际的业务要复杂的多,应对Form表单各种嵌套和布局,这个封装就用不了了,最好的解决方案还是,对应的每个原子表单都可以单独在使用时提供转换的方法,api希望可以更语义化一些,比如parseroutputer,现在的getValueFromEventgetValueProps还有normalize,用户心智成本太高了,而且我本以为normalize是干format的事情,最后发现并不是,本质上提供一个初始化的parser和最后提交的outputer就可以了。其余的不需要管呀。现在还是没有支持吗?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.