AntD 表单踩坑

1,565 阅读6分钟

Form.Item相关

form.item 带上name的表示为受控组件,不能通过onChange直接赋值

Form.item支持嵌套,但是一个Form.item只能包裹一个输入

如果你需要 antd 样式的 label,可以通过外部包裹 Form.Item 来实现

Form.item如果包裹CheckBox\Switch,需要添加属性 valuePropName="checked"

<Form.Item>
    <Form.Item name="remember" valuePropName="checked" noStyle>
      <Checkbox>Remember me</Checkbox>
    </Form.Item>

    <a className="login-form-forgot" href="">
      Forgot password
    </a>
</Form.Item>

Form.Item noStyle

多用于嵌套Form.Item的场景中

<Form.Item label="导出路径" {...filePathItemRadioLayout}>
    <Input.Group compact>
            <Form.Item
                    name={'filePath'}
                    noStyle
                    rules={[{required: true, message: '请选择导出文件路径'}]}
            >
                    <Input style={{width: 'calc(100% - 66px)'}} />
            </Form.Item>
            <Button onClick={handleSelectPath}>选择</Button>
    </Input.Group>
</Form.Item>

<Form.Item label="InputNumber">
    <Form.Item name="input-number" noStyle>
      <InputNumber min={1} max={10} />
    </Form.Item>
    <span className="ant-form-text"> machines</span>
</Form.Item>

form.getFieldValue

可以在form.item内部使用getFieldValue,获取对应字段名的值

<Form.Item
      label="User List"
      shouldUpdate={(prevValues, curValues) => prevValues.users !== curValues.users}
    >
  {({ getFieldValue }) => {
    const users: UserType[] = getFieldValue('users') || [];
    return users.length ? (
      <ul>
        {users.map((user, index) => (
          <li key={index} className="user">
            <Avatar icon={<UserOutlined />} />
            {user.name} - {user.age}
          </li>
        ))}
      </ul>
    ) : (
      <Typography.Text className="ant-form-text" type="secondary">
        ( <SmileOutlined /> No user yet. )
      </Typography.Text>
    );
  }}
</Form.Item>

确认密码:首先保证必填; 其次若值与密码一致,则返回成功

<Form.Item
   name="confirm"
   label="Confirm Password"
   dependencies={['password']}
   hasFeedback
   rules={[
     {
       required: true,
       message: 'Please confirm your password!',
     },
     ({ getFieldValue }) => ({
       validator(_, value) {
         if (!value || getFieldValue('password') === value) {
           return Promise.resolve();
         }
         return Promise.reject(new Error('The two passwords that you entered do not match!'));
       },
     }),
   ]}
 >
   <Input.Password />
</Form.Item>

Form.item 设置 shouldUpdate 自定义字段更新逻辑

Form 通过增量更新方式,只更新被修改的字段相关组件以达到性能优化目的。 大部分场景下,你只需要编写代码或者与 dependencies 属性配合校验即可。 而在某些特定场景,例如修改某个字段值后出现新的字段选项、或者纯粹希望表单任意变化都对某一个区域进行渲染。你可以通过 shouldUpdate 修改 Form.Item 的更新逻辑。

当 shouldUpdate 为 true 时,Form 的任意变化都会使该 Form.Item 重新渲染。 这对于自定义渲染一些区域十分有帮助:

<Form.Item shouldUpdate>
  {() => {
    return <pre>{JSON.stringify(form.getFieldsValue(), null, 2)}</pre>;
  }}
</Form.Item>

当 shouldUpdate 为方法时,表单的每次数值更新都会调用该方法,提供原先的值与当前的值以供你比较是否需要更新。这对于是否根据值来渲染额外字段十分有帮助:

<Form.Item shouldUpdate={(prevValues, curValues) => prevValues.additional !== curValues.additional}>
  {() => {
    return (
      <Form.Item name="other">
        <Input />
      </Form.Item>
    );
  }}
</Form.Item>

Form.Item Row Col

image.png

<Form.Item label="Captcha" extra="We must make sure that your are a human.">
    <Row gutter={8}>
      <Col span={12}>
        <Form.Item
          name="captcha"
          noStyle
          rules={[{ required: true, message: 'Please input the captcha you got!' }]}
        >
          <Input />
        </Form.Item>
      </Col>
      <Col span={12}>
        <Button>Get captcha</Button>
      </Col>
    </Row>
</Form.Item>

Form.Item 设置tooltip

image.png

<Form.Item label="Field A" required tooltip="This is a required field">
        <Input placeholder="input placeholder" />
