useDynamicFormView
Backend-agnostic dynamic form renderer — the sibling of useDocyrusFormView with no Docyrus wiring. Supply fields, values, options, and an onSubmit sink to render create/edit/view forms against any backend.
Installation
pnpm dlx @docyrus/cli add @docyrus/hooks-use-dynamic-form-viewpnpm add @tanstack/react-query jsonataThis hook is distributed as source. It shares the same rendering engine as useDocyrusFormView but performs no fetching — you inject the schema, values, options, and submit handler yourself, so it works against any backend (or none).
Overview
useDynamicFormView is the backend-agnostic counterpart of useDocyrusFormView. Both drive the exact same internal engine, so this hook supports every rendering feature of useDocyrusFormView — it just drops the Docyrus-specific data layer:
- local form state compatible with the Docyrus form-field components
- shared field-component resolution via
useDocyrusFieldComponent - layout helpers (
renderField,renderLayout) including nestedfieldset/tabpanel/tabsections - computed fields (
computedHidden/computedRequired/computedLabel/computedDescription/computedFormula) - imperative field actions and form-level actions (
onFormLoad/onFormBeforeSubmit/onFormAfterSubmit) - field-level and form-level validations —
required, plus theminLength:/maxLength:/pattern:/min:/max:tokens whenvalidationTokensis enabled, then JSONatacustomValidations, then form-level rules - companion columns / submit keys for composite fields (money, phone, status, avatar)
- click-to-edit detail mode via
EditableRecordDetail
What it does not do (and what you provide instead):
useDocyrusFormView does… | In useDynamicFormView you supply… |
|---|---|
fetches the data-source schema (getBySlug) | fields: IField[] |
loads the record (GET …/items/:id / collection.get) | initialValues / values |
loads enum / user / relation options (/v1/apps/enums, /v1/users, relation /items) | enumOptions (static, per slug) |
uploads files (POST …/files/upload) | onImageUpload / onFileUpload |
persists the record (POST / PATCH / collection) | onSubmit |
For a Docyrus-backed form, use useDocyrusFormView instead.
Usage
Create form (static options + onSubmit)
'use client';
import { useDynamicFormView } from '@docyrus/ui/library/hooks/use-dynamic-form-view';
import type { IField, EnumOption } from '@docyrus/ui/components/form-fields';
import { Button } from '@docyrus/ui/primitives/ui/button';
const fields: IField[] = [
{ id: '1', name: 'Full Name', slug: 'full_name', type: 'field-text', validations: ['required'] },
{ id: '2', name: 'Email', slug: 'email', type: 'field-email', validations: ['required'] },
{ id: '3', name: 'Status', slug: 'status', type: 'field-select' },
{ id: '4', name: 'Notes', slug: 'notes', type: 'field-textarea' }
];
const enumOptions: Record<string, EnumOption[]> = {
status: [
{ id: 'lead', name: 'Lead', color: 'sky-500' },
{ id: 'active', name: 'Active', color: 'emerald-500' }
]
};
export function CreateContactForm() {
const form = useDynamicFormView({
fields,
mode: 'create',
gridColumns: 2,
enumOptions,
fieldOrder: ['full_name', 'email', 'status', 'notes'],
fieldLayout: { notes: { colSpan: 'full' } },
onSubmit: async (payload) => {
// Persist to any backend of your choice.
await fetch('/api/contacts', { method: 'POST', body: JSON.stringify(payload) });
}
});
return (
<form
onSubmit={async (event) => {
event.preventDefault();
await form.submit();
}}
className="space-y-4">
{form.renderLayout()}
<div className="flex items-center gap-2">
<Button type="submit" disabled={form.isSubmitting}>Create Contact</Button>
<Button type="button" variant="outline" onClick={form.reset}>Reset</Button>
</div>
</form>
);
}Edit form (uncontrolled seed)
Pass initialValues to seed an edit form. Values are owned internally; read the result via form.values or the onSubmit payload.
const form = useDynamicFormView({
fields,
mode: 'edit',
initialValues: record, // seeds the form once (and re-seeds if its content changes)
enumOptions,
onSubmit: (payload) => saveContact(record.id, payload)
});Controlled values
Provide values to fully control the form from the outside. The form re-seeds whenever the object's content changes, and onValuesChange fires on every field edit.
const [values, setValues] = useState(record);
const form = useDynamicFormView({
fields,
mode: 'edit',
values, // controlled
onValuesChange: setValues, // fires on every field change
enumOptions
});Read-only view + click-to-edit
const form = useDynamicFormView({
fields,
mode: 'view',
initialValues: record,
clickToEdit: true, // rows become inline-editable on click
enumOptions,
onSubmit: (payload) => saveContact(record.id, payload)
});
return form.renderLayout();Nested layout, computed fields, actions & validations
Everything from useDocyrusFormView works identically — layout, fieldLayout (including computedHidden / computedFormula / fieldActions), formActions, and formCustomValidations.
const form = useDynamicFormView({
fields,
mode: 'create',
gridColumns: 4,
layout: [
{ id: 'identity', variant: 'fieldset', title: 'Identity', colSpan: 2, columns: 1, collapsible: true, items: ['full_name', 'email'] }
],
fieldLayout: {
vat_number: { computedHidden: 'is_company != true' },
total_price: { computedFormula: '$number(qty) * $number(unit_price)', readOnly: true }
},
formCustomValidations: [
{ id: 'v1', expression: 'close_date >= open_date', message: 'Close date cannot precede the open date.' }
]
});Render form.formValidationErrors as a destructive banner above the form; per-field errors live in form.validationErrors (keyed by slug). See the useDocyrusFormView docs for the full computed-field, field-action, form-action, and validation reference — the behavior is shared.
Uploads
Wire onImageUpload / onFileUpload to persist a picked File to your storage and return a stored-value object; the hook injects them into field-image / field-file fields.
const form = useDynamicFormView({
fields,
onImageUpload: async (file) => uploadToStorage(file), // → StoredFileValue | null
onFileUpload: async (file) => uploadToStorage(file)
});Dynamic option loading (per field)
enumOptions is static. For typeahead / lazy loading on a single select or relation field, hand-wire that field's loader through fieldLayout[slug].fieldProps — the engine threads onSearch / searching / onLoadMore / hasMore / onExpand straight to the field component.
fieldLayout={{
owner: {
fieldProps: {
onSearch: (term) => loadUsers(term),
searching: isLoading,
onLoadMore: () => loadNextPage(),
hasMore
}
}
}}API Reference
Parameters
| Option | Type | Default | Description |
|---|---|---|---|
fields | IField[] | — | Required. Field schema, already normalized to IField. Replaces the Docyrus schema fetch. |
mode | 'create' | 'edit' | 'view' | 'create' (no values) / 'edit' (with values) | Explicit form mode. |
initialValues | Record<string, unknown> | — | Uncontrolled seed, merged over field defaults. Re-seeds when its content changes. |
values | Record<string, unknown> | — | Controlled values. When set, the form is controlled and re-seeds on content change. |
onValuesChange | (values) => void | — | Fires on every field change (both controlled and uncontrolled). |
disabled | boolean | false | Disable editable fields globally. |
clickToEdit | boolean | false | In view mode, route renderLayout() through EditableRecordDetail for inline editing. |
includeReadOnlyFields | boolean | mode !== 'create' | Include fields that resolve to read-only display rows. |
validationTokens | 'off' | 'form' | 'all' | 'off' | Enforce validations token constraints beyond required on submit. 'form' enforces only fieldLayout[slug].validations; 'all' also enforces IField.validations. required is always enforced. |
unsupportedFieldBehavior | 'skip' | 'value' | 'skip' in create, 'value' otherwise | Whether unsupported field types disappear or fall back to value-render mode. |
gridColumns | 1 | 2 | 3 | 4 | 2 | Default responsive column count used by renderLayout(). |
labelAlign | 'top' | 'left' | 'top' | Form-level label placement. 'left' renders a horizontal label column. |
labelWidth | 'sm' | 'md' | 'lg' | 'md' | Width of the label column. Only meaningful with labelAlign: 'left'. |
fieldSize | 'sm' | 'md' | 'lg' | 'md' | Field density applied to every input. |
fieldVariant | 'outline' | 'filled' | 'outline' | Input style applied to every input. |
fieldSlugs | string[] | — | Whitelist fields by slug. |
fieldOrder | string[] | — | Explicit field ordering. |
hiddenFieldSlugs | string[] | — | Hard-hide fields by slug. |
fieldLayout | Record<string, DynamicFormViewFieldLayout> | — | Per-field UI overrides, computed props, field actions, validation tokens (validations), and custom validations. Same shape as DocyrusFormViewFieldLayout. |
layout | DynamicFormViewLayoutItem[] | — | Nested layout tree (fieldset / tabpanel / tab). A fieldset accepts columns (own inner grid), collapsible and defaultCollapsed in addition to id / title / description / colSpan / items. |
enumOptions | Record<string, EnumOption[]> | — | Static option lists keyed by field slug. Absent slugs fall back to the field's inline enums / options. |
onImageUpload | (file) => Promise<StoredFileValue | null> | — | Upload handler for field-image. |
onFileUpload | (file) => Promise<StoredFileValue | null> | — | Upload handler for field-file. |
onSubmit | (payload, context) => Promise<unknown> | unknown | — | Persistence handler. When omitted, submit() just returns the built payload. |
transformSubmit | (payload, context) => payload | — | Final payload transform before onSubmit. |
onSubmitSuccess | (result, payload) => void | — | Called after a successful submit. |
onSubmitError | (error, payload) => void | — | Called after a failed submit. |
formActions | FormAction[] | null | — | Form-level lifecycle actions. |
formCustomValidations | FormCustomValidationRule[] | null | — | Form-level validation rules evaluated on submit. |
client | RestApiClient | — | Optional passthrough forwarded to field / value components (e.g. inline email compose). Never used for fetching. |
Return Value
| Property | Type | Description |
|---|---|---|
mode | DynamicFormViewMode | Resolved mode. |
item | Record<string, unknown> | Current values (alias of values). |
form | { Field(...) } | Internal form object compatible with the Docyrus form-field components. |
values | Record<string, unknown> | Current live values snapshot. |
defaultValues | Record<string, unknown> | Resolved initial values after schema defaults + seed merge. |
fields | DynamicFormViewField[] | Resolved visible field descriptors. |
allFields | DynamicFormViewField[] | Same resolved field list as fields. |
unsupportedFields | DynamicFormViewField[] | Fields shown as value-render fallbacks. |
validationErrors | Map<string, string> | Per-field validation errors keyed by slug. |
formValidationErrors | string[] | Form-level validation messages from formCustomValidations. |
isDirty | boolean | Whether current values differ from the committed baseline. |
isLoading | boolean | Always false — this hook does no fetching. |
isSubmitting | boolean | true while submit() is running. |
error | Error | null | Last submit error, if any. |
setValue | (slug, value) => void | Imperatively update one field value. |
validate | () => Promise<boolean> | Run validation without submitting. |
reset | () => void | Reset values to the committed baseline. |
submit | () => Promise<unknown> | Validate and run the submit pipeline. |
resetActionOverrides | () => void | Clear accumulated field-action / form-action property overrides. |
renderField | (slug, options?) => ReactNode | Render a single resolved field by slug. |
renderLayout | (options?) => ReactNode | Render the visible field list using the responsive grid / layout tree. |
Type Exports
| Type | Description |
|---|---|
UseDynamicFormViewOptions | Options for the hook. |
UseDynamicFormViewResult | Return value of the hook. |
DynamicFormViewMode | 'create' | 'edit' | 'view'. |
DynamicFormViewField | Resolved per-field descriptor. |
DynamicFormViewFieldLayout | Per-field override shape (same as DocyrusFormViewFieldLayout). |
DynamicFormViewLayoutItem | Layout tree node (fieldset / tabpanel / tab / field). |
DynamicFormViewSubmitContext | Context passed to onSubmit / transformSubmit. |
Out of scope
- data fetching, option loading, uploads, and persistence — you provide these
- for a batteries-included Docyrus-backed form, use
useDocyrusFormView
useDocyrusTenant
Single-line tenant integration that fetches tenant preferences, builds dateUtils + numberUtils, and wires DateFormatProvider + NumberFormatProvider so every UI component (data grids, calendars, filters, value renderers) picks up the tenant's configured date/time/number formats automatically.
useLocalDataSource
Create an in-memory Docyrus-compatible data source from a JSON array, with local CRUD and Docyrus-style query parameters.