@canard/schema-form

中文文档
0.16.1发布于 2026/09/27MITTypeScriptESM + CJS构建来源已验证近 30 天下载 2871

基于反应的组件库, 以 JSON Schema 为基础, 并为验证器和 UI 组件提供插件系统支持

中文使用文档
文档目录40 节
  1. 概览
  2. 如何使用
  3. 兼容性
  4. 接口
  5. 基本用途
  6. 验证系统
  7. 校验器
  8. 验证器插件使用
  9. 可用的验证器插件
  10. 消息格式出错
  11. 窗体输入系统
  12. 窗体输入定义
  13. 文件管理( 关于 FileAttach)
  14. 窗体输入图
  15. 窗体输入选择过程和优先级
  16. 示例:利用不同的优先事项
  17. 插件系统
  18. 插件注册
  19. 可用的插件
  20. 自定义插件系统
  21. 插件类型结构
  22. 插件注册规则
  23. JSONPointer 路径系统
  24. 标准 JSONPointer
  25. 扩展 JSONPointer
  26. 实际使用实例
  27. 逃逸和脱逃
  28. 带有扩展路径的窗体输入图
  29. 程序节点导航
  30. 高级使用
  31. 自定义窗体布局
  32. 与阵列合作
  33. 带有必要处理的窗体
  34. 自定义窗体类型输入组件
  35. 带监视的有条件球场
  36. 以注射方式注射值
  37. 表格提交管理
  38. 性能优化
  39. 优化提示
  40. 虚拟

@canard/schema-form (英语)

Typescript Javascript React JSON Schema JSON Schema Form


概览

@canard/schema-form 是一个基于反应的组件库,它根据提供的 杰森·斯克马。 。 。 .

JSON Schema验证通过插件系统支持,允许使用各种验证器插件.

通过定义各种 FormTypeInput 它为方便地满足复杂需求提供了灵活性.


如何使用

yarn add @canard/schema-form
# Also install a validator plugin
yarn add @canard/schema-form-ajv8-plugin
# Or for AJV 7.x
yarn add @canard/schema-form-ajv7-plugin
# Or for AJV 6.x
yarn add @canard/schema-form-ajv6-plugin

兼容性

支持的环境 :

  • 节点js 16.11.0或以后
  • 现代浏览器(Chrome 94+,Firefox 93+,Safari 15+)

遗产环境支助: 请使用类似 Babel 的转接器来转换您的目标环境的代码 .

目标包

  • @canard/schema-form
  • @winglet/json-schema

接口

窗体选项

interface FormProps<
  Schema extends JSONSchema = JSONSchema,
  Value extends AllowedValue = InferValueType<Schema>,
> {
  /** JSON Schema to use in this SchemaForm */
  jsonSchema: Schema;
  /** Default value for this SchemaForm */
  defaultValue?: Value;
  /** Apply readOnly property to all FormTypeInput components */
  readOnly?: boolean;
  /** Apply disabled property to all FormTypeInput components */
  disabled?: boolean;
  /** Function called when the value of this SchemaForm changes */
  onChange?: SetStateFn<Value>;
  /** Function called when this SchemaForm is validated */
  onValidate?: Fn<[jsonSchemaError: JSONSchemaError[]]>;
  /** Function called when the form is submitted */
  onSubmit?: Fn<[value: Value], Promise<void> | void>;
  /** Function called when the state of this SchemaForm changes */
  onStateChange?: Fn<[state: NodeStateFlags]>;
  /** FormTypeInput definition list */
  formTypeInputDefinitions?: FormTypeInputDefinition[];
  /** FormTypeInput path mapping */
  formTypeInputMap?: FormTypeInputMap;
  /** Custom form type renderer component */
  CustomFormTypeRenderer?: ComponentType<FormTypeRendererProps>;
  /** Initial validation errors, default is undefined */
  errors?: JSONSchemaError[];
  /** Custom format error function */
  formatError?: FormTypeRendererProps['formatError'];
  /**
   * Error display condition (default: ShowError.DirtyTouched)
   *   - `true`: Always show
   *   - `false`: Never show
   *   - `ShowError.Dirty`: Show when value has changed
   *   - `ShowError.Touched`: Show when input has been focused
   *   - `ShowError.DirtyTouched`: Show when both Dirty and Touched conditions are met
   */
  showError?: boolean | ShowError;
  /**
   * Execute Validation Mode (default: ValidationMode.OnChange | ValidationMode.OnRequest)
   *  - `ValidationMode.None`: Disable validation
   *  - `ValidationMode.OnChange`: Validate when value changes
   *  - `ValidationMode.OnRequest`: Validate on request
   */
  validationMode?: ValidationMode;
  /** Custom ValidatorFactory function */
  validatorFactory?: ValidatorFactory;
  /**
   * Render-level virtualization (deferred mount) for large forms (default: off)
   *  - `true`: Enable with default options
   *  - `VirtualizationOptions`: Enable with custom options
   *  - Requires IntersectionObserver; silently disabled when unavailable (e.g. SSR)
   */
  virtualization?: boolean | VirtualizationOptions;
  /** User-defined context */
  context?: Dictionary;
  /** Child components */
  children?:
    | ReactNode
    | Fn<[props: FormChildrenProps<Schema, Value>], ReactNode>;
}

窗体处理

interface FormHandle<
  Schema extends JSONSchema,
  Value extends AllowedValue = InferValueType<Schema>,
> {
  node?: InferSchemaNode<Schema>;
  focus: Fn<[dataPath: SchemaNode['path']]>;
  select: Fn<[dataPath: SchemaNode['path']]>;
  reset: Fn;
  findNode: Fn<[path: SchemaNode['path']], SchemaNode | null>;
  findNodes: Fn<[path: SchemaNode['path']], SchemaNode[]>;
  getState: Fn<[], NodeStateFlags>;
  setState: Fn<[state: NodeStateFlags]>;
  clearState: Fn;
  getValue: Fn<[], Value>;
  setValue: SetStateFnWithOptions<Value>;
  getErrors: Fn<[], JSONSchemaError[]>;
  getAttachedFilesMap: Fn<[], AttachedFilesMap>;
  validate: Fn<[], Promise<JSONSchemaError[]>>;
  showError: Fn<[visible: boolean]>;
  submit: TrackableHandlerFunction<[], void, { loading: boolean }>;
}

随附文件映射

// Stores File[] by JSONPointer path key (e.g., "/attachment" or "/items/0/file").
type AttachedFilesMap = Map<string, File[]>;

表格儿童方案

interface FormChildrenProps<
  Schema extends JSONSchema,
  Value extends AllowedValue = InferValueType<Schema>,
> {
  node?: InferSchemaNode<Schema>;
  jsonSchema: Schema;
  defaultValue?: Value;
  value?: Value;
  errors?: JSONSchemaError[];
}

基本用途

import { Form, registerPlugin } from '@canard/schema-form';
import { ajvValidatorPlugin } from '@canard/schema-form-ajv8-plugin';

// Register validator plugin (once at app startup)
registerPlugin(ajvValidatorPlugin);

export const App = () => {
  const jsonSchema = {
    type: 'object',
    properties: {
      name: {
        type: 'string',
      },
      age: {
        type: 'number',
      },
    },
  };

  const defaultValues = {
    name: 'Woody',
    age: 30,
  };

  const [value, setValue] = useState<{
    name: string;
    age: number;
  }>(defaultValues);

  return (
    <Form
      jsonSchema={jsonSchema}
      defaultValues={defaultValues}
      onChange={setValue}
    />
  );
};

验证系统

@canard/schema-form 提供基于插件的验证系统。 各种验证器插件可用于JSON Schema验证.

校验器

校验器 Factory 是使用 JSON Schema 并返回校验函数的函数 :

interface ValidatorFactory {
  (schema: JSONSchema): ValidateFunction<any>;
}

