Skip to main content

Nested tabs

A dialog with all its fields on one form covers the single-row case. When a row has related data — a customer with several addresses, a user with per-application settings, an order with line items — you embed that related data right inside the dialog as a nested tab.

Two variants:

Tab typeWhat it showsWhen
nested_formA child-record form: zero-or-one related row, with its own SELECT / INSERT / UPDATE queries.When the related table has at most one row per parent (one-to-one or zero-to-one).
nested_tableA full TableView of related rows, embedded inline — renders another screen's grid scoped to the parent's key.When the related table has many rows per parent (one-to-many).

Both attach to the dialog like any other tab; the user clicks between them without leaving the form.


When to reach for each

PatternChoose
One customer → at most one billing address.nested_form.
One customer → many orders.nested_table.
One application → its JD Edwards-specific settings (zero-or-one row).nested_form.
One project → its line items.nested_table.
One employee → their current job assignment (one row).nested_form.
One employee → their job assignment history.nested_table.

The schema's _check validator enforces tab id uniqueness; otherwise both types can coexist in the same dialog — a Customer dialog with a nested_form on Billing and a nested_table on Orders is a natural shape.


nested_form — a child-record form

A nested_form tab embeds a sub-dialog that reads / writes a related table. The parent's primary key binds into the nested read query; if a row comes back, the form renders in edit mode; if not, it renders in add mode.

Inspector · Tab "Billing" · type=nested_formconnector(parent's connector — crm)read_query *billing_address_get ▾update_querybilling_address_put ▾insert_querybilling_address_post ▾PARAM BINDS (parent → nested):CUSTOMER_ID← parent columnCUSTOMER_ID(the parent dialog's current row)+ Add bindingFIELDSDrag fields from the Palette as on a normal form tab.

Fields

FieldNotes
connectorConnector hosting the nested queries. Blank = parent's connector.
read_query (required)Reads the linked row. Should return 0 or 1 rows after the bind narrows it.
update_queryWrites edits when a linked row already exists.
insert_queryWrites a new linked row when none existed (after the bind returned 0 rows).
param_bindsBind the parent's columns into the nested queries' :placeholder params. Typically :CUSTOMER_ID ← CUSTOMER_ID.
fieldsThe form fields, just like a regular form tab. Drag from the Palette.
colsGrid column count for the nested form.

Behaviour

When the parent dialog opens:

  1. The read_query fires with the parent's column values bound through param_binds.
  2. If a row comes back, the nested form renders in edit mode — fields are populated, Save fires update_query.
  3. If no row comes back, the nested form renders in add mode — fields are empty, Save fires insert_query.

The parent's Save and the nested tab's Save are independent — saving the parent doesn't touch the nested data; saving the nested doesn't trigger the parent's Save chain. Each operates on its own row.

Use case — JD Edwards-specific settings

A Settings → Applications dialog with a JD Edwards tab that shows up only when the application is a JDE app:

[[screens.nomasx1.settings_applications.dialog.tabs]]
id = "jde"
label = "JD Edwards"
type = "nested_form"
read_query = "settings_jde_get"
update_query = "settings_jde_put"
insert_query = "settings_jde_post"

[[screens.nomasx1.settings_applications.dialog.tabs.param_binds]]
param = "APPS_ID"
source = "APPS_ID"

[[screens.nomasx1.settings_applications.dialog.tabs.fields]]
name = "JDE_VERSION"

[[screens.nomasx1.settings_applications.dialog.tabs.fields]]
name = "JDE_ENVIRONMENT"

When the user opens the Applications dialog on a JDE app (APPS_ID = some value), the JDE tab loads the matching settings row and lets the user edit it. For non-JDE apps where no row exists, the same tab pre-fills the form so the user can add one.


A nested_table tab embeds another screen's TableView inline. No new fields — the entire grid + dialog of the target screen renders inside the tab, scoped to the parent's key.

