Skip to main content

Form Adapters

A FormAdapter handles all state management concerns — registration, validation, dirty tracking, submission — while staying completely decoupled from the UI.

Built-in Form Adapters

ExportLibraryDescription
RavenRHFAdapterReact Hook FormProduction-ready. Supports async validation, DevTools integration, per-form state isolation
RavenAntDAdapterNone (built-in)Lightweight in-memory adapter — useful for demos and simple forms. Zero extra dependencies

FormAdapter Interface

interface FormAdapter {
/** Wraps the form tree; must call onSubmit when submitted */
Provider: ComponentType<FormAdapterProviderProps>;

/** Returns binding for a single field by name */
useField: (name: string) => FieldBinding;

/** Returns a function that triggers form submission */
useSubmit: () => () => void;

/** Returns a snapshot of current form values */
useWatch: (names?: string[]) => Record<string, unknown>;

/**
* Optional: trigger validation for specific fields.
* Used by RavenWizardForm to gate step advancement.
*/
useTrigger?: () => (names: string[]) => Promise<boolean>;
}

FieldBinding

interface FieldBinding {
value: unknown;
onChange: (value: unknown) => void;
onBlur: () => void;
error?: string;
isDirty?: boolean;
isTouched?: boolean;
}

Creating a Custom FormAdapter

Use createFormAdapter to build a type-safe adapter:

import {
createFormAdapter,
type FormAdapterProviderProps,
type FieldBinding,
} from "raven-form-engine";
import { useRef, useState, useContext, createContext } from "react";

// 1. Create your own context for form state
const MyFormCtx = createContext(null);

// 2. Build the Provider
const MyProvider = ({
onSubmit,
defaultValues = {},
children,
}: FormAdapterProviderProps) => {
const [values, setValues] = useState(defaultValues);
const [errors, setErrors] = useState({});

const handleSubmit = (e) => {
e.preventDefault();
onSubmit(values);
};

return (
<MyFormCtx.Provider
value={{ values, setValues, errors, setErrors, handleSubmit }}
>
<form onSubmit={handleSubmit}>{children}</form>
</MyFormCtx.Provider>
);
};

// 3. Register with createFormAdapter
export const MyFormAdapter = createFormAdapter({
Provider: MyProvider,

useField: (name) => {
const { values, setValues, errors } = useContext(MyFormCtx);
return {
value: values[name] ?? "",
onChange: (v) => setValues((prev) => ({ ...prev, [name]: v })),
onBlur: () => {},
error: errors[name],
};
},

useSubmit: () => {
const { handleSubmit } = useContext(MyFormCtx);
return handleSubmit;
},

useWatch: (names) => {
const { values } = useContext(MyFormCtx);
if (!names) return values;
return Object.fromEntries(names.map((n) => [n, values[n]]));
},
});

Global vs Per-Form Adapters

Set the default adapter once in your app's layout:

// app/layout.tsx
<RavenFormProvider
formAdapter={RavenRHFAdapter}
uiAdapter={RavenShadcnUIAdapter}
>
{children}
</RavenFormProvider>

Override per-form when needed:

// This form uses a lightweight in-memory adapter instead of RHF
<RavenForm
schema={quickSearchSchema}
onSubmit={handleSearch}
adapter={RavenAntDAdapter}
ui={myMinimalUI}
/>

Per-Adapter Registry (Multi-Adapter)

Register multiple named adapters and reference them by key using RavenAdapterRegistry:

import { RavenFormProvider } from "raven-form-engine";

<RavenFormProvider
registry={{
default: { formAdapter: RavenRHFAdapter, uiAdapter: RavenShadcnUIAdapter },
antd: { formAdapter: RavenAntDAdapter, uiAdapter: RavenAntDUIAdapter },
simple: { formAdapter: RavenAntDAdapter, uiAdapter: myMinimalUI },
}}
>
{children}
</RavenFormProvider>

Adapter Validation Utilities

import { validateFormAdapter } from "raven-form-engine";

// In development, logs warnings for missing required fields
validateFormAdapter(myFormAdapter);