type ValidateFunction<Value = unknown> = Fn<
  [data: Value],
  Promise<JSONSchemaError[] | null> | JSONSchemaError[] | null
>;

验证器插件使用

1. 基本插件登记

import { registerPlugin } from '@canard/schema-form';
import { ajvValidatorPlugin } from '@canard/schema-form-ajv8-plugin';

// Register plugin at app startup
registerPlugin(ajvValidatorPlugin);

2. 自定义验证器

当您想要对特定表单使用不同的验证逻辑时 :

import { Form } from '@canard/schema-form';
// The version of ajv must be 8.x because the plugin is based on ajv8.x
// If you want to use ajv7.x, you can use @canard/schema-form-ajv7-plugin
// Or if you want to use ajv6.x, you can use @canard/schema-form-ajv6-plugin
import { createValidatorFactory } from '@canard/schema-form-ajv8-plugin';
import Ajv from 'ajv';

export const CustomValidationForm = () => {
  const validatorFactory = useMemo(() => {
    // Create custom AJV instance
    const customAjv = new Ajv({
      allErrors: true,
      strictSchema: false,
      validateFormats: false,
    });

    // Add custom keywords
    customAjv.addKeyword({
      keyword: 'isEven',
      type: 'number',
      validate: (schema: boolean, data: number) => {
        if (schema === false) return true;
        return data % 2 === 0;
      },
      errors: false,
    });

    // Create ValidatorFactory
    return createValidatorFactory(customAjv);
  }, []);

  const jsonSchema = {
    type: 'object',
    properties: {
      name: { type: 'string', maxLength: 10 },
      evenNumber: { type: 'number', isEven: true, maximum: 100 },
    },
  };

  return <Form jsonSchema={jsonSchema} validatorFactory={validatorFactory} />;
};

3. 通过形式提供的全球验证器

当在多个形式中使用相同的验证逻辑时:

import { FormProvider } from '@canard/schema-form';
// The version of ajv must be 8.x because the plugin is based on ajv8.x
// If you want to use ajv7.x, you can use @canard/schema-form-ajv7-plugin
// Or if you want to use ajv6.x, you can use @canard/schema-form-ajv6-plugin
import { createValidatorFactory } from '@canard/schema-form-ajv8-plugin';
import Ajv from 'ajv';

export const App = () => {
  const validatorFactory = useMemo(() => {
    const customAjv = new Ajv({
      allErrors: true,
      strictSchema: false,
    });

    return createValidatorFactory(customAjv);
  }, []);

  return (
    <FormProvider validatorFactory={validatorFactory}>
      <MyForms />
    </FormProvider>
  );
};

可用的验证器插件

请参考每个插件的 README 详细使用说明 .

消息格式出错

📌 @canard/schema-form 提供了自定义验证消息的特性.

QQ 此特性不包括验证功能. 要使用验证特性,您必须使用下列插件之一或执行自己的验证器:

QQ 如果您需要额外消息格式, 您可以写入 formatError 直接函数并应用它.

验证信息必须遵循下列规则:

  • 验证电文必须在 errorMessages jsonSchema公司的财产.
  • 验证电文必须在 {[keyword]:errorMessage} 格式.
  • 如果您定义 default 键,当关键字不匹配时,将用作默认值.
  • 每个验证消息(errorMessage)可以使用以下表达式被活性值所取代:
    • {key}: 键被替换为在 error.details。 。 。 .
    • {value}: 值被输入中当前输入的值所取代.

基本用途

const schema = {
  type: 'object',
  properties: {
    name: {
      type: 'string',
      minLength: 3,
      maxLength: 10,
      errorMessages: {
        minLength:
          'Name must be at least {limit} characters long. Current value: {value}',
        maxLength:
          'Name must be no more than {limit} characters long. Current value: {value}',
        required: 'Name is a required field.',
      },
    },
  },
  required: ['name'],
};

// AJV8 error example
const error = {
  dataPath: '/name',
  keyword: 'minLength',
  message: 'must NOT have fewer than 3 characters',
  details: {
    limit: 3,
  },
};

// Current value
const value = 'AB';

// Replacement error message
// "Name must be at least 3 characters long. Current value: AB"

多语种支持

const schema = {
  type: 'object',
  properties: {
    name: {
      type: 'string',
      minLength: 3,
      maxLength: 10,
      errorMessages: {
        minLength: {
          ko_KR: '이름은 최소 {limit} 글자 이상이어야 합니다. 현재 값: {value}',
          en_US:
            'Name must be at least {limit} characters long. Current value: {value}',
        },
        maxLength: {
          ko_KR: '이름은 최대 {limit} 글자 이하여야 합니다. 현재 값: {value}',
          en_US:
            'Name must be at most {limit} characters long. Current value: {value}',
        },
        required: {
          ko_KR: '이름은 필수 입력 항목입니다.',
          en_US: 'Name is a required field.',
        },
      },
    },
  },
  required: ['name'],
};

// AJV8 error example
const error = {
  dataPath: '/name',
  keyword: 'minLength',
  message: 'must NOT have fewer than 3 characters',
  details: {
    limit: 3,
  },
};

// Form context
const context = {
  locale: 'en_US',
};

// Current value
const value = 'AB';

// Replacement error message
// "Name must be at least 3 characters long. Current value: AB"

窗体输入系统

@canard/schema-form 提供了强大而灵活的FormTypeInput系统. 这个系统可以让用户精确地控制JSON Schema节点应该使用的输入组件.

窗体输入定义

FormTypeInputDefinition定义了用于JSON Schema节点的输入组件,这些节点匹配特定条件:

type FormTypeInputDefinition<T = unknown> = {
  test: FormTypeTestFn | FormTypeTestObject;
  Component: ComponentType<InferFormTypeInputProps<T>>;
};

每个定义由两个关键部分组成:

  • 测试: 定义 JSON Schema 节点应用此输入组件的条件
  • 构成部分: 满足条件时使用的反射组件

测试函数或测试对象

形式TypeInput的条件可以用函数或对象定义:

type Hint = {
  jsonSchema: JSONSchema;
  type: string;
  format: string;
  formType: string;
  path: string;
  [alt: string]: any;
};
type FormTypeTestFn = Fn<[hint: Hint], boolean>;

type FormTypeTestObject = Partial<{
  type: Array<string>;
  jsonSchema: JSONSchema;
  format: Array<string>;
  formType: Array<string>;
  [alt: string]: any;
}>;
  • 窗体测试Fn: 接收提示对象并返回布尔值的函数。 有助于执行更复杂的条件.
  • 窗体测试对象:允许将条件简单地定义为对象。 譬如说, { type: ['string'], format: ['email'] } 匹配既是字符串类型又是电子邮件格式的节点.

窗体输入道具

选定的部件将获得以下道具:

interface FormTypeInputProps<
  Value extends AllowedValue = any,
  Context extends Dictionary = object,
  WatchValues extends Array<any> = Array<any>,
  Schema extends JSONSchemaWithVirtual = InferJSONSchema<Value>,
  Node extends SchemaNode = InferSchemaNode<Schema>,
