본문으로 건너뛰기

API Reference

<Form>

스키마 기반 폼 컴포넌트. JSON Schema를 받아 전체 노드 트리 라이프사이클을 관리합니다. defaultValue / onChange 또는 명령형 ref API를 통해 제어/비제어 사용 방식 모두를 지원합니다.

Props

PropTypeDefaultDescription
jsonSchemaJsonSchemaRequired. JSON Schema defining the form structure
defaultValueValueundefinedInitial form value
onChange(value: Value) => voidCalled when any field value changes
onSubmit(value: Value) => Promise<void> | voidCalled on valid form submit (after validation passes)
onValidate(errors: JsonSchemaError[]) => voidCalled after each validation run
onStateChange(state: NodeStateFlags) => voidCalled when dirty/touched/validated state changes
showErrorboolean | ShowErrorShowError.DirtyTouchedWhen to display validation errors
validationModeValidationModeOnChange | OnRequestWhen validation runs
validatorFactoryValidatorFactoryplugin defaultOverride validator factory per form
readOnlybooleanfalseApply readOnly to all inputs
disabledbooleanfalseApply disabled to all inputs
errorsJsonSchemaError[]undefinedInject external validation errors
formTypeInputDefinitionsFormTypeInputDefinition[][]Form-level input component definitions
formTypeInputMapFormTypeInputMap{}Path-based input component overrides
CustomFormTypeRendererComponentType<FormTypeRendererProps>plugin defaultOverride the entire field renderer
formatErrorFormatErrorplugin defaultCustom error message formatter
contextDictionary{}User-defined context, available in all input components
childrenReactNode | (props: FormChildrenProps) => ReactNodeCustom layout or render prop
refRef<FormHandle>Imperative handle

ShowError Values

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

ShowError.Dirty // show when node.dirty === true
ShowError.Touched // show when node.touched === true
ShowError.DirtyTouched // show when dirty AND touched (default)
// Also accepts: true (always show), false (never show)

ValidationMode Values

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

ValidationMode.None // disable validation
ValidationMode.OnChange // validate on every value change
ValidationMode.OnRequest // validate only when validate() called explicitly
// Combine with bitwise OR: ValidationMode.OnChange | ValidationMode.OnRequest

FormHandle (ref API)

useRef<FormHandle<typeof schema>>(null)로 획득하고 ref prop에 전달합니다. 마운트 후 모든 메서드를 안전하게 호출할 수 있습니다.

interface FormHandle<Schema, Value> {
node: InferSchemaNode<Schema> | undefined; // root node
getValue(): Value;
setValue(value: Value | ((prev: Value) => Value), options?: SetValueOption): void;
getState(): NodeStateFlags;
setState(state: NodeStateFlags): void;
clearState(): void;
getErrors(): JsonSchemaError[];
validate(): Promise<JsonSchemaError[]>;
showError(visible: boolean): void;
focus(path: string): void; // focuses input at path
select(path: string): void; // selects input at path
findNode(path: string): SchemaNode | null;
findNodes(path: string): SchemaNode[];
reset(): void; // remounts the form (re-reads jsonSchema + defaultValue)
submit: TrackableHandlerFunction; // callable + subscribe(listener) + pending
getAttachedFilesMap(): AttachedFilesMap;
}
// Usage
const formRef = useRef<FormHandle<typeof schema>>(null);

const errors = await formRef.current?.validate();
formRef.current?.setValue({ name: 'Alice' });
formRef.current?.focus('/email');
formRef.current?.reset();

SetValueOption

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

// Bitwise flags (can be combined)
SetValueOption.Overwrite // replace entire value (default)
SetValueOption.Merge // deep merge with current value
SetValueOption.Isolate // compute dependencies synchronously

Form Sub-Components

All sub-components require a <Form> ancestor and use its internal context.

Form.Group

완전한 필드 그룹을 렌더링합니다: 레이블 + 입력 + 오류. 플러그인의 FormGroup 컴포넌트를 사용하여 Form.Label, Form.Input, Form.Error를 함께 감쌉니다.

interface FormGroupProps<Value> extends ChildNodeComponentProps {
path?: string;
FormTypeInput?: ComponentType<FormTypeInputProps<Value>>;
FormTypeRenderer?: ComponentType<FormTypeRendererProps>;
Wrapper?: ComponentType<PropsWithChildren<Dictionary>>;
// + any extra props forwarded to the input component
className?: string;
style?: CSSProperties;
placeholder?: string;
readOnly?: boolean;
disabled?: boolean;
}
<Form.Group path="/email" />
<Form.Group path="/birthDate" FormTypeInput={DatePicker} />
<Form.Group path="/address" Wrapper={({ children }) => <fieldset>{children}</fieldset>} />

Form.Label

Renders only the label using the plugin's FormLabel component.

<Form.Label path="/email" className="required-label" />

Form.Input

Renders only the input using the plugin's FormInput component.

<Form.Input path="/email" placeholder="user@example.com" autoComplete="email" />

Form.Error

