# useDynamicFormView URL: /docs/native/hooks/use-dynamic-form-view Backend-agnostic form engine for React Native. Renders an IField schema with sections, tabs, collapsible panels, computed fields, field and form actions, validation tokens and a submit payload builder, with no network I/O. ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/rn-hooks-use-dynamic-form-view ``` **Dependencies:** - [jsonata](https://www.npmjs.com/package/jsonata) - [@react-querybuilder/core](https://www.npmjs.com/package/@react-querybuilder/core) The native port of the web hook with the same signature. It drives the shared form-view engine (the same one behind [`useDocyrusFormView`](/docs/native/hooks/use-docyrus-form-view)) but fetches nothing: you pass the `IField[]` schema, the values, static option lists, upload handlers and an `onSubmit` sink. Fields render through the native `DocyFieldDynamic` controls; view mode renders the value renderers, or `EditableRecordDetail` with `clickToEdit`. ## Usage ```tsx import { ScrollView } from 'react-native'; import { Alert } from '@/components/docyrus-native/alert'; import { Button } from '@/components/docyrus-native/button'; import { type IField } from '@/components/docyrus-native/form-fields'; import { useDynamicFormView } from '@/hooks/docyrus-native/use-dynamic-form-view'; const fields: IField[] = [ { id: '1', name: 'Title', slug: 'title', type: 'field-text', validations: ['required', 'minLength:3'] }, { id: '2', name: 'Quantity', slug: 'quantity', type: 'field-number' }, { id: '3', name: 'Unit price', slug: 'unit_price', type: 'field-money' }, { id: '4', name: 'Total', slug: 'total', type: 'field-number', readOnly: true } ]; export function OrderForm() { const view = useDynamicFormView({ fields, validationTokens: 'all', layout: [ { id: 'main', variant: 'fieldset', title: 'Order', items: [{ type: 'field', slug: 'title', colSpan: 'full' }, 'quantity', 'unit_price', 'total'] } ], fieldLayout: { total: { computedFormula: '$number(quantity) * $number(unit_price)' } }, onSubmit: payload => saveOrder(payload) }); return ( ); } ``` ## Uploads `onImageUpload` / `onFileUpload` receive the picked asset as a `NativeFile` (`{ uri, name, type, size? }` from `expo-image-picker` / `expo-document-picker`) instead of a DOM `File`. Upload it and resolve to a `StoredFileValue`: ```tsx onImageUpload: async (file) => { const formData = new FormData(); formData.append('file', { uri: file.uri, name: file.name, type: file.type } as never); return uploadToStorage(formData); // → { file_name, source, signed_url, file_type, file_size } } ``` Per-field async option loaders are wired through `fieldLayout[slug].fieldProps` (`onSearch` / `searching` / `onLoadMore` / `hasMore` / `onExpand`). ## Layout `layout` is a tree of field slugs, `{ type: 'field', slug, colSpan }` items and sections: | Section | Native rendering | Keys | |---------|------------------|------| | `fieldset` | Bordered panel; with `collapsible` the title toggles a native `Collapsible` | `id`, `title`, `description`, `items`, `columns`, `collapsible`, `defaultCollapsed`, `colSpan`, `className`, `contentClassName` | | `tabpanel` | Bordered panel with scrollable underline `Tabs` | `id`, `title`, `description`, `items: tab[]`, `defaultTabId`, `colSpan` | | `tab` | One tab page (inside a `tabpanel`) | `id`, `title`, `description`, `items`, `className`, `contentClassName` | Fields not placed in the layout are appended after it. The grid is a flex-wrap grid: **phones render one column**, tablets honor `gridColumns` / `columns` (capped at 2 below 900pt) and `colSpan` (`1`–`4` or `'full'`). Sections span the full row unless they carry a `colSpan`. ## Form-level styling `labelAlign` / `labelWidth` / `fieldSize` / `fieldVariant` are resolved into field props (`getFormLayoutFieldProps` in `form-fields/lib/form-layout`) instead of the web descendant CSS selectors (Uniwind has none): `labelAlign: 'left'` → `labelAlignment: 'left'` + a fixed label column (`sm` w-24 / `md` w-40 / `lg` w-56), `fieldSize` → field `size`, `fieldVariant: 'outline' | 'filled'` → `'outlined' | 'filled'`. Explicit `fieldLayout[slug].fieldProps` win. ## Computed fields, actions and validation - **Computed** (`fieldLayout[slug]` or `IField`): `computedHidden` / `computedRequired` (JSONata string or query-builder JSON), `computedLabel` / `computedDescription` (JSONata → string), `computedFormula` (JSONata → value written back into the field). Expressions evaluate against the live values (`quantity * unit_price`). - **Field actions** (`fieldActions`, trigger `onFieldChange`): blocks of IF / ELSE IF (`conditionalItems`) / ELSE (`elseActions`) / ALWAYS (`unconditionalActions`) with the 8 step methods `setFieldValue`, `setFieldValues`, `clearFieldValue`, `showField`, `hideField`, `setFieldRequired`, `setFieldDisabled`, `setFieldReadOnly`. - **Form actions** (`formActions`): the same blocks on `onFormLoad`, `onFormBeforeSubmit` and `onFormAfterSubmit` (`$result` bound). - **Priority**: computed formulas > field-action overrides > form-action overrides > `fieldLayout` callbacks > `IField` defaults. - **Validation order per field**: `required` (always) → tokens (`validationTokens`: `'off'` advisory, `'form'` only `fieldLayout.validations`, `'all'` also the field's own tokens; resolved list on `field.enforcedValidations`, also used for the fields' live hints) → JSONata `customValidations` (`value` + `values` bound). Form-level `formCustomValidations` run once every field passes. - **Payload**: read-only and disabled fields are excluded; companion columns (`___currency`, `___country`, status sub-fields, avatar mapping) are submitted with their field. ## API Reference | Prop | Type | Default | Description | |------|------|---------|-------------| | `fields` | `IField[]` | required | Field schema (already normalized to IField) | | `mode` | `'create' \| 'edit' \| 'view'` | `'create' (no values) / 'edit' (with values)` | Form mode | | `initialValues` | `Record` | — | Uncontrolled seed — merged over field defaults; re-seeds when its content changes | | `values` | `Record` | — | Controlled values — the form re-seeds whenever its content changes | | `onValuesChange` | `(values: Record) => void` | — | Fires on every field change (controlled and uncontrolled) | | `disabled` | `boolean` | `false` | Disable every field | | `clickToEdit` | `boolean` | `false` | In view mode, render through EditableRecordDetail (inline edit + save) | | `includeReadOnlyFields` | `boolean` | `mode !== 'create'` | Render read-only fields as values | | `unsupportedFieldBehavior` | `'skip' \| 'value'` | `create ? 'skip' : 'value'` | Field types without a native editor: skip or render as a value | | `gridColumns` | `1 \| 2 \| 3 \| 4` | `2` | Form grid columns (phones render 1 column; tablets up to 2 below 900pt) | | `validationTokens` | `'off' \| 'form' \| 'all'` | `'off'` | Enforce minLength / maxLength / pattern / min / max tokens on submit (required is always enforced) | | `labelAlign` | `'top' \| 'left'` | `'top'` | Form-level label placement | | `labelWidth` | `'sm' \| 'md' \| 'lg'` | `'md'` | Label column width when labelAlign is left | | `fieldSize` | `'sm' \| 'md' \| 'lg'` | — | Form-level field density | | `fieldVariant` | `'outline' \| 'filled'` | — | Form-level input style | | `fieldSlugs` | `string[]` | — | Whitelist of field slugs to render | | `fieldOrder` | `string[]` | — | Explicit field order (unlisted fields sort by name) | | `hiddenFieldSlugs` | `string[]` | — | Field slugs to hide | | `fieldLayout` | `Record` | — | Per-field overrides: hidden / required fns, readOnly, disabled, colSpan, label, description, fieldProps, valueProps, computed*, fieldActions, customValidations, validations | | `layout` | `DocyrusFormViewLayoutItem[]` | — | Section tree: fieldset (columns, collapsible, defaultCollapsed) / tabpanel / tab, field items with colSpan | | `enumOptions` | `Record` | — | Option lists keyed by field slug (overrides inline enums) | | `onImageUpload` | `(file: NativeFile) => Promise` | — | Image field upload handler (asset from expo-image-picker) | | `onFileUpload` | `(file: NativeFile) => Promise` | — | File field upload handler (asset from expo-document-picker) | | `onSubmit` | `(payload, context) => unknown` | — | Persistence handler — omitted: submit() returns the built payload | | `transformSubmit` | `(payload, context) => Record` | — | Rewrite the built payload before persistence | | `onSubmitSuccess` | `(result, payload) => void` | — | Called after a successful submit | | `onSubmitError` | `(error, payload) => void` | — | Called when validation or persistence fails | | `formActions` | `FormAction[] \| null` | — | Form lifecycle actions (onFormLoad / onFormBeforeSubmit / onFormAfterSubmit) | | `formCustomValidations` | `FormCustomValidationRule[] \| null` | — | Form-level JSONata validations — errors land in formValidationErrors | | `client` | `RestApiClient` | — | Optional passthrough to field / value components (never used for fetching) | ## Return Value | Key | Type | Description | |-----|------|-------------| | `mode` | `DocyrusFormViewMode` | Resolved mode | | `item` / `values` | `Record` | Live form values | | `defaultValues` | `Record` | Initial values (field defaults + record) | | `form` | `{ Field }` | Render-prop `form.Field` for custom field UI (`{ name, state: { value, meta }, handleChange, handleBlur }`) | | `fields` / `allFields` | `DocyrusFormViewField[]` | Resolved, visible fields (required / hidden / readOnly / disabled / renderMode / enforcedValidations / submitKeys …) | | `unsupportedFields` | `DocyrusFormViewField[]` | Fields rendered as values because no native editor exists | | `validationErrors` | `Map` | Per-field errors from the last `validate()` | | `formValidationErrors` | `string[]` | Form-level `formCustomValidations` errors — render them as an `Alert` banner | | `isDirty` | `boolean` | Values differ from the baseline (empty values normalized) | | `isLoading` | `boolean` | Data still loading | | `isSubmitting` | `boolean` | A submit is in flight | | `error` | `Error \| null` | Load / submit error | | `setValue` | `(slug, value) => void` | Programmatic write | | `validate` | `() => Promise` | required → tokens → JSONata `customValidations`, then form-level validations | | `reset` | `() => void` | Restore the baseline | | `submit` | `() => Promise` | Validate → `onFormBeforeSubmit` → build payload → persist → `onFormAfterSubmit` | | `resetActionOverrides` | `() => void` | Clear field-action and form-action property overrides | | `renderField` | `(slug, options?) => ReactNode` | Render one field (form or value mode) | | `renderLayout` | `(options?) => ReactNode` | Render the whole layout (sections / tabs / grid, or `EditableRecordDetail` with `clickToEdit`) | ## Type Exports | Type | Description | |------|-------------| | `UseDynamicFormViewOptions` | Hook options | | `UseDynamicFormViewResult` | Hook result | | `DynamicFormViewCallbackContext` | `onSubmit` / `transformSubmit` context (`mode`, `itemId`, `values`, `fields`) | | `DynamicFormViewMode` / `DynamicFormViewRenderMode` / `DynamicFormViewValidationTokenMode` | Mode unions | | `DynamicFormViewLayoutItem` / `DynamicFormViewSection` / `DynamicFormViewFieldsetSection` / `DynamicFormViewTabPanelSection` / `DynamicFormViewTabSection` / `DynamicFormViewLayoutFieldItem` / `DynamicFormViewColSpan` | Layout tree types | | `DynamicFormViewFieldLayout` | Per-field overrides | | `DynamicFormViewField` | Resolved field | | `DynamicFormViewRenderFieldOptions` / `DynamicFormViewRenderLayoutOptions` | `renderField` / `renderLayout` options | | `DynamicFormViewSubmitContext` | Engine submit context |