> {
  /** JSON Schema for the FormTypeInput Component */
  jsonSchema: Schema;
  /** readOnly state for the FormTypeInput Component */
  readOnly: boolean;
  /** disabled state for the FormTypeInput Component */
  disabled: boolean;
  /** Whether the schema node assigned to the FormTypeInput Component is required */
  required: boolean;
  /** Schema node assigned to the FormTypeInput Component */
  node: Node;
  /** JSON Schema type of this field (e.g., 'string', 'number', 'object', 'array') */
  type: Node['schemaType'];
  /** Name of the schema node assigned to the FormTypeInput Component */
  name: Node['name'];
  /** Path of the schema node assigned to the FormTypeInput Component */
  path: Node['path'];
  /** Whether this field accepts null as a valid value (derived from schema type array including 'null') */
  nullable: Node['nullable'];
  /** Errors of the schema node assigned to the FormTypeInput Component */
  errors: Node['errors'];
  /** Whether to show errors for this field */
  errorVisible: boolean;
  /** Values being watched according to the `computed.watch`(=`&watch`) property defined in JSONSchema */
  watchValues: WatchValues;
  /** Default value for the FormTypeInput Component */
  defaultValue: Value | undefined;
  /** Value for the FormTypeInput Component */
  value: Value | undefined;
  /** onChange handler for the FormTypeInput Component */
  onChange: SetStateFnWithOptions<Value | undefined>;
  /** Attach or detach file(s) to the form's file store */
  onFileAttach: Fn<[file: File | File[] | undefined]>;
  /** Child FormTypeInput Components for this FormTypeInput Component */
  ChildNodeComponents: ChildNodeComponent[];
  /** Placeholder text for input fields */
  placeholder: string | undefined;
  /** CSS class name for styling */
  className: string | undefined;
  /** Style for the FormTypeInput Component */
  style: CSSProperties | undefined;
  /** UserDefinedContext passed to the Form */
  context: Context;
  /** Additional properties can be freely defined */
  [alt: string]: any;
}

文件管理( 关于 FileAttach)

@canard/schema-form 将实际的"文件"对象保存在一个专用的存储中,并只将文件元数据(名称,大小,类型,上一个修改后)写入了计划值. 使用 onFileAttach 在你身边 FormTypeInput ,然后调用 FormHandle.getAttachedFilesMap() ,用于向您的 API 发送文件.

  • 存储数字 : getAttachedFilesMap() 返回时 Map<string, File[]> 其中键是输入的标准 JSONPointer 路径(node.path) (中文(简体) ). 密钥必须是标准 RFC 6901 JSONPointer; 扩展语法(.., (中文(简体) ), ., (中文(简体) ), *)不用于按键.
  • 自动清理数字 :
    • 当窗体被重新初始化或一个节点被移除(包括未挂载)时,该路径上的文件被删除.
    • 有条件的计划(例如, if/then/else, (中文(简体) ), oneOf删除字段也会触发文件清理.

单/多文件格式输入示例

const FileFormTypeInput = ({
  onFileAttach,
  onChange,
  readOnly,
  disabled,
  value,
  jsonSchema,
}: FormTypeInputProps<any>) => {
  const multiple: boolean = jsonSchema?.FormTypeInputProps?.multiple ?? false;
  const accept: string | undefined = jsonSchema?.FormTypeInputProps?.accept;

  const handleChange: React.ChangeEventHandler<HTMLInputElement> = (e) => {
    const fileList = Array.from(e.target.files || []);
    if (fileList.length === 0) {
      onFileAttach(undefined);
      onChange(undefined);
      return;
    }
    if (multiple) {
      onFileAttach(fileList);
      onChange(
        fileList.map((f) => ({
          name: f.name,
          size: f.size,
          type: f.type,
          lastModified: f.lastModified,
        })),
      );
    } else {
      const file = fileList[0];
      onFileAttach(file);
      onChange({
        name: file.name,
        size: file.size,
        type: file.type,
        lastModified: file.lastModified,
      });
    }
  };

  return (
    <div>
      <input
        type="file"
        multiple={multiple}
        accept={accept}
        disabled={readOnly || disabled}
        onChange={handleChange}
      />
      <pre>{JSON.stringify(value, null, 2)}</pre>
    </div>
  );
};

向 API (FormData) 发送文件

import React, { useRef } from 'react';

import { Form, type FormHandle, ValidationMode } from '@canard/schema-form';

const schema = {
  type: 'object',
  properties: {
    title: { type: 'string' },
    attachment: {
      type: 'object',
      FormTypeInput: FileFormTypeInput,
      FormTypeInputProps: { multiple: false, accept: '*/*' },
      properties: {
        name: { type: 'string' },
        size: { type: 'number' },
        type: { type: 'string' },
        lastModified: { type: 'number' },
      },
    },
  },
} as const;

export const UploadForm = () => {
  const ref = useRef<FormHandle<typeof schema>>(null);

  const handleSubmit = async () => {
    const form = ref.current;
    if (!form) return;

    // Send schema value (JSON) together with attached files
    const values = form.getValue();
    const files = form.getAttachedFilesMap(); // AttachedFilesMap

    const body = new FormData();
    body.append(
      'json',
      new Blob([JSON.stringify(values)], { type: 'application/json' }),
    );

    for (const [path, fileList] of files.entries()) {
      if (fileList.length === 1) {
        // Single file: use path directly
        body.append(path, fileList[0]);
      } else {
        // Multiple files: use standard JSONPointer for array keys: "/0", "/1" (do not use brackets [])
        fileList.forEach((file, idx) => body.append(`${path}/${idx}`, file));
      }
    }

    await fetch('/api/upload', { method: 'POST', body });
  };

  return (
    <Form
      ref={ref}
      jsonSchema={schema}
      validationMode={ValidationMode.OnRequest}
      onSubmit={handleSubmit}
    />
  );
};

建议:

  • 对阵列文件使用标准的 JSONPointer 风格密钥( 如 "/ 0" , "/ 1" ); 不使用括号标记 []。 。 。 .
  • 对于大文件,请考虑进度/损坏处理和块化上传.

窗体输入图

您也可以根据路径直接将组件映射到特定字段:

type FormTypeInputMap = {
  [path: string]: ComponentType<FormTypeInputProps>;
};

允许在 JSON Schema 中明确指定特定路径的组件 .

窗体输入选择过程和优先级

当提供窗体时,每个JSON Schema节点的输入组件按以下优先级确定: 1

  1. 直接指定窗体输入: 当一个组件直接通过 FormTypeInput jSON Schema 对象中的属性

    const jsonSchema = {
      type: 'string',
      FormTypeInput: CustomTextInput, // Highest priority
    };
    
  2. 窗体输入图: 当有匹配路径 formTypeInputMap 传到 Form 组件

    <Form
      jsonSchema={jsonSchema}
      formTypeInputMap={{
        '/user/email': EmailInput,
        '/user/profile/avatar': AvatarUploader,
      }}
    />
    
  3. 表单格式输入定义: 第一种定义 formTypeInputDefinitions 数组传递到 Form 符合试验条件的组件

    <Form
      jsonSchema={jsonSchema}
      formTypeInputDefinitions={[
        { test: { type: ['string'], format: ['email'] }, Component: EmailInput },
        { test: { type: ['string'], format: ['date'] }, Component: DatePicker },
      ]}
    />
    
  4. 窗体提供器的窗体输入定义编号: formTypeInputDefinitions 全球通过 FormProvider

  5. 插件的窗体输入定义编号: formTypeInputDefinitions 由注册插件提供

重要:如果多个形式Type输入满足相同条件,则将使用根据以上优先权首先发现的. 当作为数组提供时,定义按其在数组中的出现顺序进行评估.

这种强大的机制使得 高度定制数字 :

  • 覆盖全球默认输入组件
  • 定义特定表格的自定义输入组件
  • 根据路径明确指定特定字段的组件
  • 为JSON Schema属性直接指定组件,以进行最具体的控制

示例:利用不同的优先事项

import { Form, FormProvider, registerPlugin } from '@canard/schema-form';
import { plugin as antd5Plugin } from '@canard/schema-form-antd5-plugin';

// Register plugin (lowest priority)
registerPlugin(antd5Plugin);

export const CustomizedForm = () => {
  // Global FormTypeInput definitions (medium priority)
  const globalDefinitions = [
    {
      test: { type: ['string'] },
      Component: GlobalTextInput,
    },
  ];

  // Form-specific FormTypeInput definitions (higher priority)
  const formDefinitions = [
    {
      test: { type: ['string'], format: ['email'] },
      Component: EmailInput,
    },
  ];

  // Path-based mapping (even higher priority)
  const formInputMap = {
    '/user/address/postalCode': PostalCodeInput,
  };

  // Direct component assignment in JSON Schema (highest priority)
  const jsonSchema = {
    type: 'object',
    properties: {
      user: {
        type: 'object',
        properties: {
          name: { type: 'string' }, // Will use GlobalTextInput
          email: { type: 'string', format: 'email' }, // Will use EmailInput
          address: {
            type: 'object',
            properties: {
              street: { type: 'string' }, // Will use GlobalTextInput
              postalCode: { type: 'string' }, // Will use PostalCodeInput (path mapping)
              country: {
                type: 'string',
                FormTypeInput: CountrySelector, // Will use directly assigned component (top priority)
              },
            },
          },
        },
      },
    },
  };

  return (
    <FormProvider formTypeInputDefinitions={globalDefinitions}>
      <Form
        jsonSchema={jsonSchema}
        formTypeInputDefinitions={formDefinitions}
        formTypeInputMap={formInputMap}
      />
    </FormProvider>
  );
};

这种强大的系统使开发者能够对其形式的各个方面有精细的控制,同时将代码重复降到最低.


插件系统

@canard/schema-form 提供插件系统,允许开发者扩展库并添加自定义功能.

插件注册

import { registerPlugin } from '@canard/schema-form';
import { plugin as ajvValidatorPlugin } from '@canard/schema-form-ajv8-plugin';
import { plugin as antd5Plugin } from '@canard/schema-form-antd5-plugin';

registerPlugin(antd5Plugin);
registerPlugin(AjvValidatorPlugin);

可用的插件

用户界面插件

验证器插件

自定义插件系统

@canard/schema-form 提供插件系统,允许开发者编写并注册自定义插件.

插件类型结构

export interface SchemaFormPlugin {
  /** Form.Group Component */
  FormGroup?: ComponentType<FormTypeRendererProps>;
  /** Form.Label Component */
  FormLabel?: ComponentType<FormTypeRendererProps>;
  /** Form.Input Component */
  FormInput?: ComponentType<FormTypeRendererProps>;
  /** Form.Error Component */
  FormError?: ComponentType<FormTypeRendererProps>;
  /** FormTypeInputDefinition */
  formTypeInputDefinitions?: FormTypeInputDefinition[];
  /** Validator Plugin */
  validator?: ValidatorPlugin;
  /** Formatting Error Function */
  formatError?: FormatError;
}

export interface ValidatorPlugin {
  /** Inject Custom Validator Instance */
  bind: Fn<[instance: any]>;
  /** Validator Factory Function */
  compile: ValidatorFactory;
}

export interface ValidatorFactory {
  (schema: JSONSchema): ValidateFunction<any>;
}

插件注册规则

如果为同一财产注册了多个插件,则适用下列优先权规则:

财产允许的计数优先权规则
FormGroup, (中文(简体) ), FormLabel, (中文(简体) ), FormInput, (中文(简体) ), FormError只有一个最后一个注册组件被应用.
formTypeInputDefinitions多重可合并合并, 并适用最后一个注册项目 .
validator, (中文(简体) ), formatError只有一个最后一个注册项目适用.

这种结构即使在使用多插件时也提供一致的优先权和可扩展性.


JSONPointer 路径系统

@canard/schema-form 使用 JSONPointer (RFC 6901) 在表单图中引用字段。 这提供了解决JSON Schema结构中具体节点的标准化方法.

标准 JSONPointer

JSONPointer遵循了RFC 6901规格:

  • / - 路径分隔符
  • # - URI 碎片标识符前缀
  • '' (空字符串) - 代表整个文档的根指针

路径公约

@canard/schema-form 区分两种路径类型:

路径类型根值示例说明
dataPath'' (空字符串)'', (中文(简体) ), '/user/name'贾森 没有URI碎片前缀的指针字符串. 用于数据值参考.
schemaPath'#''#', (中文(简体) ), '#/properties/user'JSON 指针 URI 碎片 # 前缀。 用于设计定义参考.
// Examples of standard JSONPointer usage
<Form.Render path="/user/name" />        // Access user.name (dataPath style)
<Form.Render path="/user/address/0" />   // Access first item in user.address array

说明数字 : <Form.Render path="..." /> 用途 dataPath 样式路径( 无) # 前缀).

扩展 JSONPointer

重要通知:为了更好地支持复杂的形式情景, @canard/schema-form 执行 扩展的 JSONPointer 语法 这超出了RFC 6901的正式规格。 这些扩展对于增强形式导航和操纵功能是必要的.

用法背景: 扩展 JSONPointer 语法在特定情况下可用:

  • FormTypeInputMap 密钥( 有通配符) *(中文(简体) )
  • computed(单位:千美元)&属性( 有相对路径) .., (中文(简体) ), .(中文(简体) )
  • 程序节点导航 node.find() 方法

说明: 扩展语法为 没有 支持在 <Form.Render path="..." /> 组件,只接受标准的 JSONPointer 路径. node.find() 接受相对导航( a).., (中文(简体) ), .)但不是通配符——使用 findNodes() 用于通配符.

