Migrating Apex Grid v3 to v4

Apex Grid 4.0 is a major release. Most apps upgrade with no code change, but a few defaults and types moved. This guide lists what to check when upgrading from 3.6, then what is new. apex-grid-enterprise 0.9 ships alongside it and needs it (see Enterprise 0.9).

At a glance

Areav3v4
Body rowsRecord-keyed: each row element stays with its recordRecycled by default (rowRecycling); opt out with rowRecycling = false
Grid rolerole, aria-rowcount, aria-colcount on the <apex-grid> hostOn the inner [part~="table"] element
Ignite UI theme--ig-primary-* and --ig-font-family tinted the grid with no importNeeds import 'apex-grid/ignite-ui'
--ag-fontRoot default on :rootNo root default
PaginationPinned rows counted toward totalItems and the pagesPinned rows are left out of the page count
DataPipelineConfiguration.paginationTyped, never calledRemoved
min() / max()Text that is not a number passedText that is not a number fails

Install

npm install apex-grid@^4.0.0
# with the enterprise package
npm install apex-grid@^4.0.0 apex-grid-enterprise@^0.9.0

apex-grid-enterprise 0.9 requires apex-grid@^4.0.0, and its modules fail to load against a 3.x grid, so upgrade both together. Its apexcharts, lit and @lit/context peer ranges are unchanged.

Body rows are recycled by default

A scroll, sort or filter now re-binds the rendered row elements to the records in view instead of building new rows, which makes long jumps and sorts cheaper on large grids. A row element and its cells can therefore show another record later.

