Components: Table, Form, View
GenieTable, GenieForm, and GenieView are the three render surfaces for a Genie object. They
share one contract: fetch the object’s structure once, fetch a page of rows or a record’s
values as needed, merge the two client-side, and render. The server ships no markup — the
React components reproduce the genie-* DOM themselves, and even the container/view ids are generated
in the browser.
Which of the three renders is decided by the router from the route
(/table|form|view/{name}); a single object can appear as all three (a grid, an editable form, a
read-only view).
The metadata + values merge
Section titled “The metadata + values merge”Every surface follows the same two-request pattern, wired through a hook:
GET /object/{name}/metadata ┐POST /object/{name}/table ├──▶ builder ──▶ GenieTable / GenieForm / GenieViewPOST /object/{name}/form|view ┘- Structure is fetched once and held (
schemaRefin the hook; also cached in the API client). Schemas only change on a backend redeploy, so re-navigating re-fetches only data, not structure. - Data re-fetches whenever inputs change — a new page, sort, filter, or record.
- The hook then calls a builder (
buildTableModel/buildFormModel) that merges schema + data into the render model the component consumes.
| Surface | Hook | Structure request | Data request | Builder |
|---|---|---|---|---|
GenieTable |
useGenieTable |
getObjectMetadata |
getTableData (/table) |
buildTableModel |
GenieForm |
useGenieForm |
getObjectMetadata |
getFormData (/form) |
buildFormModel |
GenieView |
useGenieForm (readOnly) |
getObjectMetadata |
getViewData (/view) |
buildFormModel |
GenieView reuses useGenieForm with readOnly: true — the same schema + values, forced into view
mode server-side. See Object endpoints for the wire contract and
the structure-vs-data split for why the boundary exists.
Client-generated ids
Section titled “Client-generated ids”Each hook generates a stable per-instance ViewId (crypto.randomUUID(), falling back to a
timestamp) and sends it with every request; the form/table container id is composed from it
client-side (e.g. `${model.Name}_${model.ViewId}_form`). The comment in the source is explicit —
“the server no longer ships one.” The id also correlates uploads and scopes the rendered DOM.
GenieTable — the grid
Section titled “GenieTable — the grid”GenieTable renders a toolbar, an optional filter row, the sortable data grid, row actions,
expandable sub-views, and pagination. It’s a controlled component: useGenieTable owns page/sort/
filter/search state and passes callbacks in.
Toolbar (suppressed entirely when the view sets DisableControls):
- Add New — the only labelled button, shown only with the
Createpermission. - Refresh, Export, Import, Columns (the column manager: show/hide + reorder). Export/Import are split buttons — a default handler plus any custom export/import handlers as dropdown items, each permission-gated.
- Filters toggle → reveals the per-column filter row; Apply (enabled only when the draft changed) and Clear.
- Select mode → row checkboxes for bulk delete (only with the
Deletepermission). - A table-level Actions dropdown for view-level actions.
- Right side: a quick-search box (only when the view is
Searchable— an OR-gatedLIKEacross the view’s searchable columns), the page-size select, and a “Showing X to Y of Z” range.
Grid body:
- Sorting — sortable headers carry independent asc/desc controls; the active column/direction is reflected back from the model.
- Per-column filters — booleans render a tri-state select, dates a date input, and text/number a
operator select (contains / equal / not-equal /
>/</>=/<=) plus a value; edited as a draft and applied on demand. - Cells — text (with an optional client-side display
Expression), booleans, dates (with optional self-updating relative time), attachment tags (thumbnails + file pills), and lookup links that open a linked record read-only in a dialog. Cell classes combine the column’sStyleClasses, its evaluatedStyleClassesExpression, and the host’s optionalcellClassresolver. - Row actions — a per-row menu. A
Linkaction navigates (SPA or new tab) after{Column}token substitution; aConfirmaction prompts first; aPromptaction collects inputs in a modal; others POST to/object/execute-row-action. Buttons are coloured by aColorvalue mapped to agx-{key}class (row actions fall back togx-info; the standard Edit isgx-primary, Deletegx-danger). - Sub-views — a row expands to a full-width panel hosting a nested table or form (
SubViewPanel), scoped to the parent row viaParent__{Column}parameters. See Sub-views.
Pagination is windowed (first/last always shown, ±1 around the current page, ellipses between), and hidden when there’s a single page.
GenieForm — the editable form
Section titled “GenieForm — the editable form”GenieForm renders the record’s layout tree (GenieFormLayout) and Submit/Cancel actions.
- Layout — the builder produces a tree of nodes the layout renderer walks: section
(
<fieldset>with a legend, optionally an accordion), row (a 12-column track), column (col-{Width}, default 12), field, line (<hr>), grid, tab, and sub (a nested sub-view). Awizardlayout type turns each top-level tab into a step with Back/Next/Submit. When a view declares no layout, fields render as one flat row. See Form layout. - Editor fields —
GenieFielddispatches onEditorTypeto the right editor: Boolean (checkbox / switch), Select (static, dataset-backed searchable, or a modal selector), Text (single-line / multiline / lazy-loaded rich text), Number, Email, Password, Date/Time/DateTime (with UTC↔local conversion), Attachment, CartTable, and Sequence. See Editor fields. - Dynamic state — each field’s
Required/Disabled/Hiddenand computedValueare resolved live by the expression engine as values change; a role/expression lock renders the field read-only. - Validation — before submit, a client-side check flags any field that is required, visible, and enabled but empty: those groups highlight, a warning toast appears, and the form scrolls to the first. This is a UX convenience — required-ness is re-validated on the server.
- Submit — the Submit button is gated on
CanSubmit(a UI hint; the server re-checks Create/Update).GenieFormitself only emits the collectedvaluesviaonSubmit; the actualPOST /object/submitand any follow-on dispatch (navigate, refresh, or a workflow start/transition) are handled by the caller (the router). Server errors surface as an error toast.
GenieView — the read-only view
Section titled “GenieView — the read-only view”GenieView renders the same layout tree in read-only form (the React equivalent of the legacy
view renderer). It walks the layout, skips hidden fields, and formats each value for display:
booleans as a disabled checkbox, Select values resolved to their option label (dataset-backed
selects resolve the label via /object/field-dataset), rich text sanitized with DOMPurify before
insertion, attachments as tags, a CartTable as a read-only nested sub-view (never raw JSON), and
empty values as an em-dash.
Its action bar (GenieViewActions) offers a single “More actions” menu: Edit (with the Update
permission, navigating to the form route), Delete (with Delete), and any visible custom row
actions — the same action semantics as the grid.