支持以下扩展:

父导航( N)..(中文(简体) )

导航到主要用于计算属性的父节点:

const jsonSchema = {
  type: 'object',
  properties: {
    user: {
      type: 'object',
      properties: {
        type: { type: 'string', enum: ['admin', 'user'] },
        permissions: {
          type: 'array',
          computed: {
            watch: '../type', // Watch parent's type field
            active: "../type === 'admin'", // Show only for admin
          },
        },
      },
    },
  },
};

// Programmatic navigation
const userNode = node.find('user');
const typeNode = userNode.find('../type'); // Navigate to sibling

当前节点( N).(中文(简体) )

引用当前节点 :

const jsonSchema = {
  type: 'object',
  properties: {
    settings: {
      type: 'object',
      computed: {
        watch: '.', // Watch current node
        // Other computed logic
      },
    },
  },
};

// Programmatic navigation
const currentNode = node.find('.'); // Reference current node

通关卡( E)*(中文(简体) )

匹配路径中的任何段, 主要是FormTypeInputMap中使用的. 通配符匹配数组索引和动态对象键(例如, additionalProperties:

const formInputMap = {
  // Array index matching
  '/users/*/name': CustomNameInput, // Matches /users/0/name, /users/1/name, etc.
  '/settings/*/enabled': ToggleInput, // Matches all array item's enabled field

  // Dynamic key matching (additionalProperties)
  '/config/*/value': ConfigValueInput, // Matches /config/theme/value, /config/lang/value, etc.
  '/permissions/*/granted': PermissionToggle, // Matches any permission key
};

// Example schema with additionalProperties
const jsonSchema = {
  type: 'object',
  properties: {
    config: {
      type: 'object',
      additionalProperties: {
        type: 'object',
        properties: {
          value: { type: 'string' },
          enabled: { type: 'boolean' },
        },
      },
    },
  },
};
// '/config/*/value' matches: /config/theme/value, /config/language/value, etc.

说明: 通关卡 * 可在 FormTypeInputMap 键和复数 findNodes() 方法。 它不支持在 <Form.Render path="..." /> 单数 node.find() 方法.

参考文献@(中文(简体) )

引用外部上下文数据转到表格,主要用于计算属性.

重要内容: 这个 @ 符号直接引用上下文对象本身,而不是 JSON 指针路径。 因此,你必须使用 JavaScript 属性访问器语法 (标记或括号标记)而不是 JSON 指针格式:

// ✅ Correct - Property accessor syntax
'@.mode'; // context.mode
'@.user.role'; // context.user.role
'@.permissions?.edit'; // context.permissions?.edit (optional chaining)

// ❌ Wrong - JSON Pointer format
'@/mode'; // This will NOT work
'@/user/role'; // This will NOT work

计算属性中的用法 :

const jsonSchema = {
  type: 'object',
  properties: {
    secretField: {
      type: 'string',
      computed: {
        visible: '@.userRole === "admin"', // Show only for admin users
        readOnly: '@.mode === "view"', // Read-only in view mode
        disabled: '@.permissions?.canEdit === false', // Disabled based on permissions
      },
    },
    combinedCondition: {
      type: 'string',
      computed: {
        // Combine context (@) with form field references (..)
        active: '@.featureEnabled && ../category === "premium"',
      },
    },
  },
};

// Pass context to Form
<Form
  jsonSchema={jsonSchema}
  context={{
    mode: 'edit',
    userRole: 'admin',
    featureEnabled: true,
    permissions: { canEdit: true },
  }}
/>;

主要特点:

  • 上下文值被动更新 - 当上下文变化时, 计算属性重新评价
  • 支持可选的链条( E)?.安全入网
  • 仅存于 computed 属性, 不在 FormTypeInputMap 或者说 Form.Render

⚠️ 建议: 自兹 @ 我们建议使用 可选的链条( e)?.(中文(简体) ) 安全进入巢穴属性。 这样可以防止上下文属性未定义或无效时的运行时出错.

// ✅ Safe access with optional chaining
'@.user?.profile?.name';
'@.settings?.theme ?? "light"';

// ⚠️ May cause errors if context structure changes
'@.user.profile.name';

实际使用实例

const jsonSchema = {
  type: 'object',
  properties: {
    user: {
      type: 'object',
      properties: {
        role: { type: 'string', enum: ['admin', 'user', 'guest'] },
        profile: {
          type: 'object',
          properties: {
            name: { type: 'string' },
            email: { type: 'string' },
            adminSettings: {
              type: 'object',
              computed: {
                watch: '../role',  // Watch sibling field
                active: "../role === 'admin'"  // Show only for admin
              },
              properties: {
                permissions: { type: 'array' }
              }
            }
          }
        },
        addresses: {
          type: 'array',
          items: {
            type: 'object',
            properties: {
              type: { type: 'string', enum: ['home', 'work'] },
              street: { type: 'string' },
              city: { type: 'string' },
              isDefault: {
                type: 'boolean',
                computed: {
                  watch: '../type',  // Watch sibling type
                  active: "../type === 'home'"  // Show only for home addresses
                }
              }
            }
          }
        }
      }
    }
  }
};

// FormTypeInputMap with wildcard
const formInputMap = {
  '/user/profile/name': CustomNameInput,
  '/user/addresses/*/street': AddressInput,      // All address streets
  '/user/addresses/*/isDefault': ToggleInput,    // All isDefault fields
};

// Standard Form.Render (no extended syntax)
<Form.Render path="/user/profile/name" />           // ✅ Standard path
<Form.Render path="/user/addresses/0/street" />     // ✅ Standard path

// ❌ These would NOT work in Form.Render:
// <Form.Render path="/user/profile/.." />           // Extended syntax not supported
// <Form.Render path="/user/addresses/*/city" />     // Extended syntax not supported

逃逸和脱逃

JSONPointer要求特殊字符根据RFC 6901被逃脱:

  • ~0 显示 ~
  • ~1 显示 /

执行情况说明数字 : @canard/schema-form 支持任何符合RFC 6901正式规格的逃逸/逃逸执行。 您可以使用任何符合的库或执行处理字段名中的特殊字符.

// Field name with special characters: "field/with~special"
<Form.Render path="/field~1with~0special" />

带有扩展路径的窗体输入图

使用时 FormTypeInputMap,可以使用通配符语法来匹配任何路径段(阵列索引或动态对象键):

const formInputMap = {
  '/user/email': EmailInput, // Standard path
  '/user/profile/avatar': AvatarUploader, // Nested path

  // Array index wildcard
  '/settings/*/enabled': ToggleInput, // ✅ All array items' enabled field
  '/users/*/permissions': PermissionSelector, // ✅ All user permissions

  // Dynamic key wildcard (additionalProperties)
  '/metadata/*/value': MetadataInput, // ✅ Any metadata key's value
  '/features/*/*': FeatureInput, // ✅ Nested wildcards supported
};

<Form jsonSchema={jsonSchema} formTypeInputMap={formInputMap} />;

匹配示例的通配符 :

  • /users/*/name 匹配 /users/0/name, (中文(简体) ), /users/1/name (阵列指数)
  • /config/*/enabled 匹配 /config/theme/enabled, (中文(简体) ), /config/lang/enabled (对象密钥)
  • /data/*/*/status 匹配任何深嵌状态字段

程序节点导航

export const AdvancedForm = () => {
  return (
    <Form jsonSchema={jsonSchema}>
      {({ node }) => {
        // Using extended syntax in node.find()
        const userRole = node?.find('/user/role');
        const parentNode = node?.find('../'); // ✅ Parent navigation

        return (
          <div>
            <Form.Render path="/user/role" /> {/* Standard path */}
            <Form.Render path="/user/profile" />
          </div>
        );
      }}
    </Form>
  );
};

说明:扩展的JSONPointer语法(.., (中文(简体) ), ., (中文(简体) ), *)是故意将RFC 6901规格扩展,被执行,以提供更强大的形式操控能力. 虽然这些扩展偏离了标准,但它们对于支持现实世界应用中通常需要的复杂形式互动和导航模式至关重要.


高级使用

自定义窗体布局

@canard/schema-form 允许使用 Form.Render 组件 :

import { Form } from '@canard/schema-form';

export const CustomLayoutForm = () => {
  const jsonSchema = {
    type: 'object',
    properties: {
      personalInfo: {
        type: 'object',
        properties: {
          name: { type: 'string', title: 'Name' },
          age: { type: 'number', title: 'Age' },
        },
      },
      contactInfo: {
        type: 'object',
        properties: {
          email: { type: 'string', format: 'email', title: 'Email' },
          phone: { type: 'string', title: 'Phone' },
        },
      },
    },
  };

  return (
    <Form jsonSchema={jsonSchema}>
      <div className="custom-form-layout">
        <div className="section">
          <h3>Personal Information</h3>
          <Form.Render path="/personalInfo/name">
            {({ Input, path, node }) => (
              <div className="form-field">
                <label htmlFor={path}>{node.jsonSchema.title}</label>
                <Input />
              </div>
            )}
          </Form.Render>

          <Form.Render path="/personalInfo/age">
            {({ Input, path, node }) => (
              <div className="form-field">
                <label htmlFor={path}>{node.jsonSchema.title}</label>
                <Input />
              </div>
            )}
          </Form.Render>
        </div>

        <div className="section">
          <h3>Contact Information</h3>
          <Form.Render path="/contactInfo/email">
            {({ Input, path, node }) => (
              <div className="form-field">
                <label htmlFor={path}>{node.jsonSchema.title}</label>
                <Input />
              </div>
            )}
          </Form.Render>

          <Form.Render path="/contactInfo/phone">
            {({ Input, path, node }) => (
              <div className="form-field">
                <label htmlFor={path}>{node.jsonSchema.title}</label>
                <Input />
              </div>
            )}
          </Form.Render>
        </div>
      </div>
    </Form>
  );
};

与阵列合作

该库为处理数组数据提供了强大的特性:

import { Form } from '@canard/schema-form';
import { isArrayNode } from '@canard/schema-form';

export const ArrayForm = () => {
  const jsonSchema = {
    type: 'object',
    properties: {
      users: {
        type: 'array',
        title: 'User List',
        items: {
          type: 'object',
          properties: {
            name: { type: 'string', title: 'Name' },
            email: { type: 'string', format: 'email', title: 'Email' },
          },
        },
      },
    },
  };

  return (
    <Form jsonSchema={jsonSchema}>
      {({ node }) => (
        <div className="array-form">
          <h3>Users</h3>

          {node && isArrayNode(node.find('/users')) && (
            <button onClick={() => node.find('/users').push()} type="button">
              Add User
            </button>
          )}

          <Form.Render path="/users">{({ Input }) => <Input />}</Form.Render>
        </div>
      )}
    </Form>
  );
};

组合分项目自动填充行为

使用时 minItems 在数组中,自动填充行为取决于是否提供默认值:

规则: 当 defaultValue 或者说 schema.default 自动填充项目 已禁用。提供的默认值被作为实际使用,即使其项目少于 minItems。 。 。 .

const jsonSchema = {
  type: 'object',
  properties: {
    // Case 1: No default → minItems auto-fill enabled
    autoFillArray: {
      type: 'array',
      items: { type: 'string', default: 'item' },
      minItems: 3,
    },
    // Result: ['item', 'item', 'item'] (auto-filled to minItems)

    // Case 2: schema.default provided → minItems auto-fill disabled
    schemaDefaultArray: {
      type: 'array',
      items: { type: 'string', default: 'item' },
      minItems: 3,
      default: ['one'],
    },
    // Result: ['one'] (uses default, ignores minItems)

    // Case 3: Empty schema.default → minItems auto-fill disabled
    emptyDefaultArray: {
      type: 'array',
      items: { type: 'string', default: 'item' },
      minItems: 3,
      default: [],
    },
    // Result: [] (uses empty default, ignores minItems)
  },
};

// Using Form's defaultValue prop also controls minItems auto-fill
<Form
  jsonSchema={jsonSchema}
  defaultValue={{
    partialArray: ['a', 'b'], // Only 2 items even if minItems=5
  }}
/>;

矩阵前缀项目支持(JSON Schema 2020-12)

库支持 prefixItems jSON Schema 2020-12号草案,用于基于位置的Tuple验证:

const jsonSchema = {
  type: 'object',
  properties: {
    // Tuple with specific types for each position
    coordinates: {
      type: 'array',
      prefixItems: [
        { type: 'number', title: 'X' },
        { type: 'number', title: 'Y' },
        { type: 'number', title: 'Z' },
      ],
      items: false, // No additional items allowed
    },

    // Mixed tuple: fixed prefix + flexible tail
    person: {
      type: 'array',
      prefixItems: [
        { type: 'string', title: 'First Name' },
        { type: 'string', title: 'Last Name' },
      ],
      items: { type: 'string', title: 'Middle Name' }, // Additional items allowed
    },
  },
};

关键行为 :

  • 基于位置的计划: 每个数组索引使用相应的 prefixItems 计划
  • 退到项目: 指数超出 prefixItems.length 使用该 items 计划
  • 物项:不实: 防止添加超出 prefixItems.length
  • 自动最大项目: 当 items 未定义或 false, (中文(简体) ), maxItems 自动设置为 prefixItems.length

审定要求:

用于 prefixItems 校验, 导入 AJV8 插件, 并配有 2020- 12 草案支持 :

import { plugin } from '@canard/schema-form-ajv8-plugin/2020';

阵列输出过滤器( mitTrailing / 省略Empty)

矩阵计划接受输出过滤器 options。它们只细化阵列 发射 母体传播、根验证和外表面(onChange, (中文(简体) ), getValue(), (中文(简体) ), submit)——而子节点和生 node.value 保留每个项目, 所以预暴露的空输入会保留:

const jsonSchema = {
  type: 'object',
  properties: {
    tags: {
      type: 'array',
      title: 'Tags',
      minItems: 3, // three empty inputs render up front
      items: { type: 'string' },
      options: { omitTrailing: true },
    },
  },
};

// The UI renders 3 inputs; the user fills only the first one.
// onChange / getValue() / validation see: { tags: ['first'] }
// node.find('/tags').value stays ['first', undefined, undefined]

关键行为 :

  • omitTrailing (选入): 删除 跟踪 连续 undefined 仅限项目。 领导和中间 undefined 项保存如此错误 dataPaths 和验证指数保持一致 -- [1, undefined, 2] 已发射.
  • omitEmpty (默认): 将空数组转换为 undefined 在父母传播路径。 筛选顺序是 omitTrailing → omitEmpty,所以一个全空阵列会崩溃到 undefined 在其母体下( 根级形式仍然发出) []) (中文(简体) ).
  • 两个值通道数字 : node.value 保持原生; 精制输出被曝光为 node.normalizedValue验证与精炼值相对应,所以 minItems 只计算已填入的前缀.
  • 重置语义: 重置标记清晰(格式重置、 任何分支重新启动) minItems 空项; 平面 setValue(undefined) 清除每个项目.
  • 自定义输入: 从 node.children (或 ChildNodeComponents),从未从 value.length ——排出值比被设计成的行短.

