Server-Side Row Model (Enterprise)
The server-side row model in apex-grid-enterprise is a lazy, level-at-a-time row model. The grid asks a ServerSideDataSource for one group level, and fetches a group's children only when that group is expanded. The server computes the aggregates shown on group rows, so a grouped view over a hundred million rows costs one request per level the user actually opens.
Added in enterprise 0.7.0.
Which row model do I want?
The grid has two server-side models and they solve different problems.
| Infinite Row Model | Server-Side Row Model | |
|---|---|---|
| Shape | One flat list | Grouped, with expandable levels |
| Fetch unit | A fixed-size block of rows | One group level, then a group's children on expand |
| Aggregates | None | Computed by the server, shown on group rows |
| Pivot | No | Yes, through pivotCols |
| Use it when | The data is flat and simply too long to hold | The user explores a hierarchy and only ever opens part of it |
They are mutually exclusive, and so is client-side grouping and pivoting. Setting serverSideRowModel alongside infiniteRowModel, groupBy or pivotOn is a configuration error, because in this model the server owns the shaping and the grid cannot also do it. Client-side sort and filter are disabled for the same reason: they are passed to the datasource instead.
Setting a datasource
import 'apex-grid-enterprise/define';
const grid = document.createElement('apex-grid-enterprise');
grid.columns = columns;
grid.serverSideRowModel = {
datasource: {
async getRows(params) {
const res = await fetch('/api/rows', {
method: 'POST',
body: JSON.stringify(params),
});
const { rows, total } = await res.json();
return { rows, rowCount: total };
},
},
rowGroupCols: ['region', 'country'],
valueCols: { revenue: ['sum'], headcount: ['sum'] },
};
document.body.appendChild(grid);
The grid renders one auto group column carrying the group value, with indent and a chevron per level. groupHeaderText sets its header; the default is the joined group fields.
ServerSideRowModelConfig
| Field | Type | Description |
|---|---|---|
datasource | ServerSideDataSource<T> | Object with a getRows(params) method. Required |
rowGroupCols | string[] | Ordered group-by column keys. An empty array means a flat server request |
valueCols | AggregationConfig | Aggregations shown on group rows, e.g. { salary: ['sum'] } |
pivotCols | string[] | Column-dimension fields for server-side pivot. Non-empty puts the grid in pivot mode |
blockSize | number | Children per fetched block, enabling intra-group pagination. Omit to load a group's children in one request |
groupHeaderText | string | Header text for the auto group column |
What the server receives
Every request carries the whole question, so a datasource never has to remember state between calls:
| Param | Type | Description |
|---|---|---|
groupKeys | string[] | The group value path being expanded. [] is the top level |
rowGroupCols | string[] | Ordered group-by column keys |
valueCols | AggregationConfig | Aggregations requested per column |
pivotCols | string[] | Column-dimension fields. Empty means no pivot |
pivotMode | boolean | Whether the grid is in server-side pivot mode |
startRow / endRow | number | The child window to fetch, inclusive and exclusive. Present only when blockSize is set |
sortModel | SortExpression<T>[] | The sort the user asked for |
filterModel | FilterExpression<T>[] | The column filters |
quickFilter | string | The quick-filter text |
groupKeys is the field that makes this a tree rather than a list. A request with groupKeys: [] asks for the top level; groupKeys: ['EMEA'] asks for the children of the EMEA group; groupKeys: ['EMEA', 'France'] asks for the rows under France.
What the server returns
{
rows: [/* group rows, or leaf rows at the deepest level */],
rowCount: 4820, // required when paginating; see below
pivotResultFields: [...], // pivot mode only, on the top-level response
pivotResultGroups: [...], // optional spanning groups over those fields
}
A group row carries the grouped field's value plus any aggregate values under their own column keys. The grid does not compute them: whatever the server puts under revenue is what the group row shows. Whether a level returns group rows or leaf rows follows from the depth: while groupKeys.length < rowGroupCols.length there is another level to group by.
Expanding and refreshing
grid.expandServerGroup(['EMEA', 'France']) // fetches children if not loaded
grid.collapseServerGroup(['EMEA'])
grid.refreshServerSide() // drop every cached level and refetch
grid.isServerSideRowModel // boolean, is this model active
expandServerGroup takes the same group value path the datasource receives as groupKeys.
Intra-group block pagination
A group with two million children should not arrive in one response. Set blockSize and a group's children load a window at a time:
grid.serverSideRowModel = {
datasource,
rowGroupCols: ['region'],
blockSize: 200,
};
The grid then passes startRow and endRow on every request, renders not-yet-loaded rows as placeholders, and fetches blocks as the virtualizer scrolls into them. grid.isRowLoading(row) reports true for a placeholder, so a cell template can render a skeleton:
{
key: 'revenue',
cellTemplate: ({ row, value }) =>
grid.isRowLoading(row) ? html`<span class="skeleton"></span>` : value,
}
Return rowCount when you paginate. The grid needs the level's total to size it and to place the placeholders. Without it the level is treated as "more may follow" until a block comes back with fewer rows than blockSize, which mirrors the infinite model but scrolls less smoothly. When you are not paginating, rowCount is ignored: the single fetch returns the whole level.
Server-side pivot
Set pivotCols and the grid runs in pivot mode. It passes pivotCols and pivotMode: true to the datasource, and installs the value columns the server describes back:
grid.serverSideRowModel = {
datasource,
rowGroupCols: ['region'],
valueCols: { revenue: ['sum'] },
pivotCols: ['quarter'],
};
The server knows the distinct pivot values, because it did the pivot, so it returns them on the top-level response:
{
rows: [...],
pivotResultFields: [
{ key: 'q1_revenue', headerText: 'Q1', group: '2026' },
{ key: 'q2_revenue', headerText: 'Q2', group: '2026' },
],
pivotResultGroups: [{ id: '2026', headerText: 'FY 2026' }],
}
pivotResultGroups is optional and draws spanning column headers over the generated fields.
For pivoting the grid computes itself, see Pivoting. The two cannot be combined.
Accessibility
Group rows carry aria-level and aria-expanded, so a screen reader announces depth and expansion state as the user moves through the tree.
Events and helpers
| Export | Description |
|---|---|
SERVER_ROWS_LOADED_EVENT | Fires when a level's rows arrive. Detail: { rows, loadedGroups } |
getServerRowMeta(row) | The grid's own metadata for a row: its depth, group path and whether it is a group row |
SSRM_GROUP_KEY | The synthetic column key the auto group column uses |
SERVER_SIDE_ROW_MODEL_MODULE_ID | Module id, for selective module registration |
Live demo
- Server-Side Row Model: grouping, server-computed aggregates,
pivotColsandblockSizepaging in one page.