# useDynamicFormView URL: /docs/web/hooks/use-dynamic-form-view 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 ```bash pnpm dlx @docyrus/cli add @docyrus/hooks-use-dynamic-form-view ``` **Dependencies:** - [@tanstack/react-query](https://tanstack.com/query/latest) - [jsonata](https://www.npmjs.com/package/jsonata) This hook is distributed as source. It shares the same rendering engine as [`useDocyrusFormView`](/docs/web/hooks/use-docyrus-form-view) 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`](/docs/web/hooks/use-docyrus-form-view). 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`](/docs/web/hooks/use-docyrus-field-component) - **layout helpers** (`renderField`, `renderLayout`) including nested `fieldset` / `tabpanel` / `tab` sections - **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 the `minLength:` / `maxLength:` / `pattern:` / `min:` / `max:` tokens when `validationTokens` is enabled, then JSONata `customValidations`, then form-level rules - **companion columns / submit keys** for composite fields (money, phone, status, avatar) - **click-to-edit** detail mode via [`EditableRecordDetail`](/docs/web/docyrus/editable-record-detail) 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`](/docs/web/hooks/use-docyrus-form-view) instead. ## Usage ### Create form (static options + onSubmit) ```tsx '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 = { 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 (
{ event.preventDefault(); await form.submit(); }} className="space-y-4"> {form.renderLayout()}
); } ``` ### 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. ```tsx 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. ```tsx 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 ```tsx 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`](/docs/web/hooks/use-docyrus-form-view) works identically — `layout`, `fieldLayout` (including `computedHidden` / `computedFormula` / `fieldActions`), `formActions`, and `formCustomValidations`. ```tsx 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](/docs/web/hooks/use-docyrus-form-view#computed-fields) 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. ```tsx 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. ```tsx 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` | — | Uncontrolled seed, merged over field defaults. Re-seeds when its content changes. | | `values` | `Record` | — | 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` | — | 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` | — | Static option lists keyed by field slug. Absent slugs fall back to the field's inline `enums` / `options`. | | `onImageUpload` | `(file) => Promise` | — | Upload handler for `field-image`. | | `onFileUpload` | `(file) => Promise` | — | Upload handler for `field-file`. | | `onSubmit` | `(payload, context) => Promise \| 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` | Current values (alias of `values`). | | `form` | `{ Field(...) }` | Internal form object compatible with the Docyrus form-field components. | | `values` | `Record` | Current live values snapshot. | | `defaultValues` | `Record` | 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` | 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` | Run validation without submitting. | | `reset` | `() => void` | Reset values to the committed baseline. | | `submit` | `() => Promise` | 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`](/docs/web/hooks/use-docyrus-form-view)