Form API

Form is the core component of Schema FormX, used to render and manage forms.

Import

import { Form } from '@schema-formx/react';

Props

schema

Form field configuration, can be an array of fields or groups.

type Schema = FieldSchema[] | GroupSchema[];

Type: FieldSchema[] | GroupSchema[]

Required: Yes

Example:

// Field array
const schema: FieldSchema[] = [
  { type: 'string', name: 'name', label: 'Name' },
  { type: 'string', name: 'email', label: 'Email' }
];

// Group array
const schema: GroupSchema[] = [
  {
    key: 'basic',
    props: { title: 'Basic Info' },
    fields: [
      { type: 'string', name: 'name', label: 'Name' }
    ]
  }
];

<Form schema={schema} />

layout

Layout configuration, controls the form's styles and arrangement.

Type: Layout

Required: Yes

Type Definition:

type Layout = {
  formClassName?: string;
  formStyle?: CSSProperties;
  groupClassName?: string;
  groupStyle?: CSSProperties;
  fieldClassName?: string;
  fieldStyle?: CSSProperties;
  labelClassName?: string;
  labelStyle?: CSSProperties;
  tipClassName?: string;
  tipStyle?: CSSProperties;
  controlClassName?: string;
  controlStyle?: CSSProperties;
  groups?: Record<string, SetUI<'group'>>;
  fields?: Record<string, SetUI<'field' | 'label' | 'control' | 'tip'>>;
};

Example:

const layout: Layout = {
  formClassName: 'my-form',
  formStyle: { maxWidth: '600px' },
  fieldClassName: 'form-field',
  fieldStyle: { marginBottom: '16px' },
  labelClassName: 'field-label',
  labelStyle: { fontWeight: '500' }
};

<Form layout={layout} />

components

Custom component mapping, used to register custom form controls and group containers.

Type: object

Required: Yes

Type Definition:

type Components = {
  DefaultControl: FieldComponent;
  DefaultGroup?: GroupComponent;
  [key: string]: FieldComponent | GroupComponent | undefined;
};

Example:

const components = {
  DefaultControl: MyInputComponent,
  CustomSelect: MySelectComponent,
  DefaultGroup: MyGroupComponent
};

<Form components={components} />

data

Controlled data, used to fully control the form data.

Type: Record<string, unknown>

Required: No

Example:

const [formData, setFormData] = useState({ name: '', email: '' });

<Form 
  data={formData}
  onChange={({ data }) => setFormData(data)}
/>

defaultData

Default data, used to set initial values for the form.

Type: Record<string, unknown>

Required: No

Example:

const defaultData = {
  name: 'John',
  email: 'john@example.com'
};

<Form defaultData={defaultData} />

onChange

Data change callback, triggered when form data changes.

Type: function

Required: No

Notes:

  • onChange fires immediately on every field value change (not on blur)
  • Each value change also triggers dependency linkage and field validation
  • The data parameter in the callback is frozen (Object.freeze); shallow-copy before modifying

Example:

<Form 
  onChange={({ data, name, value }) => {
    console.log('Field changed:', name, value);
    console.log('Current data:', data);
  }}
/>

Instance Methods

Get the form instance via ref to call the following methods:

getData

Get the form data. The returned object is frozen with Object.freeze, and directly modifying it will throw a runtime error.

type GetData = () => Record<string, unknown>;

Notes:

  • The returned data is a frozen object and cannot be directly modified
  • If you need to modify the data, create a shallow copy first: const data = { ...formRef.current.getData() }
  • The data parameter in the onChange callback is also frozen

Example:

const formRef = useRef<FormInstance>(null);

const handleGetData = () => {
  const data = formRef.current.getData();
  console.log('Form data:', data);
  // ⚠️ data.name = 'new' will throw TypeError
  // ✅ Correct: const copy = { ...data }; copy.name = 'new';
};

<Form ref={formRef} schema={schema} />

setData

Set the form data.

type SetData = (data: Record<string, unknown>) => void;

Notes:

  • Calling setData will automatically trigger dependency linkage (onDepsChange) for all fields, ensuring linked fields' schema and data remain consistent
  • The passed data will completely replace the existing form data (not a shallow merge)
  • setData does NOT trigger the onChange callback and does NOT trigger validation. If validation is needed, call validate() manually afterwards

Example:

const formRef = useRef<FormInstance>(null);

const handleSetData = () => {
  formRef.current.setData({
    name: 'Jane',
    email: 'jane@example.com'
  });
};

<Form ref={formRef} schema={schema} />

reset

Reset the form to the default data.

type Reset = () => void;

Notes:

  • Calling reset will automatically trigger dependency linkage (onDepsChange) for all fields, ensuring linked fields' schema and data are restored consistently
  • reset does NOT trigger the onChange callback and does NOT trigger validation. If validation is needed, call validate() manually afterwards

Example:

const formRef = useRef<FormInstance>(null);

const handleReset = () => {
  formRef.current.reset();
};

<Form ref={formRef} schema={schema} />

validate

Validate the form, returns the validation result. Supports partial validation and related field error clearing.

type Validate = () => Promise<{
  data: Record<string, unknown>;
  errors: Record<string, string> | null;
}>;

Example:

const formRef = useRef<FormInstance>(null);

