Custom Components

Framework:

Schema FormX allows the use of any UI component as a form control, not tied to a specific component library. This chapter explains how to create and register custom components.

Component Interface

A form control component needs to receive the following basic props:

Required Props

PropTypeDescription
valueanyCurrent field value
onChangefunctionCallback to update the value

Optional Props

Any props defined in the field's props will be passed to the component.

Example:

const CustomComponent = ({ value, onChange, ...props }) => (
  <input
    value={value || ''}
    onChange={(e) => onChange?.(e.target.value)}
    placeholder={props.placeholder}
    className={props.className}
  />
);

Tutorial

Step 1: Create Component

Create an input component that supports custom props:

Step 2: Create Rating Component

const RatingComponent = ({ value, onChange, max = 5 }) => (
  <div style={{ display: 'flex', gap: '4px' }}>
    {Array.from({ length: max }, (_, i) => (
      <span
        key={i}
        onClick={() => onChange?.(i + 1)}
        style={{
          color: i < (value || 0) ? '#fadb14' : '#d9d9d9',
          fontSize: '24px',
          cursor: 'pointer'
        }}
      >

      </span>
    ))}
  </div>
);

Usage:

const ratingSchema = {
  type: 'object',
  name: 'rating',
  label: 'Rating',
  component: 'Rating',
  props: { max: 5 }
};

Step 3: Create Tag Component

const TagInputComponent = ({ value = [], onChange }) => {
  const [inputValue, setInputValue] = useState('');

  const handleAdd = () => {
    if (inputValue.trim() && !value.includes(inputValue.trim())) {
      onChange?.([...value, inputValue.trim()]);
      setInputValue('');
    }
  };

  const handleRemove = (tag) => {
    onChange?.(value.filter(v => v !== tag));
  };

  return (
    <div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: '6px' }}>
        {value.map(tag => (
          <span key={tag} style={{ padding: '4px 8px', background: '#f0f0f0', borderRadius: '4px' }}>
            {tag}
            <button onClick={() => handleRemove(tag)}>×</button>
          </span>
        ))}
      </div>
      <input
        value={inputValue}
        onChange={(e) => setInputValue(e.target.value)}
        onKeyDown={(e) => e.key === 'Enter' && handleAdd()}
      />
      <button onClick={handleAdd}>Add</button>
    </div>
  );
};

Component Design Patterns

State Management

Use the component's own state to manage temporary values:

const ComplexComponent = ({ value, onChange }) => {
  const [localValue, setLocalValue] = useState(value || '');
  
  const handleBlur = () => {
    onChange?.(localValue);
  };
  
  return (
    <input
      value={localValue}
      onChange={(e) => setLocalValue(e.target.value)}
      onBlur={handleBlur}
    />
  );
};

Register Custom Components

Global Registration

Register all custom components via the components property:

const components = {
  DefaultControl: MyInput,      // Default component
  ColorPicker: ColorPicker,     // Color picker
  Rating: Rating,               // Rating component
  TagInput: TagInput            // Tag input
};

<Form components={components} schema={schema} />

Field Specification

Specify components for specific fields via the component property:

const schema = [
  { type: 'string', name: 'username', component: 'MyInput' },
  { type: 'object', name: 'rating', component: 'Rating', props: { max: 5 } }
];

Component Lookup Order

Form Control Lookup Order

  1. If component is specified, look up the component by name from the components object
  2. If component is not specified, default to 'DefaultControl'
  3. If not found, display error message "error component"

Group Container Lookup Order

  1. If GroupSchema.component is specified, look up the component by name from the components object
  2. If not specified, look up DefaultGroup from components
  3. If not found, use the built-in Group component

Note: The Group component is an internal component and is not exported from @schema-formx/react. To customize the group container, register it via components.DefaultGroup or GroupSchema.component.

Common Custom Components

Color Picker

const ColorPicker = ({ value, onChange, colors = [] }) => (
  <div style={{ display: 'flex', gap: '8px' }}>
    {colors.map(color => (
      <div
        key={color}
        onClick={() => onChange(color)}
        style={{
          width: '32px',
          height: '32px',
          backgroundColor: color,
          border: value === color ? '2px solid #333' : '1px solid #ddd',
          cursor: 'pointer'
        }}
      />
    ))}
  </div>
);

// Usage
{
  type: 'string',
  name: 'color',
  label: 'Color',
  component: 'ColorPicker',
  props: {
    colors: ['#ff0000', '#00ff00', '#0000ff']
  }
}

Date Picker

const DatePicker = ({ value, onChange }) => (
  <input
    type="date"
    value={value || ''}
    onChange={(e) => onChange(e.target.value)}
  />
);

// Usage
{
  type: 'string',
  name: 'birthday',
  label: 'Birthday',
  component: 'DatePicker'
}

Third-Party Component Library Integration

Ant Design Integration

import { Select, DatePicker, Upload } from 'antd';

const AntSelect = ({ value, onChange, options, ...props }) => (
  <Select
    value={value}
    onChange={onChange}
    options={options}
    style={{ width: '100%' }}
    {...props}
  />
);

const AntDatePicker = ({ value, onChange, ...props }) => (
  <DatePicker
    value={value}
    onChange={onChange}
    style={{ width: '100%' }}
    {...props}
  />
);

// Register Ant Design components
const components = {
  DefaultControl: MyInput,
  AntSelect,
  AntDatePicker
};

Material-UI Integration

import { TextField, Select, MenuItem } from '@mui/material';

const MuiTextField = ({ value, onChange, ...props }) => (
  <TextField
    value={value || ''}
    onChange={(e) => onChange(e.target.value)}
    fullWidth
    {...props}
  />
);

const MuiSelect = ({ value, onChange, options, ...props }) => (
  <Select
    value={value || ''}
    onChange={(e) => onChange(e.target.value)}
    fullWidth
    {...props}
  >
    {options.map(option => (
      <MenuItem key={option.value} value={option.value}>
        {option.label}
      </MenuItem>
    ))}
  </Select>
);

// Register Material-UI components
const components = {
  DefaultControl: MuiTextField,
  MuiSelect
};

Component Props

Custom components receive additional props configured through schema.props:

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

// ColorPicker receives colors and size props
const ColorPicker = ({ value, onChange, colors, size, ...props }) => {
  // ...
};
  • Use schema.props to pass additional configuration to components
  • All props defined in schema.props are spread onto the component
  • Components automatically receive value and onChange for data binding
  • Use React.memo to optimize rendering performance for complex components