Technical Documentation

Basic Usage

This guide covers the four core building blocks of @dfbe/core: FormEngineProvider, useFormEngine, FieldRenderer, and the ComponentRegistry. You need all four to get a working form.

1. FormEngineProvider

The FormEngineProvider is the root context provider for the form engine. It initializes the react-hook-form instance, manages step transitions, and compiles the dynamic Zod validation schema.

Props

PropTypeRequiredDescription
schemaFormSchemaYesThe JSON configuration for the form.
datasetDatasetConfigNoExternal data sources for automatic object transformation.
defaultValuesRecord<string, unknown>NoInitial values for the form fields.
onSubmit(data: unknown) => voidNoCallback triggered after the final step is submitted and validated.
childrenReactNodeYesYour form layout and components.

Usage

import { FormEngineProvider } from "@dfbe/core"; const mySchema = { /* ... */ }; export default function MyForm() { const handleSubmit = (data) => { console.log("Submission successful:", data); }; return ( <FormEngineProvider schema={mySchema} onSubmit={handleSubmit} defaultValues={{ email: "user@example.com" }} > <MyFormLayout /> </FormEngineProvider> ); }

2. useFormEngine()

The useFormEngine hook provides access to the engine's internal state and navigation methods. It must be used within a component wrapped by FormEngineProvider.

Return Values

ValueTypeDescription
schemaFormSchemaThe current form schema.
currentStepFormStepThe currently active step definition.
currentStepIndexnumberThe index (0-based) of the active step.
isFirstStepbooleanTrue if the user is on the first step.
isLastStepbooleanTrue if the user is on the last step.
handleNext(e?) => Promise<void>Validates the current step and moves forward.
handleBack() => voidMoves back to the previous step.
formMethodsUseFormReturnThe raw react-hook-form methods.
showSummarybooleanIndicates if the form is in the "Summary" state (post-last-step).
setShowSummary(show: boolean) => voidProgrammatically control the summary state.
validatedDataRecord<string, unknown>The fully transformed data after the last step is submitted. Empty {} until the final step is submitted.
onFormSubmit(data) => voidInternal submit handler. Pass to <form onSubmit={formMethods.handleSubmit(onFormSubmit)}>.
datasetDatasetConfigThe raw dataset config passed to the provider.
datasetLookupsRecord<string, Map<string, unknown>>Pre-computed O(1) lookup Maps per field.

Example: Navigation Controls

import { useFormEngine } from "@dfbe/core"; export function Navigation() { const { handleNext, handleBack, isFirstStep, isLastStep } = useFormEngine(); return ( <div className="flex gap-4"> <button onClick={handleBack} disabled={isFirstStep}> Back </button> <button onClick={handleNext}>{isLastStep ? "Submit" : "Next Step"}</button> </div> ); }

3. FieldRenderer

The FieldRenderer is a headless component that dispatches field definitions to your UI components based on the ComponentRegistry. It is used internally by the engine — you do not need to pass props to it directly from your consumer code.

Props

PropTypeDescription
fieldFormFieldDefinitionThe field definition from the schema.
registryRecord<string, ResourceRegistryEntry>Optional override of the resource registry for this field (defaults to the schema's resourceRegistry).
uidPrefixstringOptional prefix for generated element IDs (useful for avoiding ID collisions in repeated sections).

Usage

import { FieldRenderer } from "@dfbe/core"; export function Section({ section }) { return ( <div> <h3>{section.title}</h3> {section.fields.map((field) => ( <FieldRenderer key={field.name} field={field} /> ))} </div> ); }

4. ComponentRegistry — Connecting Your UI

The ComponentRegistry is the critical bridge between the JSON schema's field.type string and your actual React input components. Without it, FieldRenderer has nothing to render.

Step 1: Create Adapter Components

Each adapter receives a standardized FieldAdapterProps object from the engine:

import { Controller, useFormContext } from "react-hook-form"; import type { FieldAdapterProps } from "@dfbe/core"; function TextFieldAdapter({ field, computedState }: FieldAdapterProps) { const { control } = useFormContext(); return ( <Controller name={field.name} control={control} render={({ field: rhf, fieldState }) => ( <div> <label htmlFor={field.name}>{field.label}</label> <input {...rhf} id={field.name} placeholder={field.placeholder} aria-invalid={fieldState.invalid} disabled={computedState.disabled} /> {fieldState.error && <p>{fieldState.error.message}</p>} </div> )} /> ); }

FieldAdapterProps Interface

PropTypeDescription
fieldFormFieldDefinitionThe field definition with computed label and helperText applied.
computedStateComputedFieldStatevisible, required, disabled, computedValue from the rule engine.
apiOptionsOptionItem[]Resolved options for select / radio / combobox fields.
isApiLoadingbooleanTrue while the API is fetching options for this field.

Step 2: Build the Registry Object

Map each field.type string to its adapter component:

import type { ComponentRegistry } from "@dfbe/core"; const myRegistry: ComponentRegistry = { text: TextFieldAdapter, email: EmailFieldAdapter, number: NumberFieldAdapter, textarea: TextareaAdapter, select: SelectAdapter, radio: RadioAdapter, checkbox: CheckboxAdapter, "checkbox-group": CheckboxGroupAdapter, combobox: ComboboxAdapter, "searchable-select": SearchableSelectAdapter, date: DatePickerAdapter, file: FileUploadAdapter, markdown: MarkdownAdapter, // Render read-only markdown/HTML content };

Step 3: Inject via Context & Wire Everything Together

The registry is provided via ComponentRegistryContext, not as a prop to FormEngineProvider. This is the complete minimal setup:

import { ComponentRegistryContext, FormEngineProvider, FieldRenderer, useFormEngine, } from "@dfbe/core"; // Navigation bar — reads engine state function Navigation() { const { handleNext, handleBack, isFirstStep, isLastStep } = useFormEngine(); return ( <div> <button onClick={handleBack} disabled={isFirstStep}> Back </button> <button onClick={handleNext}>{isLastStep ? "Submit" : "Next"}</button> </div> ); } // Layout that renders a step function FormLayout() { const { currentStep } = useFormEngine(); return ( <form> {(currentStep.sections ?? []) .flatMap((s) => s.fields) .map((field) => ( <FieldRenderer key={field.name} field={field} /> ))} <Navigation /> </form> ); } // Root — registry wraps everything export function MyFormPage({ schema, onSubmit }) { return ( <ComponentRegistryContext.Provider value={myRegistry}> <FormEngineProvider schema={schema} onSubmit={onSubmit}> <FormLayout /> </FormEngineProvider> </ComponentRegistryContext.Provider> ); }

Tip: Keep the registry in a separate file (e.g. my-registry.ts) so you can swap it with a mock registry during tests without changing your form layout code.