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 ModelServer-Side Row Model
ShapeOne flat listGrouped, with expandable levels
Fetch unitA fixed-size block of rowsOne group level, then a group's children on expand
AggregatesNoneComputed by the server, shown on group rows
PivotNoYes, through pivotCols
Use it whenThe data is flat and simply too long to holdThe 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

FieldTypeDescription
datasourceServerSideDataSource<T>Object with a getRows(params) method. Required
rowGroupColsstring[]Ordered group-by column keys. An empty array means a flat server request
valueColsAggregationConfigAggregations shown on group rows, e.g. { salary: ['sum'] }
pivotColsstring[]Column-dimension fields for server-side pivot. Non-empty puts the grid in pivot mode
blockSizenumberChildren per fetched block, enabling intra-group pagination. Omit to load a group's children in one request
groupHeaderTextstringHeader 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:

ParamTypeDescription
groupKeysstring[]The group value path being expanded. [] is the top level
rowGroupColsstring[]Ordered group-by column keys
valueColsAggregationConfigAggregations requested per column
pivotColsstring[]Column-dimension fields. Empty means no pivot
pivotModebooleanWhether the grid is in server-side pivot mode
startRow / endRownumberThe child window to fetch, inclusive and exclusive. Present only when blockSize is set
sortModelSortExpression<T>[]The sort the user asked for
filterModelFilterExpression<T>[]The column filters
quickFilterstringThe 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

ExportDescription
SERVER_ROWS_LOADED_EVENTFires 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_KEYThe synthetic column key the auto group column uses
SERVER_SIDE_ROW_MODEL_MODULE_IDModule id, for selective module registration

Live demo

  • Server-Side Row Model: grouping, server-computed aggregates, pivotCols and blockSize paging in one page.