FormRenderer
The FormRenderer component provides a complete form rendering solution with minimal setup.
Basic Usage
MyForm.tsx
import { FormRenderer } from "@fogpipe/forma-react";
function MyForm() {
return (
<FormRenderer
spec={myFormaSpec}
components={myComponents}
onSubmit={(data) => console.log(data)}
/>
);
}
Props
| Prop | Type | Required | Description |
|---|---|---|---|
spec | Forma | Yes | The Forma specification |
components | ComponentMap | Yes | Map of field types to components |
initialData | Record<string, unknown> | No | Initial form values |
onSubmit | (data) => void | No | Called on valid submission |
onChange | (data, computed) => void | No | Called on value changes |
layout | React.ComponentType | No | Custom layout component |
fieldWrapper | React.ComponentType | No | Wrapper for each field |
pageWrapper | React.ComponentType | No | Wrapper for each page (wizard forms) |
validateOn | "change" | "blur" | "submit" | No | When to validate |
page | number | No | Controlled wizard page index |
Initial Data
Pre-populate form fields with existing data:
<FormRenderer
spec={spec}
components={components}
initialData={{
firstName: "John",
lastName: "Doe",
email: "john@example.com",
}}
onSubmit={handleSubmit}
/>
onChange Handler
React to form changes in real-time:
LivePreview.tsx
function LivePreview() {
const [formData, setFormData] = useState({});
const [computed, setComputed] = useState({});
return (
<div>
<FormRenderer
spec={spec}
components={components}
onChange={(data, computedValues) => {
setFormData(data);
setComputed(computedValues);
}}
/>
<div className="preview">
<h3>Current Values:</h3>
<pre>{JSON.stringify(formData, null, 2)}</pre>
<h3>Computed:</h3>
<pre>{JSON.stringify(computed, null, 2)}</pre>
</div>
</div>
);
}
Custom Layout
Override the default form layout:
CustomLayout.tsx
import type { LayoutProps } from "@fogpipe/forma-react";
const CustomLayout = ({
children,
onSubmit,
isValid,
isSubmitting,
}: LayoutProps) => (
// onSubmit already calls preventDefault — pass it directly to <form>
<form onSubmit={onSubmit} className="custom-form">
<div className="form-fields">{children}</div>
<div className="form-actions">
<button type="reset">Clear</button>
<button type="submit" disabled={!isValid || isSubmitting}>
{isSubmitting ? "Saving..." : "Save"}
</button>
</div>
</form>
);
<FormRenderer
spec={spec}
components={components}
layout={CustomLayout}
onSubmit={handleSubmit}
/>;
LayoutProps
| Prop | Type | Description |
|---|---|---|
children | ReactNode | Rendered fields |
onSubmit | (e?: React.FormEvent) => void | Form submit handler — preventDefault is called automatically |
isValid | boolean | Form validity |
isSubmitting | boolean | Submission in progress |
Field Wrapper
Wrap each field with custom markup:
FieldWrapper.tsx
import type { FieldWrapperProps } from "@fogpipe/forma-react";
const FieldWrapper = ({ children, field, visible }: FieldWrapperProps) => {
if (!visible) return null;
return (
<div
className={`field-container ${field.errors.length ? "has-error" : ""}`}
>
{children}
</div>
);
};
<FormRenderer
spec={spec}
components={components}
fieldWrapper={FieldWrapper}
/>;
FieldWrapperProps
| Prop | Type | Description |
|---|---|---|
children | ReactNode | The field component |
fieldPath | string | Field path/identifier |
field | FieldDefinition | Field definition from the Forma spec |
errors | FieldError[] | Validation errors for this field |
touched | boolean | Whether the field has been interacted with |
visible | boolean | Whether field is visible |
required | boolean | Whether field is required |
showRequiredIndicator | boolean | Whether to show * — false for boolean fields (false is a valid answer) |
Page Wrapper
Customize how each page is rendered in wizard forms:
PageWrapper.tsx
import type { PageWrapperProps } from "@fogpipe/forma-react";
const PageWrapper = ({
title,
description,
children,
pageIndex,
totalPages,
}: PageWrapperProps) => (
<div className="page-section">
<div className="page-header">
<h3>{title}</h3>
{description && <p>{description}</p>}
<span>
Page {pageIndex + 1} of {totalPages}
</span>
</div>
<div className="page-fields">{children}</div>
</div>
);
<FormRenderer spec={spec} components={components} pageWrapper={PageWrapper} />;
PageWrapperProps
| Prop | Type | Description |
|---|---|---|
title | string | Page title |
description | string | Optional page description |
children | ReactNode | Fields for this page |
pageIndex | number | Zero-based page index |
totalPages | number | Total number of pages |
Imperative Handle
Access form methods imperatively using a ref:
FormWithActions.tsx
import { useRef } from "react";
import type { FormRendererHandle } from "@fogpipe/forma-react";
function FormWithActions() {
const formRef = useRef<FormRendererHandle>(null);
const handleExternalSubmit = () => {
formRef.current?.submitForm();
};
const handleReset = () => {
formRef.current?.resetForm();
};
const handleValidate = () => {
const result = formRef.current?.validateForm();
console.log("Form is valid:", result?.valid);
};
const handleFocusError = () => {
formRef.current?.focusFirstError();
};
const getCurrentValues = () => {
const values = formRef.current?.getValues();
console.log("Current values:", values);
};
return (
<div>
<FormRenderer ref={formRef} spec={spec} components={components} />
<div className="external-controls">
<button onClick={handleExternalSubmit}>Submit</button>
<button onClick={handleReset}>Reset</button>
<button onClick={handleValidate}>Validate</button>
<button onClick={handleFocusError}>Focus First Error</button>
<button onClick={getCurrentValues}>Log Values</button>
</div>
</div>
);
}
FormRendererHandle Methods
| Method | Return Type | Description |
|---|---|---|
submitForm() | Promise<void> | Submit the form |
resetForm() | void | Reset to initial values |
validateForm() | ValidationResult | Validate and return result |
focusFirstError() | void | Focus first field with error |
focusField(path) | void | Focus a specific field |
getValues() | Record<string, unknown> | Get current form values |
setValues(values) | void | Set form values |
isValid | boolean | Whether form is valid |
isDirty | boolean | Whether any field has changed |
Matrix Fields
Matrix fields work like any other field type — add a matrix entry to your ComponentMap:
const components: ComponentMap = {
// ...other components
matrix: MatrixGrid,
};
The matrix component receives MatrixComponentProps with rows, columns, multiSelect, and a value object keyed by row ID. See Component Mapping — Matrix Fields for a full implementation example.
Validation Timing
Control when validation runs:
// Validate on blur (default) - validates when field loses focus
<FormRenderer validateOn="blur" ... />
// Validate on change - validates on every keystroke
<FormRenderer validateOn="change" ... />
// Validate on submit only - no validation until form submission
<FormRenderer validateOn="submit" ... />