# useDocyrusFormView URL: /docs/web/hooks/use-docyrus-form-view One-call wiring of a Docyrus data source item to create, edit, and read-only layouts with shared field-component mapping, item loading, option hydration, and submit handling. ## Installation ```bash pnpm dlx @docyrus/cli add @docyrus/hooks-use-docyrus-form-view ``` **Dependencies:** - [@docyrus/app-utils](https://www.npmjs.com/package/@docyrus/app-utils) - [@docyrus/api-client](https://www.npmjs.com/package/@docyrus/api-client) - [@tanstack/react-query](https://tanstack.com/query/latest) This hook is distributed as source. It requires an authenticated `RestApiClient` from `@docyrus/api-client` and a `QueryClientProvider` from `@tanstack/react-query` somewhere above your component tree. ## Overview `useDocyrusFormView` is the one-call entry point for Docyrus record forms and detail views. It wires together: - **data source metadata** (`fields`, enum expansions, relation targets) - **item loading** for edit/view flows - **local form state** compatible with the Docyrus form-field components - **shared field-component resolution** via [`useDocyrusFieldComponent`](/docs/web/hooks/use-docyrus-field-component) - **option hydration** for enum, user, and relation fields - **submit handling** for create/update flows - **layout helpers** (`renderField`, `renderLayout`) - **optional click-to-edit detail mode** in read-only views via [`EditableRecordDetail`](/docs/web/docyrus/editable-record-detail) - **computed fields** and imperative **field actions** driven by JSONata / query-builder rules - **form-level actions** (`onFormLoad` / `onFormBeforeSubmit` / `onFormAfterSubmit`) and **form-level validations** - **DB-free operation** — inject `dataSource` + `item` to render entirely from in-memory schema (no `getBySlug`) The result is a single hook you can use to build: - **create forms** - **edit forms** - **read-only record layouts** without writing your own per-field switch statements. > **Not on a Docyrus backend?** [`useDynamicFormView`](/docs/web/hooks/use-dynamic-form-view) is the backend-agnostic sibling of this hook. It shares the exact same rendering / computed / actions / validation engine but does no fetching — you supply the `fields`, `values`, static `enumOptions`, upload handlers, and an `onSubmit` sink to wire any backend. ## Centralized field-component mapping The hook does **not** keep its own parallel field-type registry. Instead it resolves components from the shared `useDocyrusFieldComponent` / `FORM_FIELD_MAP` source of truth: - **editable mode** → `useDocyrusFieldComponent(field.type, 'form-field')` - **read-only mode** → `useDocyrusFieldComponent(field.type, 'value-renderer')` That means `useDocyrusFormView`, `DynamicFormField`, `DynamicValue`, and `useDocyrusDataGrid` all stay aligned when a new Docyrus field type is added. Behavior is determined like this: 1. If the field type has a registered **form-field component** and the field is not read-only, it renders as an editable form input. 2. Otherwise it renders with the registered **value renderer**. 3. In **create** mode, unsupported editable types default to `unsupportedFieldBehavior='skip'`. 4. In **edit/view** mode, unsupported or read-only types default to `unsupportedFieldBehavior='value'`. ## Backend connection The hook supports three layers of backend work. ### 1) Data source metadata By default the hook loads the data source through `createDataSourceClient(client).getBySlug(appSlug, dataSourceSlug, { expand: schemaExpand })` (where `schemaExpand` defaults to `'enums'`). That fetch provides the field metadata used to: - build the local `IField` shape for Docyrus form/value components - read enum options for select-like fields - derive companion columns that must be requested for composite fields - detect relation target data sources Two options let you adapt or bypass this fetch: - **`schemaExpand`** — change or drop the `expand` query param. Pass `false`/`''` for backends (e.g. core/tenant system data sources) that don't support `expand`. - **`dataSource`** — inject a pre-resolved schema object. When provided, the `getBySlug` call is **skipped entirely** and every downstream derivation reads from your object instead. See [DB-free / in-memory schema](#db-free--in-memory-schema). ### 2) Item loading For **edit** and **view** flows, the hook resolves the record in this precedence order: 1. **`item`** — pre-resolved object; skips the item query entirely 2. **`collection.get(recordId, params)`** — generated/custom collection mode 3. **Direct API** — `GET /v1/apps/:appSlug/data-sources/:dataSourceSlug/items/:itemId` The hook always sends `columns`, including companion fields needed by composite renderers. ### 3) Remote option loading When needed, the hook hydrates option lists for dynamic selectors: - **enum-backed select fields** (`field-select`, `field-radioGroup`, `field-enum`, `field-systemEnum`, `field-status`, `field-multiSelect`, `field-tagSelect`) → options normally arrive inline with the schema. When a field's inline options come back empty, the hook falls back to a single tenant-wide `GET /v1/apps/enums` (shared cache key, 30 min stale time) and reads that field's options from the tree; with nothing missing the request is never made - **user fields** (`field-userSelect`, `field-userMultiSelect`) → `GET /v1/users` - **relation fields** (`field-relation`) → 1. `GET /v1/apps/data-sources?expand=fields` 2. resolve the target by `relationDataSourceId` 3. query target items with a minimal `columns` set for labels + item mapping fields You can disable user / relation fetches via `resolveUserOptions={false}` / `resolveRelationOptions={false}` or override any field with `enumOptions={{ [slug]: [...] }}`. ## Usage Below are four complete usage patterns for the same Docyrus data source. ### 1) Create Item Use `mode: 'create'` when you want to start from defaults and submit a brand-new record. ```tsx 'use client'; import { useDocyrusAuth } from '@docyrus/signin'; import { useDocyrusFormView } from '@docyrus/ui/library/hooks/use-docyrus-form-view'; import { Button } from '@docyrus/ui/primitives/ui/button'; export function CreateContactForm() { const { client } = useDocyrusAuth(); if (!client) return null; const createView = useDocyrusFormView({ client, appSlug: 'crm', dataSourceSlug: 'contacts', mode: 'create', gridColumns: 2, defaultValues: { status: 'lead' }, fieldOrder: ['full_name', 'email', 'phone', 'status', 'notes'], fieldLayout: { notes: { colSpan: 'full' } } }); return (
); } ``` ### 2) Edit Item Use `mode: 'edit'` with an `itemId` to load an existing record, keep the same field mapping, and submit updates back to Docyrus. ```tsx 'use client'; import { useDocyrusAuth } from '@docyrus/signin'; import { useDocyrusFormView } from '@docyrus/ui/library/hooks/use-docyrus-form-view'; import { Button } from '@docyrus/ui/primitives/ui/button'; export function EditContactForm({ contactId }: { contactId: string }) { const { client } = useDocyrusAuth(); if (!client) return null; const editView = useDocyrusFormView({ client, appSlug: 'crm', dataSourceSlug: 'contacts', itemId: contactId, mode: 'edit', gridColumns: 2, fieldOrder: ['full_name', 'email', 'phone', 'status', 'notes'], fieldLayout: { notes: { colSpan: 'full' }, status: { description: 'Primary pipeline status' } } }); return ( ); } ``` ### 3) View Item Use `mode: 'view'` to render the same record as a read-only detail layout. ```tsx 'use client'; import { useDocyrusAuth } from '@docyrus/signin'; import { useDocyrusFormView } from '@docyrus/ui/library/hooks/use-docyrus-form-view'; export function ContactDetail({ contactId }: { contactId: string }) { const { client } = useDocyrusAuth(); if (!client) return null; const detailView = useDocyrusFormView({ client, appSlug: 'crm', dataSourceSlug: 'contacts', itemId: contactId, mode: 'view', gridColumns: 2, fieldOrder: ['full_name', 'email', 'phone', 'status', 'notes'] }); return detailView.renderLayout(); } ``` ### 4) View Item Click to Edit When `clickToEdit` is enabled and `renderLayout()` is called in `view` mode, the hook swaps the plain value grid for [`EditableRecordDetail`](/docs/web/docyrus/editable-record-detail). Fields stay read-only visually until the user clicks into a row, then saves inline changes through the normal Docyrus update pipeline. ```tsx 'use client'; import { useDocyrusAuth } from '@docyrus/signin'; import { useDocyrusFormView } from '@docyrus/ui/library/hooks/use-docyrus-form-view'; export function ContactInlineDetail({ contactId }: { contactId: string }) { const { client } = useDocyrusAuth(); if (!client) return null; const inlineDetailView = useDocyrusFormView({ client, appSlug: 'crm', dataSourceSlug: 'contacts', itemId: contactId, mode: 'view', clickToEdit: true, fieldOrder: ['full_name', 'email', 'phone', 'status', 'notes'] }); return inlineDetailView.renderLayout(); } ``` ### Nested sections with fieldset + tabpanel + tab Use `layout` when a plain flat grid is not enough. Every section renders its own grid with the active `gridColumns` count, and both fields and sections can span multiple columns. ```tsx 'use client'; import { useDocyrusAuth } from '@docyrus/signin'; import { useDocyrusFormView } from '@docyrus/ui/library/hooks/use-docyrus-form-view'; import { Button } from '@docyrus/ui/primitives/ui/button'; const contactLayout = [ { id: 'identity', variant: 'fieldset', title: 'Identity', colSpan: 2, items: [ { type: 'field', slug: 'full_name', colSpan: 2 }, 'email', 'phone' ] }, { id: 'sales-workspace', variant: 'tabpanel', title: 'Sales Workspace', colSpan: 'full', defaultTabId: 'overview', items: [ { id: 'overview', variant: 'tab', title: 'Overview', items: [ { type: 'field', slug: 'status', colSpan: 2 }, 'owner', { type: 'field', slug: 'expected_value', colSpan: 2 }, { type: 'field', slug: 'next_step', colSpan: 2 } ] }, { id: 'notes', variant: 'tab', title: 'Notes', items: [{ type: 'field', slug: 'notes', colSpan: 'full' }] } ] } ] as const; export function EditContactWorkspace({ contactId }: { contactId: string }) { const { client } = useDocyrusAuth(); if (!client) return null; const formView = useDocyrusFormView({ client, appSlug: 'crm', dataSourceSlug: 'contacts', itemId: contactId, mode: 'edit', gridColumns: 4, layout: contactLayout }); return ( ); } ``` ### Collection mode If your app already has a generated Docyrus collection, pass it in and the hook will use `collection.get`, `collection.create`, and `collection.update` instead of raw endpoints. ```tsx const contacts = useCrmContactsCollection(); const formView = useDocyrusFormView({ client, appSlug: 'crm', dataSourceSlug: 'contacts', itemId, collection: contacts }); ``` ### DB-free / in-memory schema Pass `dataSource` to render a form from schema you already hold in memory — the hook never calls `getBySlug`. Combine it with `item` (record data) and `layout` for a form that performs **no network requests at all** (useful for previews, the form builder, snapshot tests, or system data sources without a metadata route). Setting `enabled: false` additionally stops the relation-option fetch. ```tsx 'use client'; import { useDocyrusFormView } from '@docyrus/ui/library/hooks/use-docyrus-form-view'; import type { DataSource } from '@docyrus/app-utils'; // Schema + record already resolved elsewhere (cache, props, fixture, …) const contactSchema: DataSource = { id: 'ds_contacts', slug: 'contacts', fields: [ { id: '1', name: 'Full Name', slug: 'full_name', type: 'field-text' }, { id: '2', name: 'Status', slug: 'status', type: 'field-select', options: { /* … */ } } ] // …other DataSource metadata } as DataSource; export function ContactPreview({ record }: { record: Record