Component Mapping
Map each field type to a React component. You provide the UI, Forma handles the state.
Use Default Components for pre-built fields with theming, accessibility, and wizard support out of the box. You can always swap individual components later using { ...defaultComponentMap, select: MySelect }.
Quick Start: Minimum Viable ComponentMap
You can ship a working form with just 4 components + a fallback:
import type {
ComponentMap,
TextComponentProps,
NumberComponentProps,
BooleanComponentProps,
SelectComponentProps,
} from "@fogpipe/forma-react";
const TextInput = ({ field }: TextComponentProps) => (
<input
value={field.value ?? ""}
onChange={(e) => field.onChange(e.target.value)}
onBlur={field.onBlur}
placeholder={field.placeholder}
/>
);
const NumberInput = ({ field }: NumberComponentProps) => (
<input
type="number"
value={field.value ?? ""}
onChange={(e) =>
field.onChange(e.target.value === "" ? null : Number(e.target.value))
}
onBlur={field.onBlur}
/>
);
const Checkbox = ({ field }: BooleanComponentProps) => (
<label>
<input
type="checkbox"
checked={field.value ?? false}
onChange={(e) => field.onChange(e.target.checked)}
/>{" "}
{field.label}
</label>
);
const SelectInput = ({ field }: SelectComponentProps) => (
<select
value={field.value ?? ""}
onChange={(e) =>
field.onChange(e.target.value === "" ? null : e.target.value)
}
>
<option value="">Select...</option>
{field.options.map((opt) => (
<option key={String(opt.value)} value={opt.value}>
{opt.label}
</option>
))}
</select>
);
const components: ComponentMap = {
text: TextInput, // Handles text fields
number: NumberInput, // Handles number and integer
integer: NumberInput,
boolean: Checkbox,
select: SelectInput,
fallback: TextInput, // Catches email, phone, url, textarea, and any unmapped type
};
The fallback key is the secret — any field type without an explicit entry in your ComponentMap falls through to fallback. A single TextInput as fallback covers 6 field types (text, email, phone, url, password, textarea) because they all accept string input.
Progressive enhancement — add specialized components as your needs grow:
| Add when you need... | Field type | Component to build |
|---|---|---|
| Dropdown menus with multi-select | multiselect | Multi-select list |
| Date pickers | date | Date input or calendar |
| Repeating items | array | Array field with CRUD |
| Read-only content | display | Display renderer |
| Survey grids | matrix | Matrix grid |
ComponentMap
import type { ComponentMap } from "@fogpipe/forma-react";
const components: ComponentMap = {
text: TextInput,
phone: PhoneInput,
email: EmailInput,
number: NumberInput,
integer: IntegerInput,
boolean: Checkbox,
select: SelectDropdown,
multiselect: MultiSelect,
date: DatePicker,
textarea: TextArea,
};
Component Props
All components receive { field, spec }. The field object contains everything needed to render and interact with the field:
| Property | Type | Description |
|---|---|---|
name | string | Field path/name |
label | string | Display label |
description | string | undefined | Help text |
placeholder | string | Input placeholder |
required | boolean | Whether field is required |
enabled | boolean | Whether field is editable |
readonly | boolean | Whether field is readonly (visible, value submitted) |
visible | boolean | Whether field is visible |
value | unknown | Current field value |
errors | FieldError[] | All validation errors (always populated) |
visibleErrors | FieldError[] | Errors filtered by touched/submitted state (for UI) |
prefix | string | undefined | Prefix adorner text (e.g., "$") — adornable types only |
suffix | string | undefined | Suffix adorner text (e.g., "kg") — adornable types only |
variant | string | undefined | Presentation variant hint (e.g., "slider", "nps") |
variantConfig | Record<string, unknown> | Variant-specific configuration |
| Method | Description |
|---|---|
onChange(value) | Update field value |
onBlur() | Mark field as touched |
Example Component
Here's a complete field component showing all the patterns:
import type { TextComponentProps } from "@fogpipe/forma-react";
const TextInput = ({ field }: TextComponentProps) => (
<div>
<label htmlFor={field.name}>
{field.label}
{field.required && <span className="required">*</span>}
</label>
<input
id={field.name}
type="text"
value={field.value || ""}
onChange={(e) => field.onChange(e.target.value)}
onBlur={field.onBlur}
placeholder={field.placeholder}
disabled={!field.enabled}
/>
{field.description && (
<p id={`${field.name}-description`}>{field.description}</p>
)}
{field.visibleErrors.map((error, i) => (
<p key={i} className="error" role="alert">
{error.message}
</p>
))}
</div>
);
Other field types follow the same pattern. The key difference is the typed props:
Type-Specific Props
| Type | Props Type | Extra Properties |
|---|---|---|
text, email, url, phone | TextComponentProps | - |
number | NumberComponentProps | min, max, step |
integer | IntegerComponentProps | min, max, step |
boolean | BooleanComponentProps | - |
select | SelectComponentProps | options |
multiselect | MultiSelectComponentProps | options |
date | DateComponentProps | - |
datetime | DateTimeComponentProps | - |
array | ArrayComponentProps | itemFields, helpers, minItems, maxItems |
computed | ComputedComponentProps | expression |
display | DisplayComponentProps | content, sourceValue, format |
matrix | MatrixComponentProps | rows, columns, multiSelect |
Options
For select and multiselect fields, field.options contains the available choices:
interface SelectOption {
value: string | number;
label: string;
}
Options are pre-filtered by visibleWhen FEEL expressions — your component only receives visible options. See Options Visibility.
Display Fields
Display fields show read-only content with DisplayComponentProps — no value or onChange. See Display Fields.
Matrix Fields
Matrix fields render a grid of rows × columns. The MatrixComponentProps provide:
| Property | Type | Description |
|---|---|---|
rows | Array<{ id: string; label: string; visible: boolean }> | Row definitions with visibility |
columns | MatrixColumn[] | Column definitions (shared options) |
multiSelect | boolean | Whether multiple selections per row are allowed |
value | Record<string, string | number | string[] | number[]> | null | Current selections keyed by row ID |
onChange | (value: Record<...>) => void | Update all matrix values |
import type { MatrixComponentProps } from "@fogpipe/forma-react";
function MatrixGrid({ field }: MatrixComponentProps) {
const value = field.value ?? {};
const handleSelect = (rowId: string, colValue: string | number) => {
if (field.multiSelect) {
const current = (value[rowId] as (string | number)[]) ?? [];
const next = current.includes(colValue)
? current.filter((v) => v !== colValue)
: [...current, colValue];
field.onChange({ ...value, [rowId]: next });
} else {
field.onChange({ ...value, [rowId]: colValue });
}
};
return (
<table>
<thead>
<tr>
<th />
{field.columns.map((col) => (
<th key={String(col.value)}>{col.label}</th>
))}
</tr>
</thead>
<tbody>
{field.rows
.filter((row) => row.visible)
.map((row) => (
<tr key={row.id}>
<td>{row.label}</td>
{field.columns.map((col) => {
const selected = field.multiSelect
? ((value[row.id] as (string | number)[]) ?? []).includes(
col.value,
)
: value[row.id] === col.value;
return (
<td key={String(col.value)}>
<input
type={field.multiSelect ? "checkbox" : "radio"}
name={`${field.name}_${row.id}`}
checked={selected}
onChange={() => handleSelect(row.id, col.value)}
/>
</td>
);
})}
</tr>
))}
</tbody>
</table>
);
}
Rows with visible: false (controlled by visibleWhen on the row definition) should be filtered out. The library pre-evaluates row visibility.
Full ComponentMap
const components: ComponentMap = {
text: TextInput,
phone: PhoneInput,
email: TextInput,
url: TextInput,
password: PasswordInput,
number: NumberInput,
integer: NumberInput,
boolean: Checkbox,
select: SelectDropdown,
multiselect: MultiSelect,
date: DatePicker,
datetime: DateTimePicker,
textarea: TextArea,
array: ArrayField,
object: ObjectField,
computed: ComputedDisplay,
display: DisplayField,
matrix: MatrixGrid,
};
Next Steps
- Styling Guide — Style your components with Tailwind, CSS Modules, or plain CSS
- Display Fields — Implement read-only display components
- Advanced Features — Presentation variants (slider, NPS, radio cards)