Builds a complete @uxf/form form from a schema (a list of field configs) plus a map of field-renderer components. Instead of hand-writing every field, you describe the fields as data and let FormRenderer render, submit, and validate them.
Use FormRenderer when the set of fields is data-driven — typically a schema generated from a backend entity — rather than a fixed, hand-authored layout. It renders each schema entry through a renderer chosen by the entry's type, wraps everything in a Form, and appends a submit button.
For a small, static form, compose Form and the individual field components directly; you get more control over layout and per-field props than the schema allows.
Grounded in form-renderer.stories.tsx. The renderer components (base-field, embedded, one-to-many) ship with the package but are not re-exported from the package entry — import them from their file paths (each is a default export):
import { FormRenderer, FormRendererFields, FormSchema } from "@uxf/form/form-renderer";
import baseField from "@uxf/form/form-renderer/field/base-field";
import embedded from "@uxf/form/form-renderer/field/embedded";
import oneToMany from "@uxf/form/form-renderer/field/one-to-many";
import { useForm } from "react-hook-form";
interface FormData {
name: string;
active: boolean;
}
const schema: FormSchema<FormData> = {
fields: [
{
autocomplete: null,
editable: true,
fields: [],
label: "Name",
name: "name",
options: null,
readOnly: false,
required: true,
type: "string",
},
{
autocomplete: null,
editable: true,
fields: [],
label: "Active",
name: "active",
options: null,
readOnly: false,
required: false,
type: "boolean",
},
],
};
const fields: FormRendererFields<FormData> = {
default: baseField,
embedded,
oneToMany,
};
export function Example() {
const formApi = useForm<FormData>();
return <FormRenderer fields={fields} formApi={formApi} id="example-form" onSubmit={console.log} schema={schema} />;
}
For each entry in schema.fields, FormRenderer picks the renderer as fields[field.type] ?? fields.default and renders it with the field config and formApi.control. So default handles every scalar type, while composite types (embedded, oneToMany) need their own keys matching the schema type string.
FormRendererProps<FormData> extends Omit<FormProps<FormData>, "children">. Only the props below are actually applied — see Gotchas for the inherited props that are accepted but ignored.
| Prop | Type | Default | Description |
|---|---|---|---|
schema |
FormSchema<FormData> |
— | Required. The list of field configs to render. |
fields |
FormRendererFields<FormData> |
— | Required. Map of schema type → renderer component; must include a default. |
formApi |
UseFormReturn<FormData> |
— | Required. From react-hook-form's useForm. |
id |
string |
— | Required. Form id (passed to the inner Form). |
onSubmit |
SubmitHandler<FormData> |
— | Required. Submit handler. |
isEditing |
boolean |
undefined |
When true, fields whose config has editable: false are rendered disabled. |
SubmitButton |
FunctionComponent<{ formState: FormState<FormData> }> |
built-in | Custom submit button; receives formApi.formState. Defaults to a @uxf/ui/button that is disabled while formState.isSubmitting and labelled from the uxf-form-form-renderer:submit-button translation. |
FormSchema<FormData> is { fields: FieldSchema<FormData>[] }. Each FieldSchema<FormData>:
| Key | Type | Description |
|---|---|---|
name |
Path<FormData> |
Field path in the form values. |
type |
string |
Selects the renderer (fields[type]) and, in base-field, the concrete input. |
label |
string |
Visible field label. |
required |
boolean |
Adds a required rule / indicator (unless the field is disabled). |
readOnly |
boolean |
Renders the field disabled. |
editable |
boolean |
When false and isEditing is true, the field is disabled. |
autocomplete |
string | null |
Autocomplete endpoint key for relation types (manyToOne / manyToMany). |
options |
Array<{ id: string | number; label: string }> | null |
Options for the enum type. |
fields |
FieldSchema<FieldValues>[] |
Nested field configs for composite types (embedded, oneToMany). |
FormRendererFields<FormData> is { [type: string]: FieldComponent<FormData>; default: FieldComponent<FormData> } — a map from schema type to a FieldComponent, with a mandatory default. FieldComponent<FormData> is FunctionComponent<FieldProps<FormData>>.
The package ships three ready-made renderers under form-renderer/field/:
base-field (use as default)Maps fieldSchema.type to a concrete @uxf/form field. It derives state as isDisabled = readOnly || (isEditing && !editable) and isRequired = !isDisabled && required.
type |
Renders |
|---|---|
"file", "logo", "image" |
FileInput |
"files", "images" |
DropzoneInput + DropzoneList |
"enum" |
Select (from fieldSchema.options) |
"boolean" |
CheckboxInput |
"date" |
DatePickerInput |
"datetime" |
DatetimePickerInput |
"time" |
TimePickerInput |
"integer" |
NumberInput |
"text" |
Textarea |
"manyToMany" |
MultiCombobox (loads options via fieldSchema.autocomplete) |
"manyToOne" |
Combobox (loads options via fieldSchema.autocomplete) |
anything else (e.g. "string") |
TextInput |
embedded (register under key embedded)Renders the nested fieldSchema.fields as a group, prefixing each child field name with <name>. so it maps to a nested object in the form values.
one-to-many (register under key oneToMany)Renders a repeatable list backed by react-hook-form's useFieldArray, with add/remove buttons (labels from the uxf-form-form-renderer:fields.one-to-many.* translations). Child field names are prefixed with <name>.<index>..
FieldProps<FormData>The props every renderer receives:
| Prop | Type | Notes |
|---|---|---|
fieldSchema |
FieldSchema<FormData> |
The config for this field. |
fields |
FormRendererFields<FormData> |
The full renderer map (so composite renderers can render children). |
control |
Control<FormData> |
react-hook-form control. |
isEditing |
boolean |
Passed through from FormRenderer. |
prefix |
string |
Name prefix applied by composite renderers to nested fields. |
overrides |
{ autocomplete?; upload?; onRemoveConfirm? } |
Handler overrides for relation/upload fields. Not populated by FormRenderer — only useful when you render a field component yourself. |
CSS — import the renderer stylesheet from the published package:
@import url("@uxf/form/css/form-renderer.css");
It uses Tailwind @apply / theme(), so it must be processed by the consuming app's Tailwind build. It only styles the renderer's own layout (.uxf-form-renderer__body, .uxf-form-renderer__footer); you still need the stylesheets and the @uxf/ui token layer for whichever fields base-field renders.
Translations — the built-in submit button and the one-to-many renderer read the uxf-form-form-renderer translation namespace via @uxf/core-react/translations; register it in the app.
Client boundary — it renders Form (a client component) and uses hooks, so render it inside a client component.
FormRenderer only forwards formApi, id, and onSubmit to the inner Form, and always sets the form class to uxf-form-renderer. The following props are inherited from FormProps (or declared) but silently ignored: className, method, onError, isDisabled, isReadOnly, shouldOmitSubmitInput, forwardRef. Use a custom SubmitButton or a plain Form if you need those.fields.default is required by the type — omitting it is a type error, and any schema type without a matching renderer key falls back to it.mapToRequestObject (a helper that maps form values to a request payload per the schema) lives in this directory but is not exported from the package entry.