const handleSubmit = async () => {
  const { data, errors } = await formRef.current.validate();
  
  if (errors) {
    console.log('Validation failed:', errors);
    return;
  }
  
  console.log('Validation passed:', data);
};

<Form ref={formRef} schema={schema} />

isDirty

Check whether the form data has deviated from the initial values (compared against defaultData).

type IsDirty = () => boolean;

Equivalent value rules: When comparing, certain values are treated as equivalent based on field type:

Field TypeValues treated as equivalent to undefined/null
string'', undefined
number'', null, undefined
boolean'', null, undefined
object{}, undefined
array[], undefined

For example, if a field in defaultData is an empty string '' and the current value is undefined (for a string type field), isDirty() returns false.

Example:

const formRef = useRef<FormInstance>(null);

const handleCheckDirty = () => {
  const isDirty = formRef.current.isDirty();
  console.log('Data has changed:', isDirty);
};

<Form ref={formRef} schema={schema} />

Layout Configuration

SetUI Type

SetUI is a utility type that generates prefixed ClassName and Style configurations.

type SetUI<Prefix extends string> = {
  [Key in `${Prefix}${'ClassName' | 'Style'}`]?: Key extends `${Prefix}Style`
    ? CSSProperties
    : string;
};

Example:

// SetUI<'form'> is equivalent to:
type FormUI = {
  formClassName?: string;
  formStyle?: CSSProperties;
};

Layout configuration controls the form's styles and arrangement. Supports form-level, group-level, and field-level configuration.

Form Level

const layout = {
  formClassName: 'my-form',
  formStyle: { maxWidth: '600px', margin: '0 auto' }
};

Group Level

const layout = {
  groupClassName: 'form-group',
  groupStyle: { marginBottom: '24px', padding: '20px' }
};

Field Level

const layout = {
  fieldClassName: 'form-field',
  fieldStyle: { marginBottom: '16px' },
  labelClassName: 'field-label',
  labelStyle: { fontWeight: '500' },
  controlClassName: 'field-control',
  controlStyle: { width: '100%' }
};

Label Level

const layout = {
  labelClassName: 'field-label',
  labelStyle: {
    display: 'block',
    marginBottom: '6px',
    fontWeight: '500',
    color: '#333'
  }
};

Tip Level

const layout = {
  tipClassName: 'field-tip',
  tipStyle: {
    color: '#ff4d4f',
    fontSize: '12px',
    marginTop: '4px'
  }
};

Control Level

const layout = {
  controlClassName: 'field-control',
  controlStyle: {
    width: '100%'
  }
};

Per-Group Configuration

const layout = {
  groups: {
    basic: {
      groupClassName: 'basic-group',
      groupStyle: { backgroundColor: '#f0f8ff' }
    },
    advanced: {
      groupClassName: 'advanced-group',
      groupStyle: { backgroundColor: '#fff8e1' }
    }
  }
};

Per-Field Configuration

const layout = {
  fields: {
    username: {
      fieldClassName: 'username-field',
      labelClassName: 'username-label',
      controlClassName: 'username-control'
    },
    email: {
      fieldClassName: 'email-field',
      labelClassName: 'email-label',
      controlClassName: 'email-control'
    }
  }
};

Rendered HTML Structure and CSS Attributes

The Form component automatically generates the following HTML structure and CSS data attributes, which can be used for external styling:

<!-- Rendered structure illustration -->
<form class="formClassName">
  <div class="groupClassName">  <!-- Group container -->
    <div class="fieldClassName">
      <label data-required="true" for="fieldName">Field Label</label>
      <div class="controlClassName" data-error="true">
        <!-- Form control -->
        <span class="tipClassName">Error message</span>
      </div>
    </div>
  </div>
</form>

CSS Data Attributes:

AttributeValueDescription
data-required"true"Displayed on label when field has required: true
data-error"true"Displayed on control container when field has validation errors

Usage Example:

/* Add red asterisk to required field labels */
label[data-required]::after {
  content: ' *';
  color: red;
}

/* Add red border to control container with errors */
[data-error="true"] {
  border: 1px solid #ff4d4f;
  border-radius: 4px;
}

Usage Examples

Basic Usage

import { Form } from '@schema-formx/react';
import type { FieldSchema } from '@schema-formx/react';

const schema: FieldSchema[] = [
  { type: 'string', name: 'name', label: 'Name', required: true },
  { type: 'string', name: 'email', label: 'Email', required: true }
];

function MyForm() {
  return <Form schema={schema} />;
}

Controlled Form

import { useState } from 'react';
import { Form } from '@schema-formx/react';

function MyForm() {
  const [formData, setFormData] = useState({ name: '', email: '' });

  return (
    <Form 
      schema={schema}
      data={formData}
      onChange={({ data }) => setFormData(data)}
    />
  );
}

Form Validation

import { useRef } from 'react';
import { Form } from '@schema-formx/react';
import type { FormInstance } from '@schema-formx/react';

function MyForm() {
  const formRef = useRef<FormInstance>(null);

  const handleSubmit = async () => {
    const { data, errors } = await formRef.current.validate();
    if (errors) {
      alert('Validation failed');
      return;
    }
    console.log('Submit data:', data);
  };

  return (
    <>
      <Form ref={formRef} schema={schema} />
      <button onClick={handleSubmit}>Submit</button>
    </>
  );
}