Skip to main content

Component Mapping

Map each field type to a React component. You provide the UI, Forma handles the state.

Don't need custom components?

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:

components.tsx
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 typeComponent to build
Dropdown menus with multi-selectmultiselectMulti-select list
Date pickersdateDate input or calendar
Repeating itemsarrayArray field with CRUD
Read-only contentdisplayDisplay renderer
Survey gridsmatrixMatrix grid

ComponentMap

componentMap.tsx
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:

PropertyTypeDescription
namestringField path/name
labelstringDisplay label
descriptionstring | undefinedHelp text
placeholderstringInput placeholder
requiredbooleanWhether field is required
enabledbooleanWhether field is editable
readonlybooleanWhether field is readonly (visible, value submitted)
visiblebooleanWhether field is visible
valueunknownCurrent field value
errorsFieldError[]All validation errors (always populated)
visibleErrorsFieldError[]Errors filtered by touched/submitted state (for UI)
prefixstring | undefinedPrefix adorner text (e.g., "$") — adornable types only
suffixstring | undefinedSuffix adorner text (e.g., "kg") — adornable types only
variantstring | undefinedPresentation variant hint (e.g., "slider", "nps")
variantConfigRecord<string, unknown>Variant-specific configuration
MethodDescription
onChange(value)Update field value
onBlur()Mark field as touched

Example Component

Here's a complete field component showing all the patterns:

TextInput.tsx
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

TypeProps TypeExtra Properties
text, email, url, phoneTextComponentProps-
numberNumberComponentPropsmin, max, step
integerIntegerComponentPropsmin, max, step
booleanBooleanComponentProps-
selectSelectComponentPropsoptions
multiselectMultiSelectComponentPropsoptions
dateDateComponentProps-
datetimeDateTimeComponentProps-
arrayArrayComponentPropsitemFields, helpers, minItems, maxItems
computedComputedComponentPropsexpression
displayDisplayComponentPropscontent, sourceValue, format
matrixMatrixComponentPropsrows, 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:

PropertyTypeDescription
rowsArray<{ id: string; label: string; visible: boolean }>Row definitions with visibility
columnsMatrixColumn[]Column definitions (shared options)
multiSelectbooleanWhether multiple selections per row are allowed
valueRecord<string, string | number | string[] | number[]> | nullCurrent selections keyed by row ID
onChange(value: Record<...>) => voidUpdate all matrix values
MatrixGrid.tsx
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>
);
}
info

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