Field API

Field API defines the configuration options for form fields, including field types, validation rules, and dependency linkage.

FieldSchema Type

type FieldSchema = {
  type?: FieldType;
  name: string;
  label: string;
  required?: boolean;
  component?: string;
  props?: Record<string, unknown>;
  hidden?: boolean;
  deps?: string[];
  onDepsChange?: OnDepsChange;
  validator?: Validator;
};

Property Details

type

Field type, determines which default component to use.

type FieldType = 'string' | 'number' | 'boolean' | 'object' | 'array';

Type: FieldType

Required: No

Default: 'string'

name

Field name, used for data binding.

Type: string

Required: Yes

label

Field display name.

Type: string

Required: Yes

required

Whether the field is required.

Type: boolean

Required: No

Default: false

Example:

{ 
  name: 'username', 
  label: 'Username', 
  required: true
}

Empty value rules: When required is true, the following values are judged as empty:

ValueIs Empty
null
undefined
'' (empty string)
[] (empty array)
{} (empty object)
false❌ Not empty
0❌ Not empty

Note: The required validation error message is hardcoded in Chinese as ${label}不能为空 (e.g., "Username cannot be empty"). For English error messages, use a custom validator function to override the default required validation.

component

Custom component name.

Type: string

Required: No

Component Lookup Order:

// Component lookup priority: component → DefaultControl → error
if (component) {
  // 1. First lookup: custom registered component
  Component = components[component];
  if (!Component) {
    console.error(`Component "${component}" not found`);
    Component = components['DefaultControl'];  // 2. Fallback: DefaultControl
  }
} else {
  // No component specified, use DefaultControl
  Component = components['DefaultControl'];
}

props

Component props, passed to the field component.

Type: Record<string, unknown>

Required: No

hidden

Whether the field is hidden. Setting hidden: true only hides the field visually (display: none); the field is still rendered and remains in the form data. Hidden fields still participate in validation — if a hidden field has required: true, it will still trigger validation errors. Use conditional required or a custom validator to skip validation for hidden fields.

Type: boolean

Required: No

deps

Array of dependent field names.

Type: string[]

Required: No

onDepsChange

Dependency change callback, triggered when a dependent field's value changes. Supports both synchronous and asynchronous functions.

type OnDepsChange = (params: OnDepsChangeParams) => OnDepsChangeReturn | Promise<OnDepsChangeReturn>;

Parameters:

ParameterTypeDescription
depsArray<any>Current values of dependent fields, in the same order as deps declaration
schemaFieldSchemaCurrent field's configuration (deep copy)
namestringCurrent field name
valueunknownCurrent field value
dataReadonly<Record<string, unknown>>Latest form data (read-only)
isInitialTriggerbooleanWhether this is triggered during form initialization
triggerstringThe field name that triggered this callback
triggerCountnumberNumber of times this field was triggered by trigger
totalTriggerCountnumberTotal number of times this field was triggered by all dependent fields

Return Value:

PropertyTypeDescription
patchRecord<string, unknown>Form data patch, merged into existing data
schemaFieldSchemaUpdated field configuration, replaces existing config

Notes:

  • Maximum chain trigger depth is 20 times (to prevent infinite loops)
  • deps and onDepsChange properties cannot be modified within the callback
  • data parameter is read-only, do not modify directly

Example:

{
  name: 'city',
  label: 'City',
  deps: ['province'],
  onDepsChange: async ({ deps, schema }) => {
    const province = deps[0];
    if (!province) {
      return { schema: { ...schema, hidden: true } };
    }
    const cities = await getCitiesByProvince(province);
    return {
      patch: { city: '' },
      schema: { ...schema, hidden: false, props: { options: cities } }
    };
  }
}

validator

Custom validation function for field validation. Supports both synchronous and asynchronous validation.

type Validator = (params: ValidatorParams) => ValidatorReturn | Promise<ValidatorReturn>;

Parameters:

ParameterTypeDescription
valueunknownCurrent field value
labelstringField display name
dataReadonly<Record<string, unknown>>Entire form data

Return Value:

  • undefined: Validation passed
  • string: Validation failed, returns error message

Exception Handling: Exceptions thrown by the validator are automatically caught and converted to error messages:

{
  name: 'field',
  label: 'Field',
  validator: ({ value }) => {
    if (value === 'invalid') {
      throw new Error('Invalid value');
    }
    return undefined;
  }
}
// Exception converts to: errors: { field: 'Invalid value' }

Async Validation Example:

{
  name: 'username',
  label: 'Username',
  validator: async ({ value, label }) => {
    if (!value) return `${label} is required`;
    const exists = await checkUsernameExists(value);
    if (exists) return `${label} is already taken`;
    return undefined;
  }
}

Type Definitions

FieldType

type FieldType = 'string' | 'number' | 'boolean' | 'object' | 'array';

ValidatorParams

type ValidatorParams = {
  value: unknown;
  label: string;
  data: Readonly<Record<string, unknown>>;
};

ValidatorReturn

type ValidatorReturn = undefined | string;

OnDepsChangeParams

type OnDepsChangeParams = {
  deps: Array<any>;
  schema: FieldSchema;
  name: string;
  value: unknown;
  data: Readonly<Record<string, unknown>>;
  isInitialTrigger: boolean;
  trigger: string;
  triggerCount: number;
  totalTriggerCount: number;
};

OnDepsChangeReturn

type OnDepsChangeReturn = Partial<{
  patch: Record<string, unknown>;
  schema: FieldSchema;
}>;

Return Value Behavior:

  • patch: Partially merge into form data using cloneDeep
  • schema: Completely replace the field's schema configuration

## Usage Examples

### Basic Fields

```tsx
const schema: FieldSchema[] = [
  { type: 'string', name: 'username', label: 'Username', required: true },
  { type: 'number', name: 'age', label: 'Age' },
  { type: 'boolean', name: 'agree', label: 'Agree to Terms' }
];

Custom Component

const schema: FieldSchema[] = [
  {
    type: 'string',
    name: 'color',
    label: 'Color',
    component: 'ColorPicker',
    props: { colors: ['#ff0000', '#00ff00', '#0000ff'] }
  }
];

Validation

const schema: FieldSchema[] = [
  {
    type: 'string',
    name: 'email',
    label: 'Email',
    required: true,
    validator: ({ value, label }) => {
      if (!value) return `${label} is required`;
      if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return `${label} format is incorrect`;
      return undefined;
    }
  }
];

Dependency Linkage

const schema: FieldSchema[] = [
  {
    type: 'string',
    name: 'province',
    label: 'Province',
    props: { options: ['Guangdong', 'Zhejiang', 'Jiangsu'] }
  },
  {
    type: 'string',
    name: 'city',
    label: 'City',
    deps: ['province'],
    onDepsChange: async ({ deps, schema }) => {
      const cities = await getCitiesByProvince(deps[0]);
      return {
        schema: {
          ...schema,
          props: { options: cities }
        }
      };
    }
  }
];