</Form.Item>
<Form.Item
    label="Field B"
    tooltip={{ title: 'Tooltip with customize icon', icon: <InfoCircleOutlined /> }}
  >
    <Input placeholder="input placeholder" />
</Form.Item>

Form.Item是 文件上传的话, valuePropName="fileList"

getValueFromEvent 设置如何将 event 的值转换成字段值

const normFile = (e: any) => {
  console.log('Upload event:', e);
  if (Array.isArray(e)) {
    return e;
  }
  return e?.fileList;
};
<Form.Item
    name="upload"
    label="Upload"
    valuePropName="fileList"
    getValueFromEvent={normFile}
    extra="longgggggggggggggggggggggggggggggggggg"
  >
    <Upload name="logo" action="/upload.do" listType="picture">
      <Button icon={<UploadOutlined />}>Click to upload</Button>
    </Upload>
</Form.Item>

自动提示 AutoComplete

const [autoCompleteResult, setAutoCompleteResult] = useState<string[]>([]);

  const onWebsiteChange = (value: string) => {
    if (!value) {
      setAutoCompleteResult([]);
    } else {
      setAutoCompleteResult(['.com', '.org', '.net'].map(domain => `${value}${domain}`));
    }
  };

  const websiteOptions = autoCompleteResult.map(website => ({
    label: website,
    value: website,
  }));
<Form.Item
    name="website"
    label="Website"
    rules={[{ required: true, message: 'Please input website!' }]}
  >
    <AutoComplete options={websiteOptions} onChange={onWebsiteChange} placeholder="website">
      <Input />
    </AutoComplete>
</Form.Item>

表单校验

Form.Item 设置的rules是一个数组

 <Form.Item
        name="email"
        label="E-mail"
        rules={[
          {
            type: 'email',
            message: 'The input is not valid E-mail!',
          },
          {
            required: true,
            message: 'Please input your E-mail!',
          },
        ]}
      >
        <Input />
</Form.Item>

whitespace: true 如果字段仅包含空格则校验不通过,只在 type: 'string' 时生效

 <Form.Item
    name="nickname"
    label="Nickname"
    tooltip="What do you want others to call you?"
    rules={[{ required: true, message: 'Please input your nickname!', whitespace: true }]}
  >
    <Input />
</Form.Item>

自定义 validator

你可以选择通过 async 返回一个 promise 或者使用 try...catch 进行错误捕获

validator: async (rule, value) => {
  throw new Error('Something wrong!');
}

// or

validator(rule, value, callback) => {
  try {
    throw new Error('Something wrong!');
  } catch (err) {
    callback(err);
  }
}

Form.Item dependencies 设置依赖字段

当字段间存在依赖关系时使用。如果一个字段设置了 dependencies 属性。 那么它所依赖的字段更新时,该字段将自动触发更新与校验。 一种常见的场景,就是注册用户表单的“密码”与“确认密码”字段。 “确认密码”校验依赖于“密码”字段,设置 dependencies 后,“密码”字段更新会重新触发“校验密码”的校验逻辑

<Form.Item
        name="confirm"
        label="Confirm Password"
        dependencies={['password']}
        hasFeedback
        rules={[
          {
            required: true,
            message: 'Please confirm your password!',
          },
          ({ getFieldValue }) => ({
            validator(_, value) {
              if (!value || getFieldValue('password') === value) {
                return Promise.resolve();
              }
              return Promise.reject(new Error('The two passwords that you entered do not match!'));
            },
          }),
        ]}
      >
        <Input.Password />
</Form.Item>

dependencies 不应和 shouldUpdate 一起使用,因为这可能带来更新逻辑的混乱。

hasFeedback 配合 validateStatus 属性使用,展示校验状态图标,建议只配合 Input 组件使用

image.png

校验项是数组

<Form.Item
    name="residence"
    label="Habitual Residence"
    rules={[
      { type: 'array', required: true, message: 'Please select your habitual residence!' },
    ]}
  >
    <Cascader options={residences} />
</Form.Item>

手动触发异步校验

const onCheck = async () => {
    try {
      const values = await form.validateFields();
      console.log('Success:', values);
    } catch (errorInfo) {
      console.log('Failed:', errorInfo);
    }
  };
form.validateFields()
  .then(values => {
    form.resetFields();
    onCreate(values);
  })
  .catch(info => {
    console.log('Validate Failed:', info);
  });


form.validateFields 返回示例

