XinForm Dynamic Form

XinForm is a JSON-driven dynamic form component that supports three display modes (normal form, modal form, drawer form), along with advanced features like field dependencies, grid layout, and custom rendering.

Three Layout Modes

ModeDescription
FormNormal form mode, rendered directly on the page
ModalFormModal form mode, opened via a trigger element
DrawerFormDrawer form mode, opened via a trigger element

Basic Usage

Form Mode

import XinForm from '@/components/XinForm';
import type { XinFormRef } from '@/components/XinForm';

const columns = [
  { dataIndex: 'name', title: 'Name', valueType: 'text' },
  { dataIndex: 'email', title: 'Email', valueType: 'text' },
  { dataIndex: 'status', title: 'Status', valueType: 'select', fieldProps: {
    options: [{ label: 'Enabled', value: 1 }, { label: 'Disabled', value: 0 }]
  }},
];

<XinForm
  columns={columns}
  onFinish={async (values) => {
    console.log(values);
  }}
/>

ModalForm Mode

<XinForm
  layoutType="ModalForm"
  columns={columns}
  trigger={<Button type="primary">Create</Button>}
  modalProps={{ title: 'Create User', width: 600 }}
  onFinish={async (values) => {
    await createUser(values);
  }}
/>

DrawerForm Mode

<XinForm
  layoutType="DrawerForm"
  columns={columns}
  trigger={<Button>Edit</Button>}
  drawerProps={{ title: 'Edit User', width: 500 }}
  onFinish={async (values) => {
    await updateUser(values);
  }}
/>

Grid Layout

When grid mode is enabled, form items are arranged using Col layout, with each item defaulting to 12 columns (half a row).

<XinForm
  grid
  columns={columns}
  rowProps={{ gutter: 16 }}
  colProps={{ span: 8 }}  // Global default: 8 columns per item (3 per row)
/>

You can also specify colProps for individual fields, which will be merged with the global colProps:

const columns = [
  { dataIndex: 'name', title: 'Name', colProps: { span: 24 } },  // Full row
  { dataIndex: 'email', title: 'Email', colProps: { span: 12 } },
  { dataIndex: 'phone', title: 'Phone', colProps: { span: 12 } },
];

Field Dependencies

Use the dependency configuration to implement field-level interactions, including show/hide, enable/disable, and dynamic properties.

const columns = [
  { dataIndex: 'type', title: 'Type', valueType: 'select', fieldProps: {
    options: [
      { label: 'Text', value: 'text' },
      { label: 'Number', value: 'number' },
    ]
  }},
  {
    dataIndex: 'value',
    title: 'Value',
    valueType: 'text',
    dependency: {
      dependencies: ['type'],
      // Only visible when type is 'text'
      visible: (values) => values.type === 'text',
      // Dynamically modify fieldProps
      fieldProps: (values) => ({
        placeholder: values.type === 'text' ? 'Enter text' : 'Enter number',
      }),
    },
  },
];

FieldDependency Configuration

PropertyTypeDescription
dependencies(keyof T)[]Array of dependent field names
visible(values: Partial<T>) => booleanControl visibility based on dependent values
disabled(values: Partial<T>) => booleanControl disabled state based on dependent values
fieldProps(values: Partial<T>) => Record<string, any>Dynamically modify fieldProps based on dependent values

FormColumn Configuration

Each columns item supports all Ant Design Form.Item properties, plus the following extended properties:

PropertyTypeRequiredDescription
dataIndexstring | number | (string | number)[]YesField name, corresponds to the key in form values
valueTypeFieldValueNoField type, determines which input component to render
titlestringNoField label, also used as the default placeholder
fieldPropsobjectNoProps passed to the input component, type inferred from valueType
fieldRender(form: FormInstance) => ReactNodeNoCustom field render function, replaces the default FieldRender
colPropsColPropsNoColumn properties for this field in grid layout, merged with global colProps
dependencyFieldDependencyNoField dependency configuration

Supported valueTypes

valueTypeComponentDescription
textInputText input
passwordInput.PasswordPassword input
textareaInput.TextAreaMulti-line text
digitInputNumberNumber input
moneyInputNumberCurrency input
selectSelectDropdown select
treeSelectTreeSelectTree select
cascaderCascaderCascading select
radioRadio.GroupRadio group
radioButtonRadio.Group (button)Radio button group
checkboxCheckbox.GroupCheckbox group
switchSwitchToggle switch
rateRateStar rating
sliderSliderSlider
dateDatePickerDate picker
dateTimeDatePicker (showTime)Date-time picker
dateRangeDatePicker.RangePickerDate range picker
timeTimePickerTime picker
timeRangeTimePicker.RangePickerTime range picker
weekDatePicker (week)Week picker
monthDatePicker (month)Month picker
quarterDatePicker (quarter)Quarter picker
yearDatePicker (year)Year picker
imageImageUploaderImage upload
colorColorPickerColor picker
iconIconSelectorIcon selector
userUserSelectorUser selector

Submitter Bar Configuration

Hide the Submitter Bar

<XinForm
  columns={columns}
  submitter={{ render: false }}
/>

Custom Button Text

