Ant Design 表单陷阱:正确使用 Form.Item 与自定义表单控件
问题描述与注意事项
在使用 Ant Design 的 Form 组件构建复杂的表单时,我们可能会遇到一些容易忽略的陷阱,尤其是当 Form.Item
内包含多个元素时。本文将探讨这个问题,并提供正确的实践方法。
在 Ant Design 中,Form.Item
是表单字段的容器,每个 Form.Item
可以包含一个或多个表单控件。然而,如果在一个 Form.Item
中放置了多个元素,并且错误地为这个 Form.Item
指定了 name
属性,就可能导致表单收集的数据不符合预期。
注意:当 Form.Item 内有多个元素时,不要在这个 Item 上指定 name 属性,只将其作为布局作用。
这里需要明确的是:Form.Item 中 name 属性的作用是:name 属性值会作为 form 表单数据中该 Form.Item 所对应数据的属性名。
错误示例
代码
<Form.Item name="price" label="Price">
这个 Form.Item 内部有多个元素,且指定了 name 属性,如下所示:
<Form.Item name="price" label="Price" rules={[{ validator: checkPrice }]}>
{renderPriceInput()}
</Form.Item>
完整代码:
import { Button, Form, Input, Select } from 'antd';
import React, { useState } from 'react';
const { Option } = Select;
type Currency = 'rmb' | 'dollar';
export const RenderPriceInput: React.FC = () => {
const [number1, setNumber1] = useState(0);
const [number, setNumber] = useState(0);
const [currency, setCurrency] = useState<Currency>('rmb');
const [form] = Form.useForm();
const formValues = form.getFieldsValue();
console.log("formValues", formValues);
const onNumberChange1 = (e: React.ChangeEvent<HTMLInputElement>) => {
const newNumber = parseInt(e.target.value || '0', 10);
if (Number.isNaN(number)) {
return;
}
setNumber1(newNumber);
};
const onNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newNumber = parseInt(e.target.value || '0', 10);
if (Number.isNaN(number)) {
return;
}
setNumber(newNumber);
};
const onCurrencyChange = (newCurrency: Currency) => {
setCurrency(newCurrency);
};
const onFinish = (values: any) => {
console.log('Received values from form: ', values);
};
const checkPrice = (_: any, value: any) => {
console.log("validator-checkPrice", value);
if (value > 0) {
return Promise.resolve();
}
return Promise.reject(new Error('Price must be greater than zero!'));
};
const renderPriceInput = () => {
return (
<span
style={{
display: 'flex',
gap: '8px',
alignItems: 'center'
}}
>
<Input
type="text"
value={number1}
onChange={onNumberChange1}
style={{ width: 100 }}
/>
<span>+</span>
<Input
type="text"
value={number}
onChange={onNumberChange}
style={{ width: 100 }}
/>
<Select
value={currency}
style={{ width: 80 }}
onChange={onCurrencyChange}
>
<Option value="rmb">RMB</Option>
<Option value="dollar">Dollar</Option>
</Select>
</span>
);
}
return (
<Form
form={form}
name="customized_form_controls"
layout="inline"
onFinish={onFinish}
initialValues={{
price: {
number: 0,
currency: 'rmb',
},
}}
>
<Form.Item name="price" label="Price" rules={[{ validator: checkPrice }]}>
{renderPriceInput()}
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
效果
说明
在提供的示例中,Form.Item
被赋予了 name="price"
,但其内部包含了多个输入控件。
由 name 属性的作用可以猜想到,这种错误的写法可能会造成的结果是,form 表单收集到的表单数据不正确(从打印数据 formValues 可以看出)
由于在业务开发中可能并不会持续跟踪 form 表单内的数据,所以在业务开发过程中,最可能得到的错误表现是
- onFinish 表单提交时收集到的数据(从打印数据 Received values from form 可以看出)
- validator 表单验证时的数据(从打印数据 validator-checkPrice 可以看出)
正确示例
但在业务开发中,Form.Item 内放置多个元素属于很正常的情况,一般正确的有以下一些方法:
- 使用嵌套的
Form.Item
:每个控件都包裹在独立的Form.Item
中,并为每个Form.Item
分别指定name
属性。 - 自定义表单控件:创建一个自定义组件,该组件内部管理自己的状态,并提供
value
和onChange
属性以与 Form 组件协作。
自定义表单控件
上述例子可以通过封装自定义表单控件达到同样的效果,代码如下:
import { Button, Form, Input, Select } from 'antd';
import React, { useState } from 'react';
const { Option } = Select;
type Currency = 'rmb' | 'dollar';
interface PriceValue {
number1?: number;
number?: number;
currency?: Currency;
}
interface PriceInputProps {
value?: PriceValue;
onChange?: (value: PriceValue) => void;
}
const PriceInput: React.FC<PriceInputProps> = ({ value = {}, onChange }) => {
const [number1, setNumber1] = useState(0);
const [number, setNumber] = useState(0);
const [currency, setCurrency] = useState<Currency>('rmb');
const triggerChange = (changedValue: { number1?: number; number?: number; currency?: Currency }) => {
onChange?.({ number1, number, currency, ...value, ...changedValue });
};
const onNumberChange1 = (e: React.ChangeEvent<HTMLInputElement>) => {
const newNumber = parseInt(e.target.value || '0', 10);
if (Number.isNaN(number1)) {
return;
}
if (!('number1' in value)) {
setNumber1(newNumber);
}
triggerChange({ number1: newNumber });
};
const onNumberChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const newNumber = parseInt(e.target.value || '0', 10);
if (Number.isNaN(number)) {
return;
}
if (!('number' in value)) {
setNumber(newNumber);
}
triggerChange({ number: newNumber });
};
const onCurrencyChange = (newCurrency: Currency) => {
if (!('currency' in value)) {
setCurrency(newCurrency);
}
triggerChange({ currency: newCurrency });
};
return (
<span
style={{
display: 'flex',
gap: '8px',
alignItems: 'center'
}}
>
<Input
type="text"
value={value.number1 || number1}
onChange={onNumberChange1}
style={{ width: 100 }}
/>
<span>+</span>
<Input
type="text"
value={value.number || number}
onChange={onNumberChange}
style={{ width: 100 }}
/>
<Select
value={value.currency || currency}
style={{ width: 80}}
onChange={onCurrencyChange}
>
<Option value="rmb">RMB</Option>
<Option value="dollar">Dollar</Option>
</Select>
</span>
);
};
export const FormPriceInput: React.FC = () => {
const onFinish = (values: any) => {
console.log('Received values from form: ', values);
};
const checkPrice = (_: any, value: { number: number }) => {
if (value.number > 0) {
return Promise.resolve();
}
return Promise.reject(new Error('Price must be greater than zero!'));
};
return (
<Form
name="customized_form_controls"
layout="inline"
onFinish={onFinish}
initialValues={{
price: {
number: 0,
currency: 'rmb',
},
}}
>
<Form.Item name="price" label="Price" rules={[{ validator: checkPrice }]}>
<PriceInput />
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
Submit
</Button>
</Form.Item>
</Form>
);
};
注意 PriceInput 组件可以默认接收到 value onChange 这两个属性,这是 Form 内部处理的。
小结
正确使用 Ant Design 的 Form 组件和 Form.Item
是构建有效表单的关键。通过自定义表单控件,我们可以避免常见的陷阱,并确保表单数据的准确性和完整性。记住,当 Form.Item
内包含多个元素时,不要为其指定 name
属性,而是应该使用嵌套的 Form.Item
或自定义控件来管理元素的状态。
转载自:https://juejin.cn/post/7388056946121162789