Zova Table Under the Hood
This guide explains the source-level runtime path behind Zova Table.
Use this page together with:
- Table Guide
- TableCell Authoring Cookbook
- Table + Resource CRUD Cookbook
- Rest Resource Under the Hood
- Rest Resource Source Reading Map
- Zova Table Source Reading Map
- Zova Source Reading Map
- API Schema Guide
- Bean Scene Authoring
Use this page after Table Guide when you want to move from the public authoring surface to the internal cooperation among table controllers, schema metadata, TanStack Table, tableCell beans, JSX/CEL scopes, and resource-page integration.
If your next question is not “how does this runtime work?” but “which files should I read next?”, continue with Zova Table Source Reading Map.
If your next question is specifically why row or bulk actions are visible or hidden in current list pages, continue with Table Action Visibility and Permission Flow Guide.
If your next question is specifically about the controller/render micro-pipeline inside ControllerTable, continue with Zova Table Controller Render Supplement.
TIP
Zova Table docs path
- Table Guide — learn the public authoring surface
- Zova Table Under the Hood — learn how the runtime pieces cooperate
- Zova Table Source Reading Map — learn which files to read next
You are here: step 2. Previous page: Table Guide. Next recommended page: Zova Table Source Reading Map.
Why this page exists
The public table guide already explains the authoring surface:
ZTable- schema-driven columns
getColumns(...)tableCellbeans- resource-page integration
What many contributors and AI workflows still want next is the implementation bridge:
- where the table controller is created
- where schema
tablemetadata becomes visible columns - where TanStack Table enters the runtime
- how a
tableCellonion name becomes a bean instance - how column and cell CEL/JSX scopes are prepared
- how resource page blocks feed data and permissions into the table runtime
This page is that bridge.
The shortest accurate runtime model
For a typical Zova table, the shortest accurate model is:
- the
ZTablewrapper creates a table controller bean through the normal Zova controller path - the table controller loads table-scene schema properties from the row schema
- the controller builds table metadata with visible properties and per-column render functions
- the controller creates TanStack table options through Zova’s
$useTable(...)wrapper - each cell render resolves either to text fallback, a general JSX render target, or a
tableCellbean - the cell runtime evaluates JSX/CEL props with table-aware column and cell scope
- resource pages feed schema, data, permissions, and page scope into the same table runtime through
basic-page:blockTable
That is why Zova Table is not only a thin wrapper around TanStack Table. The business-facing runtime surface is still Zova-native.
A concrete source specimen
The smallest public wrapper entry is:
zova/src/suite-vendor/a-zova/modules/a-table/src/.metadata/component/table.tsA business-facing consumer specimen is:
zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsxA representative tableCell bean specimen is:
zova/src/suite/cabloy-basic/modules/basic-table/src/bean/tableCell.actionOperationsRow.tsxThese three files already show the core architecture:
- the wrapper is thin
- the controller owns the runtime
- cell rendering is scene-driven rather than hard-coded in the page
The core source-reading path
When you want to trace the full mechanism, read these files in order:
zova/src/suite-vendor/a-zova/modules/a-table/src/.metadata/component/table.tszova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsxzova/src/suite-vendor/a-zova/modules/a-table/src/lib/beanControllerTableBase.tszova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsxzova/src/suite-vendor/a-zova/modules/a-openapi/src/lib/schema.tszova/src/suite-vendor/a-zova/modules/a-table/src/types/tableCell.tszova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsxzova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx
A compact role map is:
table.tsshows how the public wrapper entersuseController(...)component/table/controller.tsxowns schema properties, metadata refresh, TanStack bridge, and cell renderingbeanControllerTableBase.tsshows the Zova wrapper arounduseVueTable(...)component/table/render.tsxshows the default table DOM render path throughFlexRenderschema.tsshows how table-scene schema properties are loaded and orderedtypes/tableCell.tsshows thetableCellscene contractblockTable/controller.tsxshows how page blocks feeddata,schema, andtableScopeintoZTableblockPage/controller.tsxshows where resource data, permissions, and page scope come from
Step-by-step runtime path
1. ZTable creates the table controller bean
The public wrapper enters the normal Zova controller path through:
zova/src/suite-vendor/a-zova/modules/a-table/src/.metadata/component/table.tsThat wrapper calls useController(ControllerTable, RenderTable, undefined).
A practical reading takeaway is:
- the visible wrapper component is thin
- the controller bean is the real runtime owner
That wrapper also exposes controllerRef, which means the public instance-reference pattern is still controller-oriented rather than DOM-ref-oriented.
2. The table controller owns schema properties, metadata, and render context
The main runtime owner is:
zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsxInside ControllerTable.__init__() the controller:
- registers itself as
$$table - creates a column CEL environment
- creates a
ZovaJsxinstance bound to that CEL environment - creates reactive
propertiesfromthis.$sdk.loadSchemaProperties(this.schema, 'table') - refreshes
tableMetaandcolumns - watches schema changes and refreshes metadata when needed
- creates the TanStack table instance
This is one of the most important source-level facts about Zova Table.
The table controller is not only coordinating rows. It is the central bridge among:
- table-scene schema metadata
- column and cell CEL/JSX scope
- TanStack table state
tableCellbean-scene rendering
3. Why $useTable(...) exists
The shared wrapper lives in:
zova/src/suite-vendor/a-zova/modules/a-table/src/lib/beanControllerTableBase.tsand the page-controller variant lives in:
zova/src/suite-vendor/a-zova/modules/a-table/src/lib/beanControllerPageTableBase.tsThe important runtime detail is that $useTable(...) wraps TanStack useVueTable(...) like this:
- run it inside
ctx.util.instanceScope(...) - then
markRaw(...)the returned TanStack object
That matters because:
- Zova wants the controller bean to stay the business-facing runtime host
- the underlying TanStack table object still needs to be created in the correct Zova instance scope
- the raw TanStack API should not become the main architecture surface
A practical reading takeaway is:
- Zova does not replace TanStack Table
- Zova relocates the business-facing ownership into controller beans
4. How schema becomes visible columns
The default schema path starts in two places:
zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/controller.tsx
zova/src/suite-vendor/a-zova/modules/a-openapi/src/lib/schema.tsThe important runtime path is:
- the table receives
schema _createProperties()computesthis.$sdk.loadSchemaProperties(this.schema, 'table')loadSchemaProperties(...)resolves$ref, appliesrest.tableoverlays, and sorts byrest.order_createTableMeta()iterates those properties and decides visibility and render behavior_createColumnsMiddle()converts the surviving properties into TanStack column definitions
A practical reading takeaway is:
- schema is not only validation truth
- schema also drives table order, visibility, and cell render metadata
5. How table metadata is built
The internal table metadata shape is:
propertiesrenders
For each property, _createTableMeta() does this work:
- create column scope with
getColumnScope(...) - create column render context with
getColumnJsxRenderContext(...) - compute top-level column options through
getColumnComponentPropsTop(...) - skip the column if
visible === false - create a render function through
_createColumnRender(...)
That means a visible column is not only “one schema property plus one header”.
It is the result of a controller-owned pipeline that has already decided:
- whether the column exists
- which render provider it uses
- which column props belong to that provider
6. The default TanStack table options are still controller-owned
The actual TanStack table is created in _createTable().
Important defaults include:
getRowId: row => row.idgetCoreRowModel: getCoreRowModel()renderFallbackValue: this.scope.config.renderFallbackValuemanualPagination: true- reactive
datagetter returningself.data || [] - reactive
columnsgetter returningself.columns
A practical reading takeaway is:
- TanStack owns the row-model mechanics
- the controller still owns which data and columns TanStack sees
7. Column and cell render context are explicitly separated
The controller creates two related but distinct runtime contexts.
Column context
getColumnJsxRenderContext(...) exposes:
$scene: 'tableColumn'$host: this$celScope$jsx$$table
Cell context
getCellJsxRenderContext(...) exposes:
$scene: 'tableCell'$host: this$celScope$jsx$$tablecellContext
This matters because column-level decisions and cell-level rendering do not have exactly the same information.
A practical reading takeaway is:
- column configuration is prepared before one row value exists
- cell rendering gets the row-aware
CellContextonly when the cell is actually rendered
8. How a tableCell onion name becomes a bean instance
The central method is:
ControllerTable.getRenderProvider(...)Its important behavior is:
- no render -> use
'text' - onion-like render string with
:-> convert it withbeanFullNameFromOnionName(render, 'tableCell') - otherwise keep the render target as-is
Then _createColumnRender(...) can resolve that provider.
If the provider belongs to the tableCell bean scene, the controller:
- loads the bean instance through
this.sys.bean._getBean(...) - reads decorator options through
appResource.getBean(...) - merges onion options with column props via
deepExtend(...) - optionally calls
beanInstance.checkVisible(...)
That means a cell bean is not only a render callback. It is a first-class scene resource with:
- bean resolution
- decorator options
- optional async visibility logic
- render-time
next()composition
9. The cell render pipeline
The most useful durable mental model for one cell is:
column metadata -> render provider resolution -> bean/decorator option merge -> cell scope -> JSX/CEL evaluation -> bean render or direct renderThe core methods are:
cellRenderPrepare(...)cellRender(...)_cellRender(...)_cellRenderInner(...)
Important behavior includes:
Text fallback
If the render provider is 'text', the cell returns:
- the current value
- or
renderFallbackValuewhen the value is nil or empty string
Cell scope construction
If no explicit cell scope exists yet, the controller derives one from column scope plus:
valuefallbackValue
Transient helper injection
_cellRender(...) uses zovaJsx.setTransientObject(...) so CEL/JSX evaluation can call getValue(name) against the current row.
Bean-backed render path
When the render provider resolves to a tableCell bean:
- props are rendered through
zovaJsx.renderJsxProps(...) classandstyleare normalized into controller-host CSS handlingbeanInstance.render(...)receives final options, render context, andnext()
General render path
When there is no bean instance, the controller falls back to zovaJsx.render(...).
A practical reading takeaway is:
tableCellresources are part of a controller-prepared render pipeline- the page does not manually wire row value extraction, CEL scope, and option merge each time
10. What tableCell beans really are
The scene contract lives in:
zova/src/suite-vendor/a-zova/modules/a-table/src/types/tableCell.ts
zova/src/suite-vendor/a-zova/modules/a-table/src/lib/tableCell.tsThat contract defines:
ITableCellRenderIDecoratorTableCellOptionsNextTableCellRenderSysOnion.tableCellConfigOnions.tableCellIBeanSceneRecord.tableCell
The decorator itself is:
createBeanDecorator('tableCell', 'sys', true, options);That means tableCell is not only a naming convention. It is a frontend bean scene with:
- system-scoped resolution behavior
- scene-level typing
- CLI boilerplate support
- metadata-driven resource identity
11. Representative tableCell patterns
Simple formatting cells such as:
basic-text:textbasic-date:datebasic-select:select
usually implement a straightforward shape:
- call
next()to get the base value - format or map that value
- optionally wrap it with a class-aware container
Row-action cells such as:
basic-table:actionOperationsRow
show the more advanced pattern:
checkVisible(...)filters actions by permission and preloads nested rendersrender(...)reuses$$table.cellRender(...)for each visible action
That is an important source-level clue:
- one
tableCellbean can itself orchestrate more table-cell renders
12. The default DOM render still happens in a render bean
The default render bean lives in:
zova/src/suite-vendor/a-zova/modules/a-table/src/component/table/render.tsxIt does two important jobs:
- render the outer table markup with
<table class="table"> - delegate header and cell vnode creation to TanStack
FlexRender
If slotDefault is supplied, the render bean yields to that slot instead of the built-in table DOM.
That means automatic table rendering is not happening magically in the wrapper component. It is happening in the render bean.
A practical reading takeaway is:
- the wrapper starts the controller path
- the render bean owns the default DOM shape
- the table controller still owns the cell render functions that
FlexRenderconsumes
13. Resource-page integration path
Zova Table is frequently consumed through Cabloy Basic resource pages.
The strongest specimens are:
zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockPage/controller.tsx
zova/src/suite/cabloy-basic/modules/basic-page/src/component/blockTable/controller.tsxPage block path
blockPage:
- loads
ModelResource - creates page-level JSX/CEL environment
- computes resource query state
- exposes
data,schemaRow, andpermissions - refreshes table metadata when permissions change
Table block path
blockTable:
- renders
ZTable - passes
data={$$page.data} - passes
schema={$$page.schemaRow} - passes
tableScope={$$page.jsxCelScope} - captures
controllerRefand storestableRefback onto the page controller
This is one of the most important integration facts about the module.
The resource page does not manually rebuild the table runtime. It feeds page-owned resource state into the same reusable table controller.
14. Compact call-flow sketch
When in doubt, use this short call flow:
ZTablewrapper enters the normal Zova controller pathControllerTable.__init__()creates CEL/JSX support and schema-driven propertiesrefreshMeta()computes visible table properties and per-column render functions_createTable()creates the TanStack bridge through$useTable(...)RenderTablerenders headers and rows throughFlexRender- each cell render resolves to text fallback, a general render target, or a
tableCellbean tableCellbeans receive controller-prepared options, scope, andnext()- resource pages prepare
data,schemaRow,permissions, andtableScopebefore entering the same runtime
That is the shortest end-to-end explanation of how the module cooperates.
Final takeaway
Zova Table is not just TanStack Table plus JSX wrappers.
It moves table ownership into:
- table controller beans
- schema metadata
tableCellbean-scene resources- controller-prepared CEL/JSX scope
- resource-page integration
TanStack Table is still the underlying row-model engine, but the business-facing runtime model is Zova-native.
Verification checklist
When documenting or changing this area, verify in this order:
confirm the runtime claims against the current
a-tablesourceconfirm
tableCellscene metadata and boilerplates still match currentpackage.jsonandcli/wiringconfirm resource-page integration claims still match
blockPageandblockTablebuild the docs site:
bashnpm run docs:buildverify the page is reachable from the frontend sidebar and related table docs