• English
  • ProForm Advanced Form

    ProForm adds some syntactic sugar and more layout settings to the original Form to help us develop a form quickly. Also add some default behaviors to make our forms work well by default.

    Step-by-step forms, Modal forms, Drawer forms, Query forms, lightweight filters, and other layouts cover most scenarios and help you accomplish more with less layout code.

    • If you want to set default values, please use initialValues, any direct use of component value and onChange may cause value binding failure.
    • If you want to link forms or do some dependencies, you can use render props mode, ProFormDependency is definitely the best choice
    • ProForm's onFinish, unlike antd's Form, is a Promise that will automatically set the button to load for you if you return normally.
    • If you want to listen to a value, it is recommended to use onValuesChange. Keeping a unidirectional data flow is a great benefit for both developers and maintainers
    • ProForm is not magic: it is just a thin wrapper around antd's Form—use Form.Item for custom controls and mix freely.

    Issue regression scenarios

    Initialization, list conversion, and request refresh
    import type { ProFormInstance } from '@ant-design/pro-components';
    import {
      BetaSchemaForm,
      ModalForm,
      ProCard,
      ProForm,
      ProFormList,
      ProFormText,
    } from '@ant-design/pro-components';
    import { Alert, Button, Space, Tabs, Typography, message } from 'antd';
    import { useRef, useState } from 'react';
    
    const toList = (value: Record<string, string>) =>
      Object.entries(value).map(([key, itemValue]) => ({
        key,
        value: itemValue,
      }));
    
    const ListConvertValue = () => {
      const formRef = useRef<ProFormInstance>();
      return (
        <ProForm
          formRef={formRef}
          submitter={false}
          initialValues={{ configs: { first: 'one', second: 'two' } }}
        >
          <Space style={{ marginBlockEnd: 16 }}>
            <Button
              onClick={() =>
                formRef.current?.setFieldsValue({ configs: { third: 'three' } })
              }
            >
              setFieldsValue 写入对象
            </Button>
            <Typography.Text type="secondary">
              对象会在初始值及外部写入时转换为列表。
            </Typography.Text>
          </Space>
          <ProFormList name="configs" convertValue={toList}>
            <ProFormText name="key" label="Key" />
            <ProFormText name="value" label="Value" />
          </ProFormList>
        </ProForm>
      );
    };
    
    const RequestReuse = () => {
      const [id, setId] = useState(0);
      return (
        <Space direction="vertical" style={{ width: '100%' }}>
          <Space>
            {[0, 1, 0].map((value, index) => (
              <Button key={`${value}-${index}`} onClick={() => setId(value)}>
                参数 {value}
              </Button>
            ))}
          </Space>
          <ProForm
            key="request-reuse-form"
            params={{ id }}
            request={async ({ id: requestId }) => ({
              name: `服务端返回:参数 ${requestId}(${new Date().toLocaleTimeString()})`,
            })}
            submitter={false}
          >
            <ProFormText name="name" label="请求结果" width="lg" />
          </ProForm>
        </Space>
      );
    };
    
    const OverlayInitialization = () => {
      const [open, setOpen] = useState(false);
      const [record, setRecord] = useState({ name: 'Alice' });
      return (
        <Space>
          {['Alice', 'Bob'].map((name) => (
            <Button
              key={name}
              onClick={() => {
                setRecord({ name });
                setOpen(true);
              }}
            >
              编辑 {name}
            </Button>
          ))}
          <ModalForm
            open={open}
            onOpenChange={setOpen}
            initialValues={record}
            request={async () => record}
            title="受控弹窗初始化"
            onFinish={async ({ name }) => {
              message.success(`已保存 ${name}`);
              return true;
            }}
          >
            <ProFormText name="name" label="姓名" />
          </ModalForm>
        </Space>
      );
    };
    
    export default () => (
      <ProCard direction="column" ghost gutter={[0, 16]}>
        <Alert
          type="info"
          showIcon
          message="表单初始化与转换回归场景"
          description="覆盖弹层初始化、ProFormList convertValue、SchemaForm 独立宽度及重复参数请求。"
        />
        <ProCard>
          <Tabs
            items={[
              {
                key: 'overlay',
                label: '弹层初始化',
                children: <OverlayInitialization />,
              },
              {
                key: 'list',
                label: '列表转换 #8702',
                children: <ListConvertValue />,
              },
              {
                key: 'schema',
                label: 'SchemaForm 宽度 #9061',
                children: (
                  <BetaSchemaForm
                    submitter={false}
                    columns={[
                      {
                        title: '表格列宽 80,表单宽度 328',
                        dataIndex: 'title',
                        width: 80,
                        formWidth: 328,
                      },
                    ]}
                  />
                ),
              },
              {
                key: 'request',
                label: '重复参数请求 #8375',
                children: <RequestReuse />,
              },
            ]}
          />
        </ProCard>
      </ProCard>
    );
    
    // Set overall default values
    <ProForm initialValues={obj} />
    
    // Set the individual control's
    <ProForm
     onValuesChange={(changeValues) => console.log(changeValues)}
    >
      <ProFormText initialValue="prop"/>
    </ProForm>
    
    // Interdependent component linkage
    <ProForm>
      <Form.Item noStyle shouldUpdate>
        {(form) => {
          return (
            <ProFormSelect
              options={[
                {
                  value: "chapter",
                  label: "Effective when stamped",
                },
              ]}
              width="md"
              name="useMode"
              label={`Agreement with "${form.getFieldValue('name') || '—'}"`}
            />
          );
        }}
      </Form.Item>
    </ProForm>;
    
    
    // Using custom components
    <ProForm>
      <Form.Item name="switch" label="Switch" valuePropName="checked">
        <Switch />
      </Form.Item>
    </ProForm>

    When to Use ProForm?

    ProForm is the best choice when you want to implement a form quickly but don't want to spend too much time on layout.

    ProForm is a degradable encapsulation based on antd Form, fully aligned with antd functionality, but adds some preset behaviors and multiple layouts on top of it. These layouts can be switched seamlessly and share a common API.

    LayoutUsage Scenario
    ProFormStandard Form, adds automatic loading in onFinish and automatic retrieval of default values ​​based on request.
    ModalForm|DrawerFormAdded trigger on the basis of ProForm, no need to maintain open status.
    QueryFilterGenerally used as a filter form, needs to be used with other data display components.
    LightFilterGenerally used as an inline built-in filter, such as card action bar and table action bar.
    StepsFormStep form, needs to be used with StepForm configuration.

    Data conversion

    Many times there is no exact match between the data required by the component and the data required by the backend, and ProForm provides two APIs to solve this problem, transform and convertValue.

    convertValue Pre-conversion

    convertValue occurs before the component obtains data, usually the data directly sent from the backend to the frontend, and sometimes needs to be refined.

       export type SearchConvertKeyFn =
        (value: any, field: NamePath)=>string | boolean | Record<string, any>;
      /**
       * @name Converts the value when getting it, generally used to format the data into the format received by the component
       * @param value field value
       * @param namePath field name
       * @returns the new value of the field
       *
       * @example a,b => [a,b]
       * convertValue:(value,namePath)=>value.split(",")
       * @example string =>json
       * convertValue:(value,namePath)=>JSON.parse(value)
       * @example number =>date
       * convertValue:(value,namePath)=>Moment(value)
       * @example YYYY-MM-DD => date
       * convertValue:(value,namePath)=>Moment(value,"YYYY-MM-DD")
       * @example  string => object
       * convertValue:(value,namePath)=>({value,label:value})
       */
      convertValue?: SearchConvertKeyFn;

    transform transform when submitting

    Transform occurs when submitting, generally speaking, it is spit out the data stored in the database to the backend.

    For the convenience of everyone, both ProFormDependency and formRef support transform, which can get the transformed value.

    <ProFormDependency>
      {(value, form) => {
        // value is transformed by transform
        // form's current formRef, you can get the unconverted value
        return ReactNode;
      }}
    </ProFormDependency>

    formRef has several built-in methods to get the converted value, which is also more functional than antd's Form. For details, see the type definition of ProFormInstance.

      /** Get all data formatted by ProForm */
      getFieldsFormatValue?: (nameList?: true) => T;
      /** Get the single data after formatting */
      getFieldFormatValue?: (nameList?: NamePath) => T;
      /** Get the single data after formatting */
      getFieldFormatValueObject?: (nameList?: NamePath) => T;
      /** After validating the fields, return all the data after formatting */
      validateFieldsReturnFormatValue?: (nameList?: NamePath[]) => Promise<T>;
      export type SearchTransformKeyFn = (
        value: any,
        namePath: string[],
        allValues: any,
      ) => string | Record<string, any>;
    
      /**
       * @name Convert value when submitting, generally used to convert value into submitted data
       * @param value field value
       * @param namePath field name
       * @param allValues ​​all fields
       * The new value of the @returns field, if an object is returned, it will be merged with all values ​​once
       *
       * @example {name:[a,b] => {name:a,b }
       * transform: (value,namePath,allValues)=> value.join(",")
       * @example {name: string => { newName:string }
       * transform: (value,namePath,allValues)=> { newName:value }
       * @example {name:moment} => {name:string
       * transform: (value,namePath,allValues)=> value.format("YYYY-MM-DD")
       * @example {name:moment}=> {name:timestamp}
       * transform: (value,namePath,allValues)=> value.valueOf()
       * @example {name:{value,label}} => { name:string}
       * transform: (value,namePath,allValues)=> value.value
       * @example {name:{value,label}} => { valueName,labelName  }
       * transform: (value)=>{valueName:value.value,labelName:value.name}
       */
      transform?: SearchTransformKeyFn;

    In practice, transform is usually used in two ways:

    • 1) Return a primitive (most straightforward and stable): it replaces the submitted value of the current field.
    <ProFormText name="name" transform={(value) => `${value}:suffix`} />
    // Submit: { name: 'xxx:suffix' }
    • 2) Return an object (rename/split/write back nested paths): we recommend building the object by the field name/namePath to avoid “looks correct but submit unchanged”.
    import { set } from '@rc-component/util';
    
    // Write back to the same nested path (recommended)
    <ProFormText
      name={['company', 'name']}
      transform={(value) => set({}, ['company', 'name'], `${value}:suffix`)}
    />
    // Submit: { company: { name: 'xxx:suffix' } }
    
    // Rename key example: name -> displayName
    <ProFormText
      name="name"
      transform={(value) => ({ displayName: value })}
    />
    // Submit: { displayName: 'xxx' } (note: this changes the output shape)

    Note: SearchTransformKeyFn is typed as (value, namePath: string[], allValues) => any, but in some scenarios (nested paths / ProFormList) the runtime value may not always be the “full path array” you expected. That’s why using the component name to build the return object is often safer.

    Code examples

    Label and Form Item Layout

    Except for fixed layout form styles like LightFilter and QueryFilter, other form layouts support configuring three layout methods consistent with antd.

    Grid layout

    Supported in ProForm, SchemaForm, ModalForm, DrawerForm, StepsForm

    Sync submission results to url

    When opening, the url parameters are also set as default values, supports transform, but pay attention to field mapping.

    Components that hijack render functions

    FormItemRender is used specifically to handle code organization using render props, to better aggregate business code with requests, and also to better complete the function of custom form items.

    Forms are essential in middle and backend projects, and are usually accompanied by some non-standard controls and complex form items. At this time, custom form items are needed, and completing a custom form item requires at least the implementation of value and onChange. And if the component is only used once and requires a lot of context parameters, then encapsulation is very ungrateful, so there is this component.

    • Use useControlModel to quickly create a custom form item, while supporting single instance or multiple instances (suitable for encapsulating custom form components and using them in multiple places)
    • Use withFormItemRender to generate a FormItemRender, which can organize code inline (suitable for scenarios where it is used only once or requires many context parameters)
    • Use FormControlRender to convert a form component into a render props form, which is useful in certain situations (such as some design defects of certain third-party components, where the component in render cannot call the onChange method, wrapping it at this time can solve it)

    Of course, you don't necessarily have to use withFormItemRender. Form.Item can be nested, or Form.Item can be nested with noStyle set on the outer layer to organize your code. This will wrap more div elements. If it does not affect your style, you can also use it.

    Using useControlModel

    Start with an official example Customized Form Controls

    FormControlRender

    Using FormControlRender allows you to write code inline and write logic more flexibly. It is suitable for some components wrapped with ProForm.Item or Form.Item.

    Sometimes you need to use Form.Item.useStatus, but you must meet the usage specifications of hooks, which makes development require extraction into a separate component for use, and cannot be used inline, and FormControlRender solves this situation very well.

    FormItemRender & ProFormItemRender

    Using FormItemRender or ProFormItemRender makes it easier to write form items in Form

    Select labelInValue

    How labelInValue changes option payload shape inside ProForm selects.

    Custom request loading state

    While request is loading a centered Spin is rendered by default; use loadingRender to replace it with a Skeleton or any custom node.

    Select local search with request

    When request fetches the full dataset once, set fetchDataOnSearch={false} so that searching filters locally instead of re-triggering the request.

    PropertyDescriptionTypeDefault
    fetchDataOnSearchWhether searching re-triggers request; set false to fetch once on init and filter locallybooleantrue

    ProProvider / ConfigProvider (debug)

    ProForm

    ProForm is a repackaging of antd Form, if you want to customize form elements, ProForm is the same way as antd, you can still customize it with FormItem + custom components. Of course this does not affect other components, QueryFilter and other components as well.

    antd's Form api View here, initialValues related knowledge view here

    ParametersDescriptionTypeDefault
    onFinishCallback event after form is submitted and data validation is successful, supports Promise, will automatically set button loading effect(formData: T) => Promise<boolean | void> | void-
    onResetCallback for clicking the reset button(e) => void-
    submitterSubmitter button-related configurationSubmitterProps<{form?: FormInstance<any>}> | falsetrue
    loadingForm button loading stateboolean-
    loadingRenderCustom render while request is loading, defaults to a centered Spin. Pass Skeleton to align with antd 5.18+ loading styleReactNode | (() => ReactNode)<Spin />
    onLoadingChangeCallback when loading state changes(loading: boolean) => void-
    formRefGet the form instance. ProFormInstance adds formatted-value helpers compared to antd FormReact.MutableRefObject<(ProFormInstance<T> & { nativeElement?: HTMLElement; focus?: () => void }) | undefined> | React.RefObject<(ProFormInstance<T> & &#123; nativeElement?: HTMLElement; focus?: () => void &#125;) | undefined>-
    syncToUrlsync parameters to url, url only supports string, better read documentation before usingboolean | ((values: T, type: 'get' | 'set') => T)-
    syncToUrlAsImportantWhen syncToUrl is true, when the page is displayed, the parameters on the url are mainly used, default is falsebooleanfalse
    extraUrlParamsExtra url parametersRecord<string, any>-
    syncToInitialValuesSync results to initialValues, default is true, if false, data obtained from url will be ignored when form.resetbooleantrue
    omitNilProForm automatically clears null and undefined data, if you have agreed that nil means something, set to false to disable this featurebooleantrue
    dateFormatterAutoFormat data, mainly moment/dayjs forms, supports string and number modes. you also can use formatter function to format datestring | 'string' | 'number' | ((value: dayjs.Dayjs, valueType: string) => string | number) | false'string'
    onInitForm initialization successful, such as layout, label calculation completed(values: T, form: ProFormInstance<any>) => void-
    paramsParameters for initiating network requests, used in conjunction with requestU-
    requestThe parameters of the initiating network request, the return value will be overwritten to initialValuesProRequestData<T, U>-
    isKeyPressSubmitWhether to use carriage return to submitboolean-
    formKeyUsed to control whether the form key is the same, advanced usagestring-
    autoFocusFirstInputAuto focus the first input of the form (only valid for input-like fields)booleantrue
    readonlyWhether read-only mode, effective for all form items, priority lower than readonly of form itemsboolean-
    gridEnable grid mode, default width percentage, use colProps to control width view examplebooleanfalse
    rowPropsPassed to Row when grid mode is enabled, only valid in ProFormGroup, ProFormList, ProFormFieldSetRowProps{ gutter: 8 }
    colPropsPassed to Col when grid mode is enabled, only valid in ProFormGroup, ProFormList, ProFormFieldSetColProps{ xs: 24 }
    (...)Note that LightFilter and QueryFilter only support other antd Form component parameters except wrapperCol | labelCol | layout--

    ProFormInstance

    ProFormInstance adds some capabilities compared to antd's form.

      /**
       * Get all data formatted by ProForm
       * @param nameList boolean
       * @param omitNil boolean whether to ignore null and undefined
       * @returns T
       *
       * @example getFieldsFormatValue() -> returns all data
       * @example getFieldsFormatValue(true) -> returns all data, even if not hosted by form
       * @example getFieldsFormatValue(true, true) -> returns all data, ignoring null and undefined
       */
      getFieldsFormatValue?: (nameList?: true, omitNil?: boolean) => T;
      /**
       * Get a single data formatted by ProForm
       * @param nameList (string|number)[]
       * @returns T
       * @example {a:{b:value}} -> getFieldFormatValue(['a', 'b']) -> value
       */
      /** Get the single data after formatting */
      getFieldFormatValue?: (nameList?: NamePath) => T;
      /**
       * Get the single data formatted by ProForm, including his name
       * @param nameList (string|number)[]
       * @returns T
       * @example
       * {a:{b:value}} -> getFieldFormatValueObject(['a', 'b']) -> {a:{b:value}}
       */
      getFieldFormatValueObject?: (nameList?: NamePath) => T;
      /**
       * Return all data after formatting after field validation
       * @param nameList (string|number)[]
       * @returns T
       *
       * @example validateFieldsReturnFormatValue -> {a:{b:value}}
       */
      validateFieldsReturnFormatValue?: (nameList?: NamePath[]) => Promise<T>;

    ProForm.Group

    ProForm.Group supports group configs like title, tooltip, extra, see ProFormBaseGroupProps for the full type.

    ParametersDescriptionTypeDefault
    titletitleReactNode-
    childrenform control or other elementReact.ReactNode-

    submitter

    While we would prefer not to modify the submitter, it is a common requirement to do so in use, and ProForm's components use the same API to support the requirement.

    ParametersDescriptionTypeDefault
    onSubmitSubmit method(value?: T)=>void-
    onResetReset method(value?: T)=>void-
    searchConfigThe configuration of the search, generally used to configure the text{resetText,submitText}-
    submitButtonPropsThe props for the submit buttonfalse | ButtonProps-
    resetButtonPropsThe props for the reset buttonfalse | ButtonProps-
    renderRendering of custom actionsfalse|((props,dom:JSX[])=>ReactNode[]|ReactNode|false)-

    The second argument to render is the default dom array, the first is the submit button and the second is the reset button.

    <ProForm
      submitter={{
        // Configure the button text
        searchConfig: {
          resetText: 'Reset',
          submitText: 'Submit',
        },
        // Configure the properties of the button
        resetButtonProps: {
          style: {
            // Hide the reset button
            display: 'none',
          },
        },
        submitButtonProps: {},
    
        // Fully customize the entire area
        render: (props, doms) => {
          console.log(props);
          return [
            <button
              type="button"
              key="rest"
              onClick={() => props.form?.resetFields()}
            >
              Reset
            </button>,
            <button
              type="button"
              key="submit"
              onClick={() => props.form?.submit?.()}
            >
              Submit
            </button>,
          ];
        },
      }}
    />

    formRef

    This property is a high-level wrapper made by ProForm based on the original Antd's FormInstance, adding some more convenient methods. Usage is as follows:

    import type { ProFormInstance } from '@ant-design/pro-components';
    import {
      ProForm,
      ProFormDatePicker,
      ProFormText,
    } from '@ant-design/pro-components';
    import { Button, message, Space } from 'antd';
    import moment from 'dayjs';
    import { useRef } from 'react';
    
    const waitTime = (time: number = 100) => {
      return new Promise((resolve) => {
        setTimeout(() => {
          resolve(true);
        }, time);
      });
    };
    
    export default () => {
      const formRef = useRef<ProFormInstance>();
      const onFill = () => {
        formRef?.current?.setFieldsValue({
          name: 'Zhang San',
          company: 'Ant Financial',
        });
      };
    
      const getCompanyName = () => {
        message.info(
          `Company name is "${formRef?.current?.getFieldValue('company')}"`,
        );
      };
    
      const getFormatValues = () => {
        console.log(
          'All data after formatting:',
          formRef.current?.getFieldsFormatValue?.(),
        );
      };
    
      const validateAndGetFormatValue = async () => {
        const values = await formRef.current?.validateFieldsReturnFormatValue?.();
        console.log('Validate form and return all formatted data:', values);
      };
    
      return (
        <ProForm
          title="Create Form"
          formRef={formRef}
          submitter={{
            render: (props, doms) => {
              return [
                ...doms,
                <Button htmlType="button" onClick={onFill} key="edit">
                  One-click Fill
                </Button>,
                <Button htmlType="button" onClick={getCompanyName} key="read">
                  Read Company
                </Button>,
                <Space.Compact key="refs" style={{ display: 'block' }}>
                  <Button htmlType="button" onClick={getFormatValues} key="format">
                    Get all formatted data
                  </Button>
                  <Button
                    htmlType="button"
                    onClick={validateAndGetFormatValue}
                    key="format2"
                  >
                    Validate form and return all formatted data
                  </Button>
                </Space.Compact>,
              ];
            },
          }}
          onFinish={async (values) => {
            await waitTime(2000);
            console.log(values);
            message.success('Submission successful');
            return true;
          }}
        >
          <ProFormText
            width="md"
            name="name"
            label="Contract Customer Name"
            tooltip="Max 24 characters"
            placeholder="Please enter name"
          />
    
          <ProFormText
            width="md"
            name="company"
            label="Our Company Name"
            placeholder="Please enter name"
          />
          <ProFormDatePicker name="date" initialValue={moment('2021-08-09')} />
        </ProForm>
      );
    };

    ProFormInstance adds the following methods based on FormInstance:

    Method NameUsage DescriptionRemarks
    getFieldsFormatValueUsage is the same as FormInstance's getFieldsValue method, returns all formatted data
    getFieldFormatValueUsage is the same as FormInstance's getFieldValue method, returns formatted specified data
    getFieldFormatValueObjectUsage is the same as FormInstance's getFieldValue, returns formatted specified data (including name path)
    validateFieldsReturnFormatValueUsage is the same as FormInstance's validateFields method, returns all formatted data after validation passes