likes
comments
collection
share

Ant Design 表单陷阱:正确使用 Form.Item 与自定义表单控件

作者站长头像
站长
· 阅读数 51

问题描述与注意事项

官方文档

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

效果

Ant Design 表单陷阱:正确使用 Form.Item 与自定义表单控件

说明

在提供的示例中,Form.Item 被赋予了 name="price",但其内部包含了多个输入控件。

由 name 属性的作用可以猜想到,这种错误的写法可能会造成的结果是,form 表单收集到的表单数据不正确(从打印数据 formValues 可以看出)

由于在业务开发中可能并不会持续跟踪 form 表单内的数据,所以在业务开发过程中,最可能得到的错误表现是

  • onFinish 表单提交时收集到的数据(从打印数据 Received values from form 可以看出)
  • validator 表单验证时的数据(从打印数据 validator-checkPrice 可以看出)

正确示例

但在业务开发中,Form.Item 内放置多个元素属于很正常的情况,一般正确的有以下一些方法:

  1. 使用嵌套的 Form.Item:每个控件都包裹在独立的 Form.Item 中,并为每个 Form.Item 分别指定 name 属性。
  2. 自定义表单控件:创建一个自定义组件,该组件内部管理自己的状态,并提供 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
评论
请登录