Skip to main content

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

PropTypeRequiredDescription
specFormaYesThe Forma specification
componentsComponentMapYesMap of field types to components
initialDataRecord<string, unknown>NoInitial form values
onSubmit(data) => voidNoCalled on valid submission
onChange(data, computed) => voidNoCalled on value changes
layoutReact.ComponentTypeNoCustom layout component
fieldWrapperReact.ComponentTypeNoWrapper for each field
pageWrapperReact.ComponentTypeNoWrapper for each page (wizard forms)
validateOn"change" | "blur" | "submit"NoWhen to validate
pagenumberNoControlled 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

PropTypeDescription
childrenReactNodeRendered fields
onSubmit() => voidForm submit handler
isValidbooleanForm validity
isSubmittingbooleanSubmission 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

PropTypeDescription
childrenReactNodeThe field component
fieldPathstringField path/identifier
fieldFieldDefinitionField definition from the Forma spec
errorsFieldError[]Validation errors for this field
touchedbooleanWhether the field has been interacted with
visiblebooleanWhether field is visible
requiredbooleanWhether field is required
showRequiredIndicatorbooleanWhether 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

PropTypeDescription
titlestringPage title
descriptionstringOptional page description
childrenReactNodeFields for this page
pageIndexnumberZero-based page index
totalPagesnumberTotal 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

MethodReturn TypeDescription
submitForm()Promise<void>Submit the form
resetForm()voidReset to initial values
validateForm()ValidationResultValidate and return result
focusFirstError()voidFocus first field with error
focusField(path)voidFocus a specific field
getValues()Record<string, unknown>Get current form values
setValues(values)voidSet form values
isValidbooleanWhether form is valid
isDirtybooleanWhether 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>
);
}