Renders only the error message using the plugin's FormError component.

<Form.Error path="/email" className="error-text" />

Form.Render

커스텀 렌더링을 위한 저수준 슬롯. 렌더 prop 또는 컴포넌트에 원시 FormTypeRendererProps를 전달합니다. 필드가 표시되는 방식을 완전히 제어할 때 사용합니다.

<Form.Render path="/status">
{(props) => (
<div className={`status-field ${props.value}`}>
<props.Input />
{props.errorVisible && <span>{props.errorMessage}</span>}
</div>
)}
</Form.Render>

FormProvider

전역 등록 없이 서브트리 전체에 플러그인 구성을 공유하는 컨텍스트 프로바이더입니다. 동일한 앱의 여러 폼 인스턴스가 다른 플러그인이 필요할 때 유용합니다.

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

<FormProvider
formTypeInputDefinitions={customDefinitions}
CustomFormTypeRenderer={MyRenderer}
formatError={myFormatError}
>
<Form jsonSchema={schema} />
</FormProvider>

Hooks

useFormSubmit

const { submit, pending } = useFormSubmit(formRef);
// submit: () => Promise<void>
// pending: boolean | undefined

useSyncExternalStore를 통해 폼의 내부 제출 핸들러와 동기화됩니다. onSubmit promise가 처리 중일 때 pendingtrue입니다.

function SubmitButton({ formRef }) {
const { submit, pending } = useFormSubmit(formRef);
return (
<button onClick={submit} disabled={pending}>
{pending ? 'Submitting…' : 'Submit'}
</button>
);
}

useSchemaNodeTracker

const version = useSchemaNodeTracker(node, eventMask?);
// Returns an incrementing number; triggers re-render on matching events.
// Default mask: all events (BIT_MASK_ALL)
function PriceDisplay({ node }: { node: NumberNode }) {
useSchemaNodeTracker(node); // re-render whenever node emits any event
return <span>{node.value}</span>;
}

useSchemaNodeSubscribe

useSchemaNodeSubscribe(node, (event) => {
console.log(event.type, node.value);
});
// Raw event subscription — does NOT trigger re-render by itself.

useChildNodeComponentMap

const componentMap = useChildNodeComponentMap(objectNode);
// Returns a Map<string, ChildNodeComponent> keyed by field name.
// Use inside custom FormTypeInput components to render child fields.

useChildNodeErrors

const errors = useChildNodeErrors(node);
// Returns flattened array of all errors from node and its descendants.

registerPlugin

registerPlugin(plugin: SchemaFormPlugin | null): void

플러그인을 전역으로 등록합니다. 안정적인 콘텐츠 해시로 중복 제거됩니다 — 동일한 플러그인 객체로 두 번 호출해도 아무 일도 일어나지 않습니다.

registerPlugin(null)을 호출하면 모든 플러그인이 시스템 기본값으로 재설정됩니다.

SchemaFormPlugin Interface

interface SchemaFormPlugin {
FormGroup?: ComponentType<FormTypeRendererProps>;
FormLabel?: ComponentType<FormTypeRendererProps>;
FormInput?: ComponentType<FormTypeRendererProps>;
FormError?: ComponentType<FormTypeRendererProps>;
formTypeInputDefinitions?: FormTypeInputDefinition[];
validator?: ValidatorPlugin;
formatError?: FormatError;
}

ValidatorPlugin Interface

interface ValidatorPlugin {
bind(instance: any): void;
compile(jsonSchema: JsonSchema): ValidateFunction;
}

type ValidateFunction = (
value: any
) => JsonSchemaError[] | null | Promise<JsonSchemaError[] | null>;

FormTypeInputProps Interface

formTypeInputDefinitions를 통해 등록된 모든 커스텀 입력 컴포넌트는 이 props를 받습니다.

interface FormTypeInputProps<Value, Context, WatchValues, Schema, Node> {
jsonSchema: Schema;
node: Node;
type: JsonSchemaType; // 'string' | 'number' | 'boolean' | ...
name: string;
path: string;
required: boolean;
nullable: boolean;
readOnly: boolean;
disabled: boolean;
value: Value | undefined;
defaultValue: Value | undefined;
onChange: SetStateFnWithOptions<Value | undefined>;
errors: JsonSchemaError[];
errorVisible: boolean;
watchValues: WatchValues; // values from computed.watch paths
onFileAttach: (file: File | File[] | undefined) => void;
ChildNodeComponents: ChildNodeComponent[];
placeholder?: string;
className?: string;
style?: CSSProperties;
context: Context;
[alt: string]: any; // extra props from Form.Group
}

SchemaNode Interface

interface SchemaNode {
// Identity
readonly type: string; // node type ('string', 'object', etc.)
readonly schemaType: JsonSchemaType;
readonly jsonSchema: JsonSchema;
readonly required: boolean;
readonly nullable: boolean;

// Tree structure
readonly depth: number;
readonly isRoot: boolean;
readonly parentNode: SchemaNode | null;
readonly name: string;
readonly path: string;

// Value
readonly value: any;
readonly defaultValue: any;
readonly enhancedValue: any; // includes virtual field values
setValue(input: any | ((prev: any) => any), option?: SetValueOption): void;

// Computed properties
readonly visible: boolean;
readonly active: boolean;
readonly readOnly: boolean;
readonly disabled: boolean;

// State
readonly dirty: boolean;
readonly touched: boolean;
readonly validated: boolean;
readonly initialized: boolean;
readonly errors: JsonSchemaError[];
readonly globalErrors: JsonSchemaError[];
readonly globalState: NodeStateFlags;

// Validation
validate(): Promise<JsonSchemaError[]>;

// Navigation
find(path: string): SchemaNode | undefined;
findAll(path: string): SchemaNode[];

// Events
publish(event: NodeEventType): void;
subscribe(listener: NodeListener): () => void;
}

ArrayNode Methods

interface ArrayNode extends SchemaNode {
readonly length: number;
readonly children: Array<{ node: SchemaNode }>;
push(value?: any): void; // add item (respects maxItems)
remove(index: number): void;
clear(): void; // remove all items (respects minItems)
}

Node Type Guards

import {
isArrayNode, isObjectNode, isStringNode, isBooleanNode,
isNumberNode, isBranchNode, isTerminalNode, isVirtualNode,
isSchemaNode,
} from '@canard/schema-form';

if (isArrayNode(node)) {
node.push(); // TypeScript knows it's ArrayNode here
}

Error Guards

import {
isSchemaFormError,
isJsonSchemaError,
isValidationError,
isUnhandledError,
} from '@canard/schema-form';

FormatError Function

type FormatError = (
error: JsonSchemaError,
node: SchemaNode,
context: Dictionary,
) => ReactNode;

// Example
const formatError: FormatError = (error, node, context) => {
if (error.keyword === 'required')
return `${node.jsonSchema.title ?? node.name} is required`;
if (error.keyword === 'minLength')
return `Minimum ${error.details?.limit} characters required`;
return error.message;
};

JSONPointer

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

// Static utilities for RFC 6901 JSONPointer strings
JSONPointer.escape('path/with/slashes'); // → 'path~1with~1slashes'
JSONPointer.unescape('path~1with~1slashes'); // → 'path/with/slashes'
AI Agent Reference

AI API Quick-Reference

// ── registerPlugin ─────────────────────────────────────────────
registerPlugin(plugin: SchemaFormPlugin | null): void
// null → reset all plugins

// ── Form props (all optional except jsonSchema) ─────────────────
jsonSchema: Schema // required
defaultValue?: Value
onChange?: (value: Value) => void
onSubmit?: (value: Value) => Promise<void> | void
onValidate?: (errors: JsonSchemaError[]) => void
onStateChange?: (state: NodeStateFlags) => void
showError?: boolean | ShowError // default: DirtyTouched
validationMode?: ValidationMode // default: OnChange|OnRequest
readOnly?: boolean
disabled?: boolean
errors?: JsonSchemaError[]
formTypeInputDefinitions?: FormTypeInputDefinition[]
formTypeInputMap?: FormTypeInputMap
CustomFormTypeRenderer?: ComponentType<FormTypeRendererProps>
formatError?: FormatError
validatorFactory?: ValidatorFactory
context?: Dictionary
children?: ReactNode | (props: FormChildrenProps) => ReactNode
ref?: Ref<FormHandle>

// ── FormHandle ──────────────────────────────────────────────────
node?: SchemaNode
getValue(): Value
setValue(v, opts?): void
getState(): NodeStateFlags
setState(s): void
clearState(): void
getErrors(): JsonSchemaError[]
validate(): Promise<JsonSchemaError[]>
showError(b): void
focus(path): void
select(path): void
findNode(path): SchemaNode | null
findNodes(path): SchemaNode[]
reset(): void
submit: TrackableHandlerFunction // .pending, .subscribe()
getAttachedFilesMap(): AttachedFilesMap

// ── SchemaNode key members ──────────────────────────────────────
node.value / node.defaultValue / node.enhancedValue
node.setValue(input, option?)
node.errors / node.globalErrors
node.dirty / node.touched / node.validated / node.initialized
node.visible / node.active / node.readOnly / node.disabled
node.required / node.nullable
node.path / node.name / node.depth / node.isRoot
node.find(path) / node.findAll(path)
node.validate()
node.publish(NodeEventType.RequestRemount)
node.subscribe(listener) → unsubscribe

// ── ArrayNode additional ────────────────────────────────────────
arrayNode.length / arrayNode.children
arrayNode.push(value?) / arrayNode.remove(index) / arrayNode.clear()

// ── Hooks ───────────────────────────────────────────────────────
useFormSubmit(ref){ submit, pending }
useSchemaNodeTracker(node, mask?) → version: number
useSchemaNodeSubscribe(node, listener)void
useChildNodeComponentMap(node) → Map<string, ChildNodeComponent>
useChildNodeErrors(node) → JsonSchemaError[]