Skip to content

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).

Every surface follows the same two-request pattern, wired through a hook:

GET /object/{name}/metadata ┐
POST /object/{name}/table ├──▶ builder ──▶ GenieTable / GenieForm / GenieView
POST /object/{name}/form|view ┘
Prefer a picture?
  • Structure is fetched once and held (schemaRef in 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.

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 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 Create permission.
  • 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 Delete permission).
  • A table-level Actions dropdown for view-level actions.
  • Right side: a quick-search box (only when the view is Searchable — an OR-gated LIKE across 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’s StyleClasses, its evaluated StyleClassesExpression, and the host’s optional cellClass resolver.
  • Row actions — a per-row menu. A Link action navigates (SPA or new tab) after {Column} token substitution; a Confirm action prompts first; a Prompt action collects inputs in a modal; others POST to /object/execute-row-action. Buttons are coloured by a Color value mapped to a gx-{key} class (row actions fall back to gx-info; the standard Edit is gx-primary, Delete gx-danger).
  • Sub-views — a row expands to a full-width panel hosting a nested table or form (SubViewPanel), scoped to the parent row via Parent__{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 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). A wizard layout 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 fieldsGenieField dispatches on EditorType to 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 / Hidden and computed Value are 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). GenieForm itself only emits the collected values via onSubmit; the actual POST /object/submit and 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 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.