What you might notice:

  • A cellTemplate or editorTemplate that keeps state in the elements it creates (an input's typed value, a third-party widget) can show that state on another record after a scroll or a sort.
  • Code that holds grid.rows elements, or ctx.row / ctx.parent, across a scroll, a sort or an await finds them showing other records.

Templates should derive what they render from their context on every render, and read the record from the new ctx.data:

import { html } from 'lit';
import { keyed } from 'lit/directives/keyed.js';

{
  key: 'notes',
  // ctx.data is the record this cell was rendered for, and stays that record after an await
  cellTemplate: (ctx) => html`<span title=${ctx.data.owner}>${ctx.value}</span>`,
}

{
  key: 'widget',
  // a template that keeps element state is rebuilt per record
  cellTemplate: (ctx) => keyed(ctx.data, html`<my-widget .value=${ctx.value}></my-widget>`),
}

To keep record-keyed rows, turn recycling off:

grid.rowRecycling = false;
<apex-grid row-recycling="false"></apex-grid>

Detail panel content follows its record and keeps its DOM state across a scroll, a sort or a moveRow, so detail templates need no change. See Cell Template.

The grid role moved to the table part

role (grid or treegrid), aria-rowcount, aria-colcount, aria-multiselectable and the accessible name are now on an inner element exposed as part table. It holds only the header rows, the pinned rows and the body, so the toolbar, paginator, filter panel and live region are no longer read as table content. The layout is unchanged.

Code or tests that read these attributes from the host should query the part instead:

const table = grid.shadowRoot.querySelector('[part~="table"]');
table.getAttribute('aria-rowcount');

An aria-label or aria-labelledby on the host still names the grid, and aria-describedby becomes its description. See Accessibility.

Ignite UI theming is opt-in

In 3.x the grid took the brand colour (--ig-primary-500, --ig-primary-700) and the font (--ig-font-family) from an Ignite UI theme on the page by itself. In 4.0 an Ignite UI stylesheet changes nothing until you import the add-on, which brings the brand and font back along with the rest of the theme:

import 'apex-grid/define';
import 'apex-grid/ignite-ui';

Two related changes:

  • --ag-font has no root default any more, so an explicit --ag-font wins over the family and Ignite UI fonts. Page CSS that reads var(--ag-font) needs its own fallback, as with --ag-brand. The default Inter stack is unchanged.
  • The grid follows the family --apx-* tokens (--apx-accent, --apx-fore, --apx-grid, --apx-surface, --apx-font-family, --apx-radius-factor). A page that sets them on :root for its charts sees the grid follow them too. Set the matching --ag-* tokens to keep the grid as it was, or set the family tokens on the elements they are meant for instead of :root. tokens="off" on a grid stops the accent, font and radius family tokens, but --apx-fore, --apx-grid and --apx-surface set on :root still reach its text, line and surface defaults unless the apex-grid/ignite-ui add-on is loaded.

See Using with Ignite UI.

Pagination

  • Pinned rows no longer count toward the pages. totalItems, pageCount, the paginator summary and the pageChanged detail count the rows the pager pages through. Without pagination totalItems still counts every row.
  • A local page or page size change no longer reruns the pipeline, so custom sort and filter hooks in dataPipelineConfiguration are not called on a local page change. Remote mode still reruns it.
  • DataPipelineConfiguration.pagination and the 'pagination' value of DataPipelineParams.type are removed. The hook was never called, so no runtime behaviour changes, but TypeScript code that sets it stops compiling. For server paging use pagination.mode: 'remote', fetch the page on pageChanged and keep pagination.totalItems current. See Remote pagination.
  • pageChanged reports the change that was applied, so two quick nextPage() calls report page 2 and then page 3.
  • A page change is announced once ("Page X of N"); the paginator summary and page label are no longer live regions.

Validators

min() and max() fail a value that is not a number. Before, 'abc' (or any text Number() could not read) passed both. Typed text is now read the way a person writes it, so 123, 1,000 or 1.234,5 in a column with locale: 'de-DE' still pass as numbers. Empty values still pass; compose with required().

Smaller type and DOM changes

  • ctx.data is a required field of BaseApexCellContext, so code that builds a cell context by hand must supply it.
  • ApexGridPaginator.pageSizeOptions is typed readonly number[].
  • The paginator's previous button has the id prev (it was previous) inside its shadow root. Target it with apex-grid::part(paginator-prev) instead.
  • The protected ApexGrid.headerRowCount and ApexGridHeaderRow.renderDragGhost are removed, which affects subclasses only.
  • pageItems keeps the same array between updates while nothing that shapes it changes. Code that compared it by identity sees a new array only when the body rows change.

Other changes you may notice

  • The column header row is no longer a Tab stop.
  • The built-in column menu opens in the top layer where the browser supports the Popover API, so it can extend outside the grid's box.
  • The column drag ghost renders at the grid root, and apex-grid::part(drag-ghost) reaches it.
  • The paginator follows --header-background, and its text size follows --ag-fs-cell-secondary.
  • The grid uses the treegrid role whenever rows carry levels or expand: tree data, master-detail with a detail template, and the enterprise row grouping, expandable pivot and server-side grouping.
  • The first grid on a page adds one window error listener that cancels "ResizeObserver loop" messages and hides them from listeners added after it. Listeners registered before the first grid still receive them, and every other error is untouched.

New in 4.0

  • showHeader (show-header="false") leaves out the header rows and the filter panel, for a short grid such as a dashboard tile. See Column Configuration.
  • selection.isRowSelectable(row) keeps chosen rows, such as a pinned total row, out of selection. See Row Selection.
  • pagination.paginator: 'compact' draws the small paginator. See Pagination.
  • apex-grid::part() reaches every paginator and toolbar part. See Pagination and Toolbar.
  • ctx.data in cell and editor templates.
  • The apex-grid/ignite-ui add-on and the family --apx-* tokens.
  • Input method (IME) support in the cell editor, the filter panel and the quick filter, and numbers typed in full width or with the column locale's grouping. See Data Filtering.
  • Locale keys for text that stayed English, and ValidatorContext.localize. See Localization.

Enterprise 0.9

apex-grid-enterprise 0.9 takes everything above, and apps may also notice:

  • Body rows are recycled in <apex-grid-enterprise> too. Master-detail detail grids take the master grid's rowRecycling when they are created, so a master that turns it off turns it off for its detail grids. configureDetail runs after and can set its own.
  • A bulk write fires one apex-view-changed (applyEdits, a range paste, a fill drag, or undo or redo of a batch) instead of one per cell. After a single cell edit the event fires when the grid finishes its update, a microtask later, rather than inside the cellValueChanged dispatch.
  • Only the visible columns' values reach a model or proxy. Row fields that are not columns, and hidden columns, are no longer sent. maxDataRows on createClaudeReasoner also bounds the proxy transport.
  • AI requests and replies are checked more strictly. Without a mode, only "how many", "number of", "what is", "what are" or a trailing "?" make a prompt a question; pass mode: 'ask' for the rest. In a proxy response, answer must be a string and patch an object; any other shape rejects with an error.
  • Charts skip blank and non-numeric cells when they aggregate, so avg, min, max, median and count match the grid's own group totals. The forecast band is a true 95% prediction interval, and thousands, percent and currency labels format for the locale.
  • ChartSeries.data is typed readonly (number | null)[], and the tool panel's grid defaults to null like the other companion elements.
  • The filter builder's value field is a text input for numeric columns too, so it reads the column's locale and no longer has a spinner.

New in 0.9: the popups and the chart panel follow the grid's theme (and an Ignite UI theme with apex-grid/ignite-ui), IME input and full-width formulas and numbers, more locale keys, and the ApexGridEnterpriseHost, ApexGridEnterpriseEventMap, ParseError, FormulaAst and FormulaExportOptions exports.