ant-design/docs/blog/form-names.en-US.md
叶枫 b173ce10a6
docs: add names demo (#48488)
* feat: add names demo

* feat: snap

* feat: add doc

* feat: add doc

* feat: add doc

* feat: remove range

* feat: remove range

* feat: review

* feat: review

* feat: review

* feat: review

* feat: review

* feat: review

* feat: hidden

* feat: resert

* feat: noStyle

* feat: doc

* feat: review

* feat: review

* feat: review

* feat: review

* feat: add en

* feat: doc

* feat: review

* feat: review

* feat: review

* feat: review

* feat: review

* feat: review

* feat: 方法

* feat: review

* feat: review

* feat: review

* feat: review

* feat: doc

* feat: review

* feat: review
2024-04-26 21:10:09 +08:00

6.0 KiB

title date author
HOC Aggregate FieldItem 2024-04-26 crazyair

During the form development process, there are occasional needs for combining attributes. The UI display fields are different from the backend data structure fields. For example, when interfacing with the backend, the province and city fields are often defined as two separate fields { province: Beijing, city: Haidian }, rather than a combined one { province: [Beijing, Haidian] }. Therefore, it is necessary to handle the values in initialValues and onFinish as follows:

import React from 'react';
import { Cascader, Form } from 'antd';

const data = { province: 'Beijing', city: 'Haidian' };
const options = [
  { value: 'zhejiang', label: 'Zhejiang', children: [{ value: 'hangzhou', label: 'Hangzhou' }] },
  { value: 'jiangsu', label: 'Jiangsu', children: [{ value: 'nanjing', label: 'Nanjing' }] },
];
const createUser = (values) => console.log(values);

const Demo = () => (
  <Form
    initialValues={{ province: [data.province, data.city] }}
    onFinish={(values) => {
      const { province, ...rest } = values;
      createUser({ province: province[0], city: province[1], ...rest });
    }}
  >
    <Form.Item label="Address" name="province">
      <Cascader options={options} placeholder="Please select" />
    </Form.Item>
  </Form>
);
export default Demo;

Encapsulating Aggregate Field Components

When the form is relatively simple, it's manageable, but when encountering a Form.List scenario, it becomes necessary to process the values using map, which can become quite complex. Therefore, we need to encapsulate an aggregated field component to enable a single Form.Item to handle multiple name attributes.

Approach Summary

To implement the aggregation field functionality, we need to utilize getValueProps, getValueFromEvent, and transform to facilitate the transformation of data from FormStore and to re-insert the structure into FormStore upon change.

getValueProps

By default, Form.Item passes the field value from FormStore as the value prop to the child component. However, with getValueProps, you can customize the props that are passed to the child component to implement transformation functionality. In an aggregation scenario, we can iterate through names and combine the values from FormStore into a single value that is then passed to the child component:

getValueProps={() => ({ value: names.map((name) => form.getFieldValue(name)) })}

getValueFromEvent

When the child component modifies the value, the setFields method is used to set the aggregated value returned by the child component to the corresponding name, thereby updating the values of names in FormStore:

getValueFromEvent={(values) => {
    form.setFields(names.map((name, index) => ({ name, value: values[index] })));
    return values[0];
}}

transform

In rules, the default provided value for validation originates from the value passed to the corresponding name when the child component changes. Additionally, it is necessary to retrieve the values of names from FormStore and use the transform method to modify the value of rules:

rules={[{
  transform: () => {
    const values = names.map((name) => form.getFieldValue(name));
    return values;
  },
}]}

Final Result

import React from 'react';
import type { FormItemProps } from 'antd';
import { Cascader, Form } from 'antd';

export const AggregateFormItem = (
  props: FormItemProps & { names?: FormItemProps<Record<string, any>>['name'][] },
) => {
  const form = Form.useFormInstance();

  const { names = [], rules = [], ...rest } = props;
  const [firstName, ...resetNames] = names;
  return (
    <>
      <Form.Item
        name={firstName}
        // Convert the values of names into an array passed to children
        getValueProps={() => ({ value: names.map((name) => form.getFieldValue(name)) })}
        getValueFromEvent={(values) => {
          // Set the form store values for names
          form.setFields(names.map((name, index) => ({ name, value: values[index] })));
          return values[0];
        }}
        rules={rules.map((thisRule) => {
          if (typeof thisRule === 'object') {
            return {
              ...thisRule,
              transform: () => {
                // Set the values of the names fields for the rule value
                const values = names.map((name) => form.getFieldValue(name));
                return values;
              },
            };
          }
          return thisRule;
        })}
        {...rest}
      />
      {/*  Bind other fields so they can getFieldValue to get values and setFields to set values */}
      {resetNames.map((name) => (
        <Form.Item key={name?.toString()} name={name} noStyle />
      ))}
    </>
  );
};

const data = { province: 'Beijing', city: 'Haidian' };
const options = [
  { value: 'zhejiang', label: 'Zhejiang', children: [{ value: 'hangzhou', label: 'Hangzhou' }] },
  { value: 'jiangsu', label: 'Jiangsu', children: [{ value: 'nanjing', label: 'Nanjing' }] },
];
const createUser = (values) => console.log(values);

export const Demo = () => (
  <Form
    initialValues={data}
    onFinish={(values) => {
      createUser(values);
    }}
  >
    <AggregateFormItem label="Address" names={['province', 'city']} rules={[{ required: true }]}>
      <Cascader options={options} placeholder="Please select" />
    </AggregateFormItem>
  </Form>
);

Summary

By doing so, we have implemented a feature that allows for operating multiple names within a Form.Item, making the form logic clearer and easier to maintain. Additionally, there are some edge cases in this example that have not been considered. For instance, setFields([{ name:'city', value:'nanjing' }]) will not update the selected value of Cascader. To achieve a refresh effect, you need to add Form.useWatch(values => resetNames.map(name => get(values, name)), form);. Feel free to explore more edge cases and handle them as needed.