自定义组件

Framework:

概述

Schema FormX 支持完全自定义表单控件,您可以使用任意 UI 组件作为表单控件。本章节将详细介绍如何创建和使用自定义组件。

自定义组件接口

自定义组件需要实现以下接口:

interface CustomComponentProps {
  // 当前字段值
  value: any;
  // 值变化回调
  onChange: (value: any) => void;
  // 其他自定义属性
  [key: string]: any;
}

基础示例

const MyInput = ({ value, onChange, placeholder, ...props }) => (
  <input
    value={value || ''}
    onChange={(e) => onChange(e.target.value)}
    placeholder={placeholder}
    style={{
      padding: '8px',
      border: '1px solid #ccc',
      borderRadius: '4px'
    }}
    {...props}
  />
);

自定义组件示例

注册自定义组件

全局注册

通过 components 属性注册所有自定义组件:

const components = {
  DefaultControl: MyInput,      // 默认组件
  ColorPicker: ColorPicker,     // 颜色选择器
  Rating: Rating,               // 评分组件
  TagInput: TagInput            // 标签输入
};

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

字段指定

通过 component 属性为特定字段指定组件:

const schema: FieldSchema[] = [
  {
    type: 'string',
    name: 'color',
    label: '颜色',
    component: 'ColorPicker',  // 指定使用 ColorPicker 组件
    props: {
      colors: ['#ff0000', '#00ff00', '#0000ff']
    }
  }
];

组件查找顺序

表单控件查找顺序

  1. 如果指定了 component,从 components 对象中查找对应名称的组件
  2. 如果未指定 component,默认使用 'DefaultControl'
  3. 如果都未找到,显示错误提示 "error component"

分组容器查找顺序

  1. 如果指定了 GroupSchema.component,从 components 对象中查找对应名称的组件
  2. 如果未指定,从 components 中查找 DefaultGroup
  3. 如果都未找到,使用内置 Group 组件

注意Group 组件是内部组件,不从 @schema-formx/react 导出。如需自定义分组容器,请通过 components.DefaultGroupGroupSchema.component 注册。

常用自定义组件

颜色选择器

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>
);

评分组件

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

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

标签输入

const TagInput = ({ value = [], onChange }) => {
  const [input, setInput] = useState('');

  const handleKeyDown = (e) => {
    if (e.key === 'Enter' && input.trim()) {
      onChange([...value, input.trim()]);
      setInput('');
    }
  };

  const removeTag = (index) => {
    onChange(value.filter((_, i) => i !== index));
  };

  return (
    <div>
      <div style={{ display: 'flex', flexWrap: 'wrap', gap: '8px' }}>
        {value.map((tag, index) => (
          <span key={index}>
            {tag}
            <button onClick={() => removeTag(index)}>×</button>
          </span>
        ))}
      </div>
      <input
        value={input}
        onChange={(e) => setInput(e.target.value)}
        onKeyDown={handleKeyDown}
      />
    </div>
  );
};

日期选择器

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

文件上传

const FileUpload = ({ value, onChange, accept }) => {
  const handleChange = (e) => {
    const file = e.target.files[0];
    if (file) {
      onChange(file);
    }
  };

  return (
    <div>
      <input type="file" accept={accept} onChange={handleChange} />
      {value && <span>{value.name}</span>}
    </div>
  );
};

组件设计模式

状态管理

使用组件自身的 state 管理临时值,在失焦时提交:

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}
    />
  );
};

与第三方组件库集成

Ant Design 集成

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}
  />
);

const components = {
  DefaultControl: MyInput,
  AntSelect,
  AntDatePicker
};

Material-UI 集成

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>
);

组件 Props 传递

自定义组件可以接收通过 props 配置的额外属性:

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

// ColorPicker 组件将接收 colors 和 size 属性
const ColorPicker = ({ value, onChange, colors, size, ...props }) => {
  // ...
};

最佳实践

  1. 保持简单:自定义组件应该专注于单一功能
  2. 处理空值:确保组件能正确处理 undefinednull
  3. 类型安全:使用 TypeScript 定义组件的 Props 类型
  4. 性能优化:对于复杂组件,使用 React.memo 优化性能
  5. 可访问性:确保组件支持键盘操作和屏幕阅读器