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

docs: add names demo #48488

Merged
merged 42 commits into from
Apr 26, 2024
Merged
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
42 commits
Select commit Hold shift + click to select a range
f5cb8bc
feat: add names demo
crazyair Apr 16, 2024
5cd56d8
feat: snap
crazyair Apr 16, 2024
5dec714
feat: add doc
crazyair Apr 16, 2024
1ab194d
feat: add doc
crazyair Apr 16, 2024
a99a533
feat: add doc
crazyair Apr 16, 2024
bc0d52f
feat: remove range
crazyair Apr 16, 2024
3b96296
feat: remove range
crazyair Apr 16, 2024
ed93a94
feat: review
crazyair Apr 16, 2024
90c9da8
feat: review
crazyair Apr 16, 2024
792b1ff
feat: review
crazyair Apr 16, 2024
e40c373
feat: review
crazyair Apr 16, 2024
a50d8dd
feat: review
crazyair Apr 16, 2024
c5c9c65
feat: review
crazyair Apr 16, 2024
c956a93
feat: hidden
crazyair Apr 17, 2024
a9a4bd5
feat: resert
crazyair Apr 17, 2024
2eb9a4b
feat: noStyle
crazyair Apr 17, 2024
cdbec87
feat: doc
crazyair Apr 17, 2024
2bec1c6
Merge remote-tracking branch 'origin/master' into add-names
crazyair Apr 17, 2024
dd2246f
feat: review
crazyair Apr 17, 2024
031957d
feat: review
crazyair Apr 17, 2024
9889a42
feat: review
crazyair Apr 17, 2024
9d2d276
Merge remote-tracking branch 'origin/master' into add-names
crazyair Apr 19, 2024
bd1dd94
feat: review
crazyair Apr 20, 2024
d725cc2
feat: add en
crazyair Apr 20, 2024
49e1c3e
feat: doc
crazyair Apr 22, 2024
c73936c
Merge remote-tracking branch 'origin/master' into add-names
crazyair Apr 22, 2024
79c713e
feat: review
crazyair Apr 23, 2024
5364406
feat: review
crazyair Apr 23, 2024
896e239
feat: review
crazyair Apr 23, 2024
5af877c
feat: review
crazyair Apr 23, 2024
9a9bc71
feat: review
crazyair Apr 24, 2024
d8c0403
feat: review
crazyair Apr 24, 2024
9c24ee9
feat: 方法
crazyair Apr 24, 2024
ad5fddb
feat: review
crazyair Apr 24, 2024
30b6923
feat: review
crazyair Apr 24, 2024
8aec2f9
feat: review
crazyair Apr 24, 2024
daca42e
feat: review
crazyair Apr 24, 2024
8ec692c
feat: doc
crazyair Apr 25, 2024
8bbac59
feat: review
crazyair Apr 26, 2024
9f530fd
feat: review
crazyair Apr 26, 2024
0c4ccf8
Merge branch 'master' into add-names
crazyair Apr 26, 2024
3e7c2ce
Merge branch 'master' into add-names
crazyair Apr 26, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
105 changes: 105 additions & 0 deletions docs/blog/form-names.en-US.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
---
title: Encapsulating Form.Item to Convert Arrays to Objects
crazyair marked this conversation as resolved.
Show resolved Hide resolved
date: 2024-04-17
crazyair marked this conversation as resolved.
Show resolved Hide resolved
author: crazyair
---

During the development of forms, there is occasionally a need to combine attributes. The fields displayed in the UI differ from the data structure fields returned by the backend. For example, when interfacing with the backend, the fields for province and city are often defined as two fields `{ province: Beijing, city: Haidian }`, rather than `{ province: [Beijing, Haidian] }`. We often need to handle these values in `initialValues` and `onFinish` as follows:

```tsx
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 simple, it's manageable, but if you encounter a `Form.List` scenario, you'll need to process values with `map`, which can become very complex. Therefore, we need to encapsulate an aggregate field component that allows a single `Form.Item` to handle multiple `name` properties, as follows:

```tsx
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 for children
getValueProps={() => ({ value: names.map((name) => form.getFieldValue(name)) })}
getValueFromEvent={(values) => {
// Set the values of the names fields to the form store
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 to the rule value
const values = names.map((name) => form.getFieldValue(name));
return values;
},
};
}
return thisRule;
})}
{...rest}
/>
{/* Bind additional 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>
);
```
149 changes: 149 additions & 0 deletions docs/blog/form-names.zh-CN.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
---
title: 封装 Form.Item 实现数组转对象
date: 2024-04-17
crazyair marked this conversation as resolved.
Show resolved Hide resolved
author: crazyair
---

在表单开发过程中,偶尔会遇到组合属性的需求。UI 展示字段与后端返回数据结构字段有所不同。比如说,跟后端对接接口,定义省市字段经常是 2 个字段 `{ province: Beijing, city: Haidian }`,而不是 `{ province:[Beijing,Haidian] }`,因此则需要在 `initialValues` 以及 `onFinish` 处理值,如下:

```tsx
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;
```

## 封装聚合字段组件

当表单比较简单还好,如果遇到 `Form.List` 场景,就需要 `map` 处理值,将变的很复杂。于是我们需要封装聚合字段组件,实现一个 `Form.Item` 可以写多个 `name`。

## 思路整理

要实现聚合字段功能,我们需要用到 `getValueProps` `getValueFromEvent` `transform`,从而实现数据从 `FormStore` 中的转化,以及变更时重新传入 `FormStore` 结构中。

### getValueProps

默认情况下,`Form.Item` 会将 `FormStore` 中的字段值作为 `value prop` 传递给子组件。而通过 `getValueProps` 可以自定义传入给子组件的 `props` 从而实现转化功能。在聚合场景中,我们可以遍历 `names` 分别将 `FormStore` 中的值组合为一个 `value` 传递给子组件。
crazyair marked this conversation as resolved.
Show resolved Hide resolved

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

### getValueFromEvent

当子组件修改值时,使用 `setFields` 方法将子组件返回的聚合 `value` 分别设置给对应的 `name`,从而实现更新 `FormStore` 中 `names` 的值
crazyair marked this conversation as resolved.
Show resolved Hide resolved

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

### transform

`rules` 中校验默认提供的 `value` 来源于子组件变更时传递给 `name` 对应的值,还需要从 `FormStore` 获取 `names` 的值使用 `transform` 方法修改 `rules` 的 `value`
crazyair marked this conversation as resolved.
Show resolved Hide resolved

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

## 最终效果

当表单比较简单还好,如果遇到 `Form.List` 场景,就需要 `map` 处理值,将变的很复杂。于是我们需要封装聚合字段组件,实现一个 `Form.Item` 可以写多个 `name`,如下:
crazyair marked this conversation as resolved.
Show resolved Hide resolved

```tsx
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}
// 将 names 的值转成数组传给 children
getValueProps={() => ({ value: names.map((name) => form.getFieldValue(name)) })}
getValueFromEvent={(values) => {
// 将 form store 分别设置给 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: () => {
// 将 names 字段的值设置给 rule value
const values = names.map((name) => form.getFieldValue(name));
crazyair marked this conversation as resolved.
Show resolved Hide resolved
return values;
},
};
}
return thisRule;
})}
{...rest}
/>
{/* 绑定其他字段,使其可以 getFieldValue 获取值、setFields 设置值 */}
{resetNames.map((name) => (
crazyair marked this conversation as resolved.
Show resolved Hide resolved
<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>
);
```
zombieJ marked this conversation as resolved.
Show resolved Hide resolved

## 总结

通过这种方式,我们实现了一个可以在 `Form.Item` 中操作多个 `name` 的功能,使得表单逻辑更加清晰和易于维护。另外此示例还有些边界场景没有考虑,比如 `setFields([{ name:'city' value:'nanjing' }])` 不会更新 `Cascader` 选中的值,需要增加 `Form.useWatch(values => resetNames.map(name => get(values, name)), form);` 达到刷新效果等。更多的边界问题就交给你去试试吧~