带有必要处理的窗体

使用 FormHandle 在方案上控制表格:

import React, { useRef } from 'react';

import { Form, FormHandle, ValidationMode } from '@canard/schema-form';

export const ImperativeForm = () => {
  const jsonSchema = {
    type: 'object',
    properties: {
      username: {
        type: 'string',
        minLength: 3,
        title: 'Username',
      },
      password: {
        type: 'string',
        format: 'password',
        minLength: 8,
        title: 'Password',
      },
    },
    required: ['username', 'password'],
  };

  const formRef = useRef<FormHandle<typeof jsonSchema>>(null);

  const handleSubmit = () => {
    formRef.current?.validate();
    const values = formRef.current?.getValue();

    // Process valid form values
    console.log('Form values:', values);
  };

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

  return (
    <div className="login-form">
      <Form
        ref={formRef}
        jsonSchema={jsonSchema}
        validationMode={ValidationMode.OnRequest}
      />

      <div className="form-actions">
        <button onClick={handleSubmit} type="button">
          Submit
        </button>
        <button onClick={handleReset} type="button">
          Reset
        </button>
      </div>
    </div>
  );
};

自定义窗体类型输入组件

您可以通过创建自定义输入组件来扩展形式能力 :

import React from 'react';

import { Form, FormTypeInputDefinition } from '@canard/schema-form';

