useForma Hook
The useForma hook provides complete form state management for custom form implementations.
Basic Usage
import { useForma } from "@fogpipe/forma-react";
import type { Forma } from "@fogpipe/forma-core";
function CustomForm({ spec }: { spec: Forma }) {
const forma = useForma({
spec,
onSubmit: (data) => console.log("Submitted:", data),
});
return (
<form
onSubmit={(e) => {
e.preventDefault();
forma.submitForm();
}}
>
{spec.fields &&
Object.entries(spec.fields).map(([fieldId, fieldDef]) => {
if (!forma.visibility[fieldId]) return null;
return (
<div key={fieldId}>
<label>{fieldDef.label}</label>
<input
value={String(forma.data[fieldId] || "")}
onChange={(e) => forma.setFieldValue(fieldId, e.target.value)}
onBlur={() => forma.setFieldTouched(fieldId)}
/>
</div>
);
})}
<button type="submit" disabled={!forma.isValid || forma.isSubmitting}>
Submit
</button>
</form>
);
}
Options
const forma = useForma({
spec, // Required: Forma specification
initialData, // Optional: Initial form values
onSubmit, // Optional: Submit handler
onChange, // Optional: Change handler
validateOn, // Optional: When to validate
referenceData, // Optional: Additional reference data
validationDebounceMs, // Optional: Debounce validation
on, // Optional: Event listeners
});
Options Reference
| Option | Type | Default | Description |
|---|---|---|---|
spec | Forma | required | The Forma specification |
initialData | Record<string, unknown> | {} | Initial form values |
onSubmit | (data) => void | - | Called on valid submission |
onChange | (data, computed) => void | - | Called on any value change |
validateOn | "change" | "blur" | "submit" | "blur" | When to run validation |
referenceData | Record<string, unknown> | - | Additional reference data |
validationDebounceMs | number | 0 | Debounce validation (ms) |
on | FormaEvents | - | Event listeners for side effects |
Return Value
The hook returns an object with state and methods:
State Properties
| Property | Type | Description |
|---|---|---|
data | Record<string, unknown> | Current form values |
computed | Record<string, unknown> | Computed field values |
visibility | Record<string, boolean> | Field visibility state |
required | Record<string, boolean> | Field required state |
enabled | Record<string, boolean> | Field enabled state |
readonly | Record<string, boolean> | Field readonly state |
optionsVisibility | OptionsVisibilityResult | Visible options per select field, keyed by field path |
errors | FieldError[] | All validation errors |
touched | Record<string, boolean> | Which fields have been touched |
isValid | boolean | Whether form is valid |
isDirty | boolean | Whether any field has changed |
isSubmitted | boolean | Whether form has been submitted |
isSubmitting | boolean | Whether submission is in progress |
spec | Forma | The resolved Forma specification |
wizard | WizardHelpers | null | Wizard navigation (if pages defined) |
Methods
| Method | Description |
|---|---|
setFieldValue(path, value) | Set a field's value |
setFieldTouched(path, touched?) | Mark field as touched |
setValues(values) | Set multiple values at once |
validateField(path) | Validate a single field |
validateForm() | Validate entire form |
submitForm() | Submit the form |
resetForm() | Reset to initial values |
on(event, listener) | Subscribe to lifecycle events; returns unsubscribe |
Working with Field Values
Setting Values
// Set a single field
forma.setFieldValue("email", "user@example.com");
// Set nested field (for objects)
forma.setFieldValue("address.city", "New York");
// Set array item
forma.setFieldValue("items[0].name", "Widget");
// Set multiple values
forma.setValues({
firstName: "John",
lastName: "Doe",
email: "john@example.com",
});
Reading Values
const email = forma.data.email;
const city = forma.data.address?.city;
const firstItem = forma.data.items?.[0];
Validation
Validate on Different Events
// Validate on blur (default)
const forma = useForma({ spec, validateOn: "blur" });
// Validate on every change
const forma = useForma({ spec, validateOn: "change" });
// Validate only on submit
const forma = useForma({ spec, validateOn: "submit" });
Manual Validation
// Validate a single field
forma.validateField("email");
// Validate entire form
const result = forma.validateForm();
console.log(result.valid); // boolean
Accessing Errors
// All errors
forma.errors.forEach((error) => {
console.log(`${error.field}: ${error.message}`);
});
// Errors for specific field
const emailErrors = forma.errors.filter((e) => e.field === "email");
// Check if field has errors
const hasEmailError = forma.errors.some((e) => e.field === "email");
Computed Values
Computed values are calculated automatically and available in forma.computed:
function OrderTotal({ spec }: { spec: Forma }) {
const forma = useForma({ spec });
return (
<div>
<p>Subtotal: ${forma.computed.subtotal}</p>
<p>Tax: ${forma.computed.tax}</p>
<p>Total: ${forma.computed.total}</p>
</div>
);
}
Visibility and Required State
function DynamicField({ fieldId, forma }) {
// Check if field should be shown
if (!forma.visibility[fieldId]) {
return null;
}
return (
<div>
<label>
{fieldDef.label}
{forma.required[fieldId] && <span>*</span>}
</label>
<input
disabled={!forma.enabled[fieldId]}
// ...
/>
</div>
);
}
Form Submission
function SubmitButton({ forma }) {
const handleSubmit = async () => {
// Validate before submitting
const result = forma.validateForm();
if (!result.valid) {
return;
}
// Submit will call onSubmit callback
await forma.submitForm();
};
return (
<button
onClick={handleSubmit}
disabled={!forma.isValid || forma.isSubmitting}
>
{forma.isSubmitting ? "Submitting..." : "Submit"}
</button>
);
}
Change Tracking
function UnsavedChangesPrompt({ forma }) {
useEffect(() => {
const handleBeforeUnload = (e: BeforeUnloadEvent) => {
if (forma.isDirty) {
e.preventDefault();
e.returnValue = "";
}
};
window.addEventListener("beforeunload", handleBeforeUnload);
return () => window.removeEventListener("beforeunload", handleBeforeUnload);
}, [forma.isDirty]);
return null;
}
Reset Form
function FormActions({ forma }) {
return (
<div>
<button onClick={() => forma.resetForm()}>Reset</button>
<button onClick={() => forma.submitForm()}>Submit</button>
</div>
);
}
Array Fields
For fields with type: "array", use getArrayHelpers() to get manipulation functions.
Getting Array Helpers
const { getArrayHelpers } = useForma({ spec });
// Get helpers for a specific array field
const medicationsHelpers = getArrayHelpers("medications");
ArrayHelpers Properties
| Property | Type | Description |
|---|---|---|
items | unknown[] | Current array items |
minItems | number | Minimum items allowed |
maxItems | number | Maximum items allowed |
canAdd | boolean | Whether more items can be added |
canRemove | boolean | Whether items can be removed |
ArrayHelpers Methods
| Method | Description |
|---|---|
push(item?) | Add item to end of array |
insert(index, item) | Insert item at specific index |
remove(index) | Remove item at index |
move(from, to) | Move item from one index to another |
swap(indexA, indexB) | Swap items at two indices |
getItemFieldProps(index, fieldName) | Get props for an item's field |
Basic Array Operations
function MedicationsList({ forma }) {
const helpers = forma.getArrayHelpers("medications");
return (
<div>
{helpers.items.map((item, index) => (
<div key={index}>
<input
value={item.name || ""}
onChange={(e) =>
forma.setFieldValue(`medications[${index}].name`, e.target.value)
}
/>
<button
onClick={() => helpers.remove(index)}
disabled={!helpers.canRemove}
>
Remove
</button>
</div>
))}
<button
onClick={() => helpers.push({ name: "", dosage: "" })}
disabled={!helpers.canAdd}
>
Add Medication
</button>
</div>
);
}
Drag and Drop Reordering
Use move() for drag-and-drop functionality:
import { DragDropContext, Droppable, Draggable } from "@hello-pangea/dnd";
function SortableList({ forma }) {
const helpers = forma.getArrayHelpers("items");
const handleDragEnd = (result) => {
if (!result.destination) return;
helpers.move(result.source.index, result.destination.index);
};
return (
<DragDropContext onDragEnd={handleDragEnd}>
<Droppable droppableId="items">
{(provided) => (
<div ref={provided.innerRef} {...provided.droppableProps}>
{helpers.items.map((item, index) => (
<Draggable key={index} draggableId={String(index)} index={index}>
{(provided) => (
<div
ref={provided.innerRef}
{...provided.draggableProps}
{...provided.dragHandleProps}
>
{item.name}
</div>
)}
</Draggable>
))}
{provided.placeholder}
</div>
)}
</Droppable>
</DragDropContext>
);
}
Inserting at Specific Position
function insertAtBeginning() {
helpers.insert(0, { name: "", value: "" });
}
function insertAfterCurrent(currentIndex) {
helpers.insert(currentIndex + 1, { name: "", value: "" });
}
Swapping Items
function moveUp(index) {
if (index > 0) {
helpers.swap(index, index - 1);
}
}
function moveDown(index) {
if (index < helpers.items.length - 1) {
helpers.swap(index, index + 1);
}
}
Using getItemFieldProps
For cleaner code, use getItemFieldProps to get props for item fields:
function ArrayItem({ helpers, index }) {
const nameProps = helpers.getItemFieldProps(index, "name");
const quantityProps = helpers.getItemFieldProps(index, "quantity");
return (
<div>
<input
value={nameProps.value || ""}
onChange={(e) => nameProps.onChange(e.target.value)}
onBlur={nameProps.onBlur}
/>
<input
type="number"
value={quantityProps.value || ""}
onChange={(e) => quantityProps.onChange(Number(e.target.value))}
onBlur={quantityProps.onBlur}
/>
</div>
);
}
Respecting Min/Max Items
function ArrayField({ forma, fieldId }) {
const helpers = forma.getArrayHelpers(fieldId);
return (
<div>
{helpers.items.map((_, index) => (
<div key={index}>
{/* ... item fields ... */}
<button
onClick={() => helpers.remove(index)}
disabled={!helpers.canRemove}
title={
!helpers.canRemove
? `Minimum ${helpers.minItems} items required`
: undefined
}
>
Remove
</button>
</div>
))}
<button
onClick={() => helpers.push()}
disabled={!helpers.canAdd}
title={
!helpers.canAdd
? `Maximum ${helpers.maxItems} items allowed`
: undefined
}
>
Add Item ({helpers.items.length}/{helpers.maxItems || "∞"})
</button>
</div>
);
}