<XinForm
  columns={columns}
  submitter={{
    submitText: 'Save',
    resetText: 'Clear',
    closeText: 'Cancel',
  }}
/>

Custom Button Props

<XinForm
  columns={columns}
  submitter={{
    submitButtonProps: { danger: true, disabled: true },
    resetButtonProps: { type: 'dashed' },
  }}
/>

Fully Custom Submitter Bar

<XinForm
  columns={columns}
  submitter={{
    render: (buttons, formRef) => (
      <Space>
        {buttons.submit}
        <Button onClick={() => console.log('draft')}>Save Draft</Button>
        {buttons.reset}
      </Space>
    ),
  }}
/>

SubmitterProps Configuration

PropertyTypeDescription
renderfalse | ((buttons, formRef) => ReactNode)Set to false to hide the bar, or pass a custom render function
submitTextstring | ReactNodeSubmit button text, defaults to t('xinForm.submit')
resetTextstring | ReactNodeReset button text, defaults to t('xinForm.reset')
closeTextstring | ReactNodeClose button text, defaults to t('xinForm.cancel')
submitButtonPropsButtonPropsAdditional props for the submit button
resetButtonPropsButtonPropsAdditional props for the reset button
closeButtonPropsButtonPropsAdditional props for the close button

Note: Close button is not displayed in Form mode; it is displayed in ModalForm / DrawerForm.

XinFormRef Instance Methods

Use formRef to access the form instance, which supports all Ant Design Form methods plus the following extensions:

MethodDescription
open()Open the modal/drawer (ModalForm / DrawerForm only)
close()Close the modal/drawer (ModalForm / DrawerForm only)
isOpen()Get the open state of the modal/drawer
setLoading(loading: boolean)Set the loading state of the submit button
import { useRef } from 'react';
import type { XinFormRef } from '@/components/XinForm';

const formRef = useRef<XinFormRef>(null);

// Open modal from outside
<Button onClick={() => formRef.current?.open()}>Open Form</Button>

<XinForm
  formRef={formRef}
  layoutType="ModalForm"
  columns={columns}
/>

// Access form instance methods
const values = formRef.current?.getFieldsValue();
formRef.current?.setFieldsValue({ name: 'Default Name' });
formRef.current?.setLoading(true);

XinFormProps Full Reference

PropertyTypeDefaultDescription
columnsFormColumn<T>[]-Form column configuration (required)
layoutType'Form' | 'ModalForm' | 'DrawerForm''Form'Form layout type
gridbooleanfalseEnable grid layout
rowPropsRowProps-Props passed to Row when grid is enabled
colPropsColProps{ span: 12 }Default Col props for each form item
onFinish(values: T) => Promise<boolean | void>-Form submit callback
formRefRefObject<XinFormRef>-Form instance reference
formFormInstance<T>-Externally provided form instance
modalPropsModalProps-Modal configuration for ModalForm (excluding open)
drawerPropsDrawerProps-Drawer configuration for DrawerForm (excluding open)
triggerReactNode-Trigger element for opening modal/drawer
submitterSubmitterProps-Submitter bar configuration

Also inherits all properties from FormProps<T> except onFinish, so you can directly pass initialValues, labelCol, etc.

Complete Example

The following example demonstrates a user creation drawer form with grid layout and field dependencies:

import { useRef } from 'react';
import XinForm from '@/components/XinForm';
import type { XinFormRef } from '@/components/XinForm';
import { Button, message } from 'antd';

const userColumns = [
  {
    dataIndex: 'username',
    title: 'Username',
    valueType: 'text',
    colProps: { span: 12 },
    fieldProps: { maxLength: 50 },
    rules: [{ required: true, message: 'Please enter a username' }],
  },
  {
    dataIndex: 'email',
    title: 'Email',
    valueType: 'text',
    colProps: { span: 12 },
    rules: [
      { required: true, message: 'Please enter an email' },
      { type: 'email', message: 'Invalid email format' },
    ],
  },
  {
    dataIndex: 'role',
    title: 'Role',
    valueType: 'select',
    colProps: { span: 12 },
    fieldProps: {
      options: [
        { label: 'Admin', value: 'admin' },
        { label: 'User', value: 'user' },
      ],
    },
  },
  {
    dataIndex: 'role_extra',
    title: 'Admin Note',
    valueType: 'textarea',
    colProps: { span: 24 },
    dependency: {
      dependencies: ['role'],
      visible: (values) => values.role === 'admin',
    },
  },
];

function UserCreate() {
  const formRef = useRef<XinFormRef>(null);

  const handleCreate = async (values: any) => {
    const result = await createUser(values);
    if (result) {
      message.success('Created successfully');
      return true;  // Return true to close the drawer
    }
    return false;
  };

  return (
    <>
      <Button type="primary" onClick={() => formRef.current?.open()}>
        Create User
      </Button>

      <XinForm
        formRef={formRef}
        layoutType="DrawerForm"
        grid
        rowProps={{ gutter: 16 }}
        columns={userColumns}
        onFinish={handleCreate}
        drawerProps={{
          title: 'Create User',
          width: 600,
        }}
        submitter={{
          submitText: 'Create',
          resetText: 'Clear',
        }}
      />
    </>
  );
}