// Custom date picker component
const DatePickerInput = (props) => {
  const { value, onChange, disabled, readOnly } = props;

  return (
    <div className="custom-date-picker">
      <input
        type="date"
        value={value}
        onChange={(e) => onChange(e.target.value)}
        disabled={disabled}
        readOnly={readOnly}
      />
    </div>
  );
};

// FormTypeInput definition
const datePickerDefinition: FormTypeInputDefinition = {
  test: { format: ['date'] },
  Component: DatePickerInput,
};

export const FormWithCustomInput = () => {
  const jsonSchema = {
    type: 'object',
    properties: {
      birthDate: {
        type: 'string',
        format: 'date',
        title: 'Birth Date',
      },
    },
  };

  return (
    <Form
      jsonSchema={jsonSchema}
      formTypeInputDefinitions={[datePickerDefinition]}
    />
  );
};

带监视的有条件球场

有条件属性的类型

  • watch: 订阅另一个节点的值变化(通过为watchValues prof)
  • derived:根据使用JSONPointer表达式的其他字段引用自动计算值. 使用 &derived 或者说 computed.derived 属性
  • active:确定是否应激活一个节点。 不活动节点不显示, 并且从窗体数据中删除其值
  • visible: 确定是否应显示节点。 隐藏输入保留其值,但不可见
  • pristine:确定一个节点的状态是否应被重置. 如果表达式评价为 true,节点状态将会被重置.
  • readOnly:确定一个节点是否应当只读。 只读输入不能被修改
  • disabled:确定一个节点是否应当被禁用。 无法修改已禁用输入
  • if + 键 oneOf: 显示一个定义中符合条件的图谋定义(仅对象节点)

优先权制度

这个 active, (中文(简体) ), visible, (中文(简体) ), readOnly,以及 disabled 属性遵循此优先级 :

  1. Root jsonSchema 属性( 布尔)
  2. 快捷键属性( E)jsonSchema[{fieldName}],布尔)
  3. 计算的财产(jsonSchema.computed[{fieldName}],布尔 qq字符串)
  4. 非计算财产( E)jsonSchema['&{fieldName}'],布尔 qq字符串)

当多重定义时,较高优先级值高于较低优先级值.

基本示例

import React from 'react';

import { Form } from '@canard/schema-form';

export const ConditionalForm = () => {
  const jsonSchema = {
    type: 'object',
    properties: {
      employmentType: {
        type: 'string',
        enum: ['fulltime', 'parttime', 'contractor'],
        title: 'Employment Type',
        default: 'fulltime',
      },
      commonField: {
        type: 'string',
        title: 'Common Field',
        computed: {
          watch: '../employmentType',
          active: '../employmentType !== null',
          visible: '../employmentType !== null',
        },
      },
    },
    oneOf: [
      {
        computed: {
          if: "./employmentType === 'fulltime'",
        },
        properties: {
          salary: {
            type: 'number',
            title: 'Annual Salary',
          },
          bonus: {
            type: 'number',
            title: 'Bonus',
          },
          benefits: {
            type: 'object',
            title: 'Benefits',
            properties: {
              healthInsurance: {
                type: 'boolean',
                title: 'Health Insurance',
              },
              pension: {
                type: 'boolean',
                title: 'Pension',
              },
            },
          },
          probationPeriod: {
            type: 'number',
            title: 'Probation Period (months)',
            minimum: 0,
            maximum: 12,
          },
        },
      },
      {
        computed: {
          if: "./employmentType === 'parttime'",
        },
        properties: {
          hourlyRate: {
            type: 'number',
            title: 'Hourly Rate',
          },
          workingHours: {
            type: 'number',
            title: 'Weekly Working Hours',
            minimum: 1,
            maximum: 40,
          },
        },
      },
      {
        computed: {
          if: "./employmentType === 'contractor'",
        },
        properties: {
          hourlyRate: {
            type: 'number',
            title: 'Hourly Rate',
          },
          contractType: {
            type: 'string',
            enum: ['hourly', 'project', 'temporary'],
            title: 'Contract Type',
          },
          workingHours: {
            type: 'number',
            title: 'Weekly Working Hours',
            minimum: 1,
            maximum: 168,
            computed: {
              active: '../contractType === "hourly"',
            },
          },
        },
      },
    ],
  };

  return <Form jsonSchema={jsonSchema} />;
};

以注射方式注射值

这个 injectTo 特性可以在窗体字段之间自动传播值。 当源字段的值变化时,可以自动将衍生值注入到表单中的其他字段.

基本用途

定义一个 injectTo 在 JSON Schema 中将对象映射目标路径返回值的处理器函数:

const jsonSchema = {
  type: 'object',
  properties: {
    source: {
      type: 'string',
      title: 'Source',
      injectTo: (value: string) => ({
        '../target': `injected: ${value}`,
      }),
    },
    target: {
      type: 'string',
      title: 'Target (auto-injected)',
    },
  },
};

// When user types "hello" in source field,
// target field automatically becomes "injected: hello"

路径类型

injectTo 支持相对和绝对的 JSON 指针路径 :

相对路径 (来自当前节点):

injectTo: (value) => ({
  '../sibling': value, // Sibling field
  '../nested/child': value, // Nested sibling's child
  '../../uncle': value, // Parent's sibling
});

绝对路径 (从根出):

injectTo: (value) => ({
  '/rootField': value, // Root level field
  '/user/profile/name': value, // Deeply nested field
});

多个目标

同时导入多个字段 :

const jsonSchema = {
  type: 'object',
  properties: {
    fullName: {
      type: 'string',
      injectTo: (value: string) => ({
        '../displayName': value,
        '../searchName': value.toLowerCase(),
        '../initials': value
          .split(' ')
          .map((n) => n[0])
          .join(''),
      }),
    },
    displayName: { type: 'string' },
    searchName: { type: 'string' },
    initials: { type: 'string' },
  },
};

矩阵格式

对于动态路径或顺序事项,使用数组格式:

injectTo: (value: number) => [
  ['/calculations/doubled', value * 2],
  ['/calculations/squared', value * value],
];

访问上下文

处理器接收一个上下文对象, 访问父值、 根形式值和用户定义的上下文 :

import type { InjectToHandler } from '@canard/schema-form';

const handler: InjectToHandler<string> = (value, ctx) => {
  // ctx.dataPath - Current node's JSON Pointer path
  // ctx.schemaPath - Current node's schema path
  // ctx.jsonSchema - Current node's JSON Schema
  // ctx.parentValue - Parent node's value (null if root)
  // ctx.parentJSONSchema - Parent's JSON Schema (null if root)
  // ctx.rootValue - Entire form value
  // ctx.rootJSONSchema - Root JSON Schema
  // ctx.context - User-defined context passed to Form

  // Conditional injection based on parent
  if (ctx.parentValue?.locked) {
    return null; // Skip injection
  }

  // Use root form value for calculations
  return {
    '/total': ctx.rootValue.baseAmount + parseFloat(value),
  };
};

使用窗体上下文

有条件注射时访问全窗体上下文数据:

const jsonSchema = {
  type: 'object',
  properties: {
    secretField: {
      type: 'string',
      injectTo: (value, ctx) => {
        // Only inject for admin users
        if (ctx.context.userRole === 'admin') {
          return { '/adminCopy': value };
        }
        return null;
      },
    },
  },
};

// Pass context to Form
<Form jsonSchema={jsonSchema} context={{ userRole: 'admin' }} />;

预防参考通知

当字段互相注入时,库会自动防止无限循环:

const jsonSchema = {
  type: 'object',
  properties: {
    fieldA: {
      type: 'string',
      injectTo: (value) => ({ '../fieldB': `from A: ${value}` }),
    },
    fieldB: {
      type: 'string',
      injectTo: (value) => ({ '../fieldA': `from B: ${value}` }),
    },
  },
};

// User types in fieldA:
// 1. fieldA → fieldB (injection succeeds)
// 2. fieldB → fieldA (blocked - circular reference prevented)

循环参考预防在同一个宏观任务执行范围内的任何链长(A-B-C-A等)都有效.

处理错误

错误 injectTo 处理者被抓住并包裹在 JSONSchemaError 详细背景:

injectTo: (value) => {
  if (!value) {
    throw new Error('Value is required');
  }
  return { '../target': value };
};

// Error includes:
// - Source node path
// - Target paths attempted
// - Original error message
// - Suggested resolution steps

类型脚本支持

带有通用名的全类型安全 :

import type { InjectToHandler } from '@canard/schema-form';

interface FormContext {
  userId: string;
  permissions: string[];
}

const typedHandler: InjectToHandler<
  string, // Value type
  ParentType, // Parent value type
  RootType, // Root form value type
  FormContext // Context type
> = (value, ctx) => {
  // ctx.context is typed as FormContext
  if (ctx.context.permissions.includes('write')) {
    return { '/audit/lastModifiedBy': ctx.context.userId };
  }
  return null;
};

表格提交管理

@canard/schema-form 提供了有效管理表格提交状态的各种方法.

使用投递

您可以使用 onSubmit 道具 :

import React, { useState } from 'react';

import { Form, JSONSchemaError, isValidationError } from '@canard/schema-form';

export const FormWithSubmit = () => {
  const jsonSchema = {
    type: 'object',
    properties: {
      name: { type: 'string', title: 'Name' },
      email: { type: 'string', format: 'email', title: 'Email' },
    },
    required: ['name', 'email'],
  };

  const [errors, setErrors] = useState<JSONSchemaError[]>([]);

  const handleSubmit = async (value: any) => {
    try {
      // API call or other async operation
      const response = await fetch('/api/submit', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(value),
      });

      if (!response.ok) {
        throw new Error('Submission failed');
      }

      console.log('Submission successful!');
    } catch (error) {
      console.error('Submission error:', error);
    }
  };

  return (
    <Form
      jsonSchema={jsonSchema}
      onSubmit={handleSubmit}
      onValidate={setErrors}
      errors={errors}
    />
  );
};

使用FormSubmit Hook 软件

对于更复杂的提交状态管理,您可以使用 useFormSubmit 钩:

import React, { useRef, useState } from 'react';

import {
  Form,
  FormHandle,
  JSONSchemaError,
  isValidationError,
  useFormSubmit,
} from '@canard/schema-form';

export const AdvancedSubmitForm = () => {
  const jsonSchema = {
    type: 'object',
    properties: {
      name: { type: 'string', title: 'Name' },
      email: { type: 'string', format: 'email', title: 'Email' },
      message: { type: 'string', title: 'Message' },
    },
    required: ['name', 'email'],
  };

  const formRef = useRef<FormHandle<typeof jsonSchema>>(null);
  const [errors, setErrors] = useState<JSONSchemaError[]>([]);

  // Async submission handler
  const handleSubmit = async (value: any) => {
    // Simulate server request
    await new Promise((resolve) => setTimeout(resolve, 2000));
    console.log('Submitted data:', value);
  };

  // Submission state management
  const { submit, loading } = useFormSubmit(formRef);

  const onSubmitClick = async () => {
    setErrors([]); // Clear previous errors

    try {
      await submit();
      alert('Submission completed!');
    } catch (error) {
      if (isValidationError(error)) {
        console.log('Validation error:', error.details);
      } else {
        console.error('Submission error:', error);
      }
    }
  };

  return (
    <div>
      <Form
        ref={formRef}
        jsonSchema={jsonSchema}
        onSubmit={handleSubmit}
        onValidate={setErrors}
        errors={errors}
      />

      <button
        onClick={onSubmitClick}
        disabled={loading}
        style={{
          marginTop: '16px',
          padding: '8px 16px',
          backgroundColor: loading ? '#ccc' : '#1890ff',
          color: 'white',
          border: 'none',
          borderRadius: '4px',
          cursor: loading ? 'not-allowed' : 'pointer',
        }}
      >
        {loading ? 'Submitting...' : 'Submit'}
      </button>

      {loading && (
        <div style={{ marginTop: '8px', color: '#666' }}>
          Processing... Please wait.
        </div>
      )}
    </div>
  );
};

以输入密钥提交

您可以在字符串输入字段中按 Enter 提交表单 :

import React from 'react';

import { Form } from '@canard/schema-form';

export const EnterSubmitForm = () => {
  const jsonSchema = {
    type: 'string',
    title: 'Search Term',
  };

  const handleSubmit = (value: string) => {
    console.log('Search term:', value);
    // Execute search logic
  };

  return (
    <Form
      jsonSchema={jsonSchema}
      onSubmit={handleSubmit}
      placeholder="Enter search term and press Enter"
    />
  );
};

处理提交错误

如何处理提交过程中可能发生的各种错误:

import React, { useRef, useState } from 'react';

import {
  Form,
  FormHandle,
  ValidationMode,
  isValidationError,
  useFormSubmit,
} from '@canard/schema-form';

export const ErrorHandlingForm = () => {
  const jsonSchema = {
    type: 'object',
    properties: {
      username: { type: 'string', minLength: 3, title: 'Username' },
      password: { type: 'string', minLength: 8, title: 'Password' },
    },
    required: ['username', 'password'],
  };

  const formRef = useRef<FormHandle<typeof jsonSchema>>(null);
  const [submitError, setSubmitError] = useState<string>('');

  const handleSubmit = async (value: any) => {
    // Simulate server request
    const response = await fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(value),
    });

    if (!response.ok) {
      const errorData = await response.json();
      throw new Error(errorData.message || 'Login failed.');
    }

    return response.json();
  };

  const { submit, loading } = useFormSubmit(formRef);

  const onSubmitClick = async () => {
    setSubmitError(''); // Clear previous errors

    try {
      await submit();
      alert('Login successful!');
    } catch (error) {
      if (isValidationError(error)) {
        // Validation errors are automatically displayed in the form
        console.log('Validation failed');
      } else {
        // Server errors or network errors
        setSubmitError(error.message || 'An unexpected error occurred.');
      }
    }
  };

  return (
    <div>
      <Form
        ref={formRef}
        jsonSchema={jsonSchema}
        onSubmit={handleSubmit}
        validationMode={ValidationMode.OnRequest}
      />

      {submitError && (
        <div
          style={{
            color: 'red',
            marginTop: '8px',
            padding: '8px',
            backgroundColor: '#fff2f2',
            border: '1px solid #ffcccc',
            borderRadius: '4px',
          }}
        >
          {submitError}
        </div>
      )}

      <button
        onClick={onSubmitClick}
        disabled={loading}
        style={{ marginTop: '16px' }}
      >
        {loading ? 'Logging in...' : 'Login'}
      </button>
    </div>
  );
};

性能优化

图书馆的性能优化有以下功能: 1

  • 复杂形式的懒惰渲染
  • 对部件进行催化,以防止不必要的重交
  • 基于插件的有效验证系统
  • 优化验证时间的可配置验证模式

优化提示

  1. 验证 模式设置数字 : 配置 validationMode 属性以防止不必要的验证.

    // Only validate when the user clicks the submit button
    <Form jsonSchema={jsonSchema} validationMode={ValidationMode.OnRequest} />
    
  2. 缓存窗体输入数字 : 定义常用的自定义形式Type输入外组件来防止不必要的娱乐.

    // Define once globally
    const CUSTOM_INPUTS = [
      { test: { type: ['string'], format: ['email'] }, Component: EmailInput },
    ];
    
    // Reuse within component
    <Form jsonSchema={jsonSchema} formTypeInputDefinitions={CUSTOM_INPUTS} />;
    

虚拟

版本与发布记录

@canard/schema-form 共发布 197 个版本,首次发布于 2024/12/25;过去 12 个月发布了 42 个版本。最新版本附带 npm provenance 构建来源证明,可追溯到公开的 CI 构建记录。

版本发布日期距今
0.16.1latest2026/09/271 个月前
0.16.02026/09/191 个月前
0.15.12026/09/191 个月前
0.14.02026/08/182 个月前
0.13.62026/08/182 个月前
0.13.52026/08/122 个月前

关于 @canard/schema-form 的常见问题

@canard/schema-form 是什么?

基于反应的组件库, 以 JSON Schema 为基础, 并为验证器和 UI 组件提供插件系统支持

如何安装 @canard/schema-form?

在项目目录运行 npm install @canard/schema-form,或使用 pnpm add / yarn add / bun add。它还需要宿主项目提供 peer 依赖:react(>=18 <20)、react-dom(>=18 <20)。npm 7 及以上与 pnpm 8 及以上默认会自动安装必需的 peer 依赖,Yarn 需要手动安装。

@canard/schema-form 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@canard/schema-form 支持 ESM 还是 CommonJS?

同时提供 ES Module 与 CommonJS 入口,import 和 require() 都可以使用,打包工具会自动选择合适的版本。

@canard/schema-form 可以商用吗?

@canard/schema-form 使用 MIT 许可。宽松许可:允许商业使用、修改和再分发,通常只需在分发时保留原版权与许可声明。以上为概括,不构成法律意见,请以许可原文为准。

@canard/schema-form 的下载量有多少?

根据 npm 官方下载统计,@canard/schema-form 在 2026/10/09 – 2026/11/07 这 30 天内被下载 2,871 次,日均约 96 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@canard/schema-form 还在维护吗?

最新版本 0.16.1 发布于 2026/09/27(1 个月前);至今共发布 197 个版本,其中过去 12 个月发布了 42 个;首次发布于 2024/12/25。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。