Hooks

useDynamicFormView

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.

iOSAndroidExpo Go

Installation

pnpm dlx @docyrus/cli add @docyrus/rn-hooks-use-dynamic-form-view
Required Packages(2 packages)
pnpm add jsonata @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) 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

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 (
    <ScrollView contentContainerClassName="gap-4 p-4">
      {view.formValidationErrors.length > 0 && (
        <Alert variant="destructive" title="Please fix the form" description={view.formValidationErrors.join('\n')} />
      )}
      {view.renderLayout()}
      <Button onPress={() => view.submit()} loading={view.isSubmitting}>Save</Button>
    </ScrollView>
  );
}

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:

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:

SectionNative renderingKeys
fieldsetBordered panel; with collapsible the title toggles a native Collapsibleid, title, description, items, columns, collapsible, defaultCollapsed, colSpan, className, contentClassName
tabpanelBordered panel with scrollable underline Tabsid, title, description, items: tab[], defaultTabId, colSpan
tabOne 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 (__<slug>_currency, __<slug>_country, status sub-fields, avatar mapping) are submitted with their field.

API Reference

PropTypeDefaultDescription
fieldsIField[]requiredField schema (already normalized to IField)
mode'create' | 'edit' | 'view''create' (no values) / 'edit' (with values)Form mode
initialValuesRecord<string, unknown>—Uncontrolled seed — merged over field defaults; re-seeds when its content changes
valuesRecord<string, unknown>—Controlled values — the form re-seeds whenever its content changes
onValuesChange(values: Record<string, unknown>) => void—Fires on every field change (controlled and uncontrolled)
disabledbooleanfalseDisable every field
clickToEditbooleanfalseIn view mode, render through EditableRecordDetail (inline edit + save)
includeReadOnlyFieldsbooleanmode !== '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
gridColumns1 | 2 | 3 | 42Form 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
fieldSlugsstring[]—Whitelist of field slugs to render
fieldOrderstring[]—Explicit field order (unlisted fields sort by name)
hiddenFieldSlugsstring[]—Field slugs to hide
fieldLayoutRecord<string, DocyrusFormViewFieldLayout>—Per-field overrides: hidden / required fns, readOnly, disabled, colSpan, label, description, fieldProps, valueProps, computed*, fieldActions, customValidations, validations
layoutDocyrusFormViewLayoutItem[]—Section tree: fieldset (columns, collapsible, defaultCollapsed) / tabpanel / tab, field items with colSpan
enumOptionsRecord<string, EnumOption[]>—Option lists keyed by field slug (overrides inline enums)
onImageUpload(file: NativeFile) => Promise<StoredFileValue | null>—Image field upload handler (asset from expo-image-picker)
onFileUpload(file: NativeFile) => Promise<StoredFileValue | null>—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<string, unknown>—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
formActionsFormAction[] | null—Form lifecycle actions (onFormLoad / onFormBeforeSubmit / onFormAfterSubmit)
formCustomValidationsFormCustomValidationRule[] | null—Form-level JSONata validations — errors land in formValidationErrors
clientRestApiClient—Optional passthrough to field / value components (never used for fetching)

Return Value

KeyTypeDescription
modeDocyrusFormViewModeResolved mode
item / valuesRecord<string, unknown>Live form values
defaultValuesRecord<string, unknown>Initial values (field defaults + record)
form{ Field }Render-prop form.Field for custom field UI ({ name, state: { value, meta }, handleChange, handleBlur })
fields / allFieldsDocyrusFormViewField[]Resolved, visible fields (required / hidden / readOnly / disabled / renderMode / enforcedValidations / submitKeys …)
unsupportedFieldsDocyrusFormViewField[]Fields rendered as values because no native editor exists
validationErrorsMap<string, string>Per-field errors from the last validate()
formValidationErrorsstring[]Form-level formCustomValidations errors — render them as an Alert banner
isDirtybooleanValues differ from the baseline (empty values normalized)
isLoadingbooleanData still loading
isSubmittingbooleanA submit is in flight
errorError | nullLoad / submit error
setValue(slug, value) => voidProgrammatic write
validate() => Promise<boolean>required → tokens → JSONata customValidations, then form-level validations
reset() => voidRestore the baseline
submit() => Promise<unknown>Validate → onFormBeforeSubmit → build payload → persist → onFormAfterSubmit
resetActionOverrides() => voidClear field-action and form-action property overrides
renderField(slug, options?) => ReactNodeRender one field (form or value mode)
renderLayout(options?) => ReactNodeRender the whole layout (sections / tabs / grid, or EditableRecordDetail with clickToEdit)

Type Exports

TypeDescription
UseDynamicFormViewOptionsHook options
UseDynamicFormViewResultHook result
DynamicFormViewCallbackContextonSubmit / transformSubmit context (mode, itemId, values, fields)
DynamicFormViewMode / DynamicFormViewRenderMode / DynamicFormViewValidationTokenModeMode unions
DynamicFormViewLayoutItem / DynamicFormViewSection / DynamicFormViewFieldsetSection / DynamicFormViewTabPanelSection / DynamicFormViewTabSection / DynamicFormViewLayoutFieldItem / DynamicFormViewColSpanLayout tree types
DynamicFormViewFieldLayoutPer-field overrides
DynamicFormViewFieldResolved field
DynamicFormViewRenderFieldOptions / DynamicFormViewRenderLayoutOptionsrenderField / renderLayout options
DynamicFormViewSubmitContextEngine submit context

On this page