validateFields()
  .then(values => {
    /*
  values:
    {
      username: 'username',
      password: 'password',
    }
  */
  })
  .catch(errorInfo => {
    /*
    errorInfo:
      {
        values: {
          username: 'username',
          password: 'password',
        },
        errorFields: [
          { name: ['password'], errors: ['Please input your Password!'] },
        ],
        outOfDate: false,
      }
    */
  });

Form相关

Form layout="inline" 会将每一项输入框在一行进行排列

const [form] = Form.useForm();

需要将form关联到Form标签上

form.setFieldsValue

支持对象深层次修改单个属性,例如

const toggleAllSelected = (checked: boolean) => {
        form.setFieldsValue({
                originChannel: {checked: checked},
                extractChromatogram: {checked: checked},
                extractMassSpectrum: {checked: checked},
        })
}

form.resetFields();

重置一组字段到 initialValues

设置确认按钮在表单所有字段未都被触摸过或表单校验有误时禁用

shouldUpdate 表单值改变就要刷新 form.isFieldsTouched(true) 检查一组字段是否被用户操作过,allTouched 为 true 时检查是否所有字段都被操作过 !form.isFieldsTouched(true) 检查是否所有字段没有都被操作过

form.getFieldsError() 获取一组字段名对应的错误信息,返回为数组形式

 <Form.Item shouldUpdate>
    {() => (
      <Button
        type="primary"
        htmlType="submit"
        disabled={
          !form.isFieldsTouched(true) ||
          !!form.getFieldsError().filter(({ errors }) => errors.length).length
        }
      >
        Log in
      </Button>
    )}
</Form.Item>

{...formItemLayout} {...tailFormItemLayout}

const formItemLayout = {
  labelCol: {
    xs: { span: 24 },
    sm: { span: 8 },
  },
  wrapperCol: {
    xs: { span: 24 },
    sm: { span: 16 },
  },
};
const tailFormItemLayout = {
  wrapperCol: {
    xs: {
      span: 24,
      offset: 0,
    },
    sm: {
      span: 16,
      offset: 8,
    },
  },
};

Form.useFormInstance

4.20.0 新增,获取当前上下文正在使用的 Form 实例,常见于封装子组件消费无需透传 Form 实例

const Sub = () => {
  const form = Form.useFormInstance();

  return <Button onClick={() => form.setFieldsValue({})} />;
};

export default () => {
  const [form] = Form.useForm();

  return (
    <Form form={form}>
      <Sub />
    </Form>
  );
};

Form.useWatch

4.20.0 新增,用于直接获取 form 中字段对应的值。通过该 Hooks 可以与诸如 useSWR 进行联动从而降低维护成本:

const Demo = () => {
  const [form] = Form.useForm();
  const userName = Form.useWatch('username', form);

  const { data: options } = useSWR(`/api/user/${userName}`, fetcher);

  return (
    <Form form={form}>
      <Form.Item name="username">
        <AutoComplete options={options} />
      </Form.Item>
    </Form>
  );
};

Form 仅会对变更的 Field 进行刷新,从而避免完整的组件刷新可能引发的性能问题。 因而你无法在 render 阶段通过 form.getFieldsValue 来实时获取字段值,而 useWatch 提供了一种特定字段访问的方式,从而使得在当前组件中可以直接消费字段的值。 同时,如果为了更好的渲染性能,你可以通过 Field 的 renderProps 仅更新需要更新的部分。 而当当前组件更新或者 effect 都不需要消费字段值时,则可以通过 onValuesChange 将数据抛出,从而避免组件更新。

FAQ

为什么 Form.Item 下的子组件 defaultValue 不生效?#

当你为 Form.Item 设置 name 属性后,子组件会转为受控模式。 因而 defaultValue 不会生效。你需要在 Form 上通过 initialValues 设置默认值。

Form 的 initialValues 与 Item 的 initialValue 区别?#

在大部分场景下,我们总是推荐优先使用 Form 的 initialValues。 只有存在动态字段时你才应该使用 Item 的 initialValue。 默认值遵循以下规则:

  1. Form 的 initialValues 拥有最高优先级
  2. Field 的 initialValue 次之 *. 多个同 name Item 都设置 initialValue 时,则 Item 的 initialValue 不生效

为什么字段设置 rules 后更改值 onFieldsChange 会触发三次?#

字段除了本身的值变化外,校验也是其状态之一。 因而在触发字段变化会经历以下几个阶段:

  1. Trigger value change
  2. Rule validating
  3. Rule validated

在触发过程中,调用 isFieldValidating 会经历 false > true > false 的变化过程。