FormRenderer
The FormRenderer component provides a complete form rendering solution with minimal setup.
Basic Usage
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:
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:
import type { LayoutProps } from "@fogpipe/forma-react";
const CustomLayout = ({
children,
onSubmit,
isValid,
isSubmitting,
}: LayoutProps) => (
<form
onSubmit={(e) => {
e.preventDefault();
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 | () => void | Form submit handler |
isValid | boolean | Form validity |
isSubmitting | boolean | Submission in progress |
Field Wrapper
Wrap each field with custom markup:
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:
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:
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 |
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" ... />
Complete Example
import { useRef } from "react";
import { FormRenderer, FormaErrorBoundary } from "@fogpipe/forma-react";
import type { FormRendererHandle, LayoutProps } from "@fogpipe/forma-react";
const CustomLayout = ({
children,
onSubmit,
isValid,
isSubmitting,
}: LayoutProps) => (
<form
onSubmit={(e) => {
e.preventDefault();
onSubmit();
}}
className="space-y-4"
>
{children}
<div className="flex gap-2">
<button
type="submit"
disabled={!isValid || isSubmitting}
className="btn-primary"
>
{isSubmitting ? "Submitting..." : "Submit"}
</button>
</div>
</form>
);
function OrderForm({ spec, onOrderSubmit }) {
const formRef = useRef<FormRendererHandle>(null);
const handleSubmit = async (data) => {
try {
await onOrderSubmit(data);
formRef.current?.resetForm();
} catch (error) {
console.error("Submit failed:", error);
}
};
return (
<FormaErrorBoundary fallback={<div>Form error occurred</div>}>
<FormRenderer
ref={formRef}
spec={spec}
components={components}
layout={CustomLayout}
initialData={{ quantity: 1 }}
onSubmit={handleSubmit}
onChange={(data, computed) => {
console.log("Total:", computed.total);
}}
validateOn="blur"
/>
</FormaErrorBoundary>
);
}