Inspector · Tab "Deals" · type=nested_tableconnector(parent's connector — crm)screen *deals ▾PARAM BINDS (parent → nested screen's read_query):CUSTOMER_ID← parent columnCUSTOMER_ID+ Add bindingA nested-table tab embeds another screen's TableView — its rows / columns come from that screen, not from this tab. No fields to configure here.

Fields

FieldNotes
connectorConnector hosting the target screen. Blank = parent's connector.
screen (required)The target screen's id. The picker is fed from the same connector's screens list.
param_bindsBind parent columns into the target screen's read_query :placeholder params.

Behaviour

The tab renders the target screen — its grid, its filters, its add / edit dialog, its row menu — exactly as it would on its own page, but narrowed by the bound parameters. The user can:

  • Browse the related rows in the embedded grid.
  • Open the + Add button to create a new related row — the bound parent columns pre-fill matching columns on the add dialog.
  • Right-click for the target screen's row menu.
  • Click the inline edit button to open the target screen's dialog and edit a related row.

Use case — customer + deals

A Customer dialog with a Deals tab showing every deal for this customer:

[[screens.crm.customers.dialog.tabs]]
id = "deals"
label = "Deals"
type = "nested_table"
screen = "deals"

[[screens.crm.customers.dialog.tabs.param_binds]]
param = "CUSTOMER_ID"
source = "CUSTOMER_ID"

The Deals tab embeds the deals screen's TableView, filtered to the current customer. When the user clicks + Add deal in the embedded grid, the new deal's CUSTOMER_ID is pre-filled with the parent's value.

What renders inside

The embedded TableView carries everything the target screen has:

ElementInherited
Columns + their hints
Filters✓ — the user can narrow further within the parent's scope.
Dialog (add / edit)
Actions (toolbar / row menu / hooks)
Export config

The target screen exists independently — you can navigate to it as a standalone page, AND embed it as a nested tab. The same configuration drives both surfaces.


When to nest vs. when to navigate

A common decision: should the related data be embedded in the parent's dialog, or open as a separate screen on row-click?

Choose embeddingChoose row-click navigation
The user always wants to see the related data alongside the parent (single-screen flow).The related data is occasionally relevant; opening it on demand keeps the parent dialog focused.
The related table is small (a few columns, few rows).The related table is large; embedding clutters the dialog.
Users edit both at the same time.Users typically edit one then the other in separate steps.
Multiple sibling screens drill into the same target — defining it once as a standalone screen is cleaner.

Embedding is the high-affinity choice for mandatory relationships (a customer with at least one address); row-click is the right choice for broad data (a customer with hundreds of historical orders).


Permissions on nested data

Nested tabs honour the same permission model as standalone screens — the underlying query's sql:<connector>:<query> permission is checked. A user who can see the parent dialog but lacks permission on the nested read query sees the nested tab as empty / read-only / hidden depending on the missing permission:

Missing permissionEffect on the nested tab
sql:<connector>:<nested_read>The tab is hidden entirely.
sql:<connector>:<nested_update>The form is read-only.
sql:<connector>:<nested_insert>The + Add button is hidden.
sql:<connector>:<nested_delete>The row delete is disabled.

This means you can build dialogs that progressively reveal data based on the user's role — same screen definition, different surfaces per user.


Common pitfalls

MistakeSymptomFix
nested_form.read_query returns more than one row.The form renders only the first row and silently ignores the rest.Tighten the bind so the read returns 0 or 1. Use nested_table for genuine many-relationships.
nested_table without param_binds.The embedded grid shows every row of the target — including unrelated ones.Always bind at least the parent's foreign-key columns.
nested_form with insert_query blank, no row yet exists.The user can't add a new row — the form has nothing to fire.Either wire insert_query, or pre-create the row through another flow.
nested_table pointing at a screen on a different connector + missing connector override.The runtime tries to resolve the target screen on the parent's connector and fails.Set connector explicitly to the target screen's connector.
Two nested tabs with the same id.Save validation fails ("duplicate dialog tab id").Pick unique ids per dialog.

What's next