apex-grid-enterprise
v0.7.0
Published
Enterprise (pro-licensed) features for apex-grid.
Downloads
295
Maintainers
Readme
apex-grid-enterprise
Pro-licensed enterprise features for apex-grid.
apex-grid-enterprise extends the community grid and registers as
<apex-grid-enterprise>, layering enterprise-only features on top of everything
apex-grid already does. Use it as a drop-in replacement for <apex-grid>: the
configuration API, theming, and events are identical, plus the additions below.
Requires a valid license key for production use. Without one, the grid keeps working but renders a watermark and logs a console notice. Set a key with
ApexGridEnterprise.setLicense(...); see Licensing below.
Enterprise features
- Column aggregations: sum / avg / min / max / count per column.
- Row grouping: collapsible groups, nested to any depth, with live per-group subtotals.
- Pivoting: reshape rows into a cross-tab with aggregated cells.
- Set filter: Excel-style column value checklist with search and select-all.
- Columns tool panel: drag-and-drop panel for visibility, ordering, grouping, and pivot.
- Cell range selection & status bar: Excel-style range select with live aggregates.
- Excel (XLSX) export: native-typed
.xlsxexport plus a toolbar menu entry. - Master / detail grids: embed a child grid in each expandable row.
- Integrated charts: render the grid's data as an ApexCharts chart, live-bound to the view (redraws on sort / filter / edit), with a data-mapping definition, a format popover (colors, number format, axis titles, trend / reference lines & bands, forecasting), image export, save / restore, and optional chart-driven cross-filtering.
- Context menu: right-click or the header kebab button for sort / pin / hide / group / copy, a "Chart range" submenu, and custom items.
- Infinite (server-side) row model: stream large remote datasets, block by block.
- AI Toolkit: natural-language grid control and read-only Q&A on a built-in deterministic rule engine (offline, no key), with optional LLM escalation and a first-class Claude reasoner included.
- Formulas: spreadsheet-style cell formulas with relative/absolute A1 references, drag-to-fill, editor autocomplete and click-to-insert, a broad built-in function set (plus custom functions), dependency-graph recalculation, a show-formulas view, and formula-aware export.
Install
npm install apex-grid-enterprise apex-grid litapex-grid and lit are peer dependencies shared with the community package:
install a single copy of each.
apexcharts is an optional peer
dependency, used only by the integrated charts (renderChart()). It is loaded
through a dynamic import(), so a grid that never charts neither bundles nor
downloads it. Install it only when you chart:
npm install apexchartsApexCharts 5.x, 6.x and 7.x are supported (peer range
^5.15.0 || ^6.0.0 || ^7.0.0); a single copy serves the grid's integrated
charts and any charts you draw yourself with apexcharts /
react-apexcharts.
Usage
import 'apex-grid-enterprise/define'; // registers the enterprise element set/define is the batteries-included entry: it opts the grid into every
built-in feature module and registers the grid plus its companion elements
(<apex-grid-enterprise>, <apex-grid-tool-panel>, <apex-grid-status-bar>,
<apex-grid-set-filter>). The configuration API is identical to apex-grid;
see the apex-grid README for column
configuration, theming, and events.
Composing only the features you use
Importing from the package root wires in no feature modules by default, so
each one is tree-shaken unless you opt into it via ApexGridEnterprise.use().
Call it once at startup, before registering the element:
import { ApexGridEnterprise, pivotModule, rangeSelectionModule } from 'apex-grid-enterprise';
ApexGridEnterprise.use(pivotModule, rangeSelectionModule); // only these are bundled + wired
ApexGridEnterprise.register();Available modules: aggregationModule, groupingModule, pivotModule,
rangeSelectionModule, contextMenuModule, formulaModule. The
enterpriseModules array is the full set (it is what /define passes to
use()). Modules a grid is not opted into add nothing to your bundle.
Column aggregations
Request sum / avg / min / max / count per column via the aggregations property,
then read the computed values with getAggregations():
grid.aggregations = { price: ['sum', 'avg'], sold: ['sum', 'max'] };
const totals = grid.getAggregations();
// → { price: { sum: 657.92, avg: 109.65 }, sold: { sum: 347, max: 91 } }AggregationConfig is Record<string, AggregationFn[]>, where AggregationFn is
'sum' | 'avg' | 'min' | 'max' | 'count'. Non-numeric values are ignored. The
same engine powers group subtotals and pivot cells, so numbers are computed
consistently everywhere.
Grand-total row
totalRow adds one synthesized row showing aggregations across the whole view:
grid.totalRow = { aggregations: { price: ['sum'], sold: ['sum'] } };
grid.getTotals(); // → { price: { sum: 657.92 }, sold: { sum: 347 } }This is not the same thing as the per-group aggregates groupBy renders, which
total a group's own leaves. The total row totals the view, so it answers "what
is the sum of this column" without grouping anything, and it follows filtering —
a filtered grid totals what it shows. In a grouped view the group-header rows are
excluded, so their aggregates are not counted twice.
position: 'top' moves it above the rows, label overrides the leading text
(default: the localized "Grand Total"), and null removes it. Style it through
the total-row, total-row-content, total-row-label, total-row-values, and
total-row-value parts. Pivot brings its own grand total, so this covers the
flat and grouped views.
Row grouping
Group rows by one or more columns. Group headers render full-width with a chevron toggle, a leaf count, and inline subtotals. Aggregates are computed over the filtered leaves, so they track what is on screen.
grid.groupBy = ['region', 'department']; // ordered; [] = no grouping
grid.aggregations = { salary: ['sum', 'avg'] }; // shown on each group header
grid.groupingOptions = { defaultExpanded: true }; // true | false | depth number
grid.expandAllGroups();
grid.collapseAllGroups();
grid.expandGroup('/EMEA/Engineering'); // stable path key
grid.getGroups(); // GroupRowMeta<T>[]Cancellable groupExpanding and follow-up groupExpanded events fire on toggle.
Pivoting
Reshape data into a cross-tab: pick the row dimension(s), the column(s) to pivot
on, and the measures aggregated into each cell. Pivot columns are generated from
the data and recompute on filter. Pivoting and groupBy are mutually exclusive
(pivot wins).
grid.pivotRows = ['region']; // down the left
grid.pivotOn = 'department'; // its distinct values become columns
grid.pivotValues = { salary: ['sum'] }; // measure(s) per cell (AggregationConfig)
grid.pivotOn = ''; // disable, restoring the original columnsgrid.isPivoting reflects whether a pivot view is currently active.
Spanning headers, totals, and multi-field column dimensions
With more than one measure, or more than one column-dimension field, the value
columns group under spanning headers. pivotOn accepts an ordered list; the
first field heads the spanning group (core column groups are one level deep, so
inner fields fold into the leaf header). getPivotColumnGroups() returns the
generated groups.
pivotOptions adds subtotals (a row after each first-dimension value; needs
a multi-field row dimension) and a grand total:
grid.pivotRows = ['region', 'department'];
grid.pivotOn = ['level', 'quarter']; // multi-field column dimension
grid.pivotValues = { salary: ['sum', 'avg'] };
grid.pivotOptions = { subtotals: true, grandTotal: 'bottom' };Total rows are ordinary rows carrying the aggregate; getPivotMeta(row) returns
{ kind: 'subtotal' | 'grandTotal' } for them (and 'data' otherwise), so you
can style or skip them. Both totals are off unless requested.
Expandable row groups
pivotOptions.expandable renders the row dimension as a nested, collapsible tree:
the per-field leading columns collapse into one auto group column with indentation
and chevrons, and each parent row carries the aggregate over its subtree.
grid.pivotRows = ['region', 'department'];
grid.pivotOn = 'level';
grid.pivotValues = { salary: ['sum'] };
grid.pivotOptions = { expandable: true, defaultExpanded: true, grandTotal: 'bottom' };defaultExpanded accepts true / false / a depth threshold. Clicking a chevron
toggles a node; total rows still compose. In expandable mode the subtotals option
is ignored (parent rows already are the subtotals).
Set filter
An Excel-style value checklist for a column. A standalone companion element wired to the grid; every toggle applies immediately and composes with the grid's other filters.
<apex-grid-set-filter id="filter" column="department"></apex-grid-set-filter>filter.grid = grid; // required
await filter.updateComplete;
filter.setSelectedTokens(['Engineering', 'Marketing']);
filter.selectAll(); // select all values (clears the column filter)
filter.clearAll(); // deselect all (hides every row)
filter.refresh(); // re-read distinct values after data changes
filter.distinctValues; // ReadonlyArray<{ token, value, label }>
filter.selectedTokens; // string[]The panel includes a search box, a (Select all) row, and folds empty values
(null / undefined / '') into a single entry. CSS parts are exposed for
styling.
Advanced filter builder
A nested AND / OR visual query builder — beyond the per-column header filters.
Compose conditions and sub-groups like (region = EMEA OR region = AMER) AND salary > 80000.
<apex-grid-filter-builder id="builder"></apex-grid-filter-builder>builder.grid = grid; // required
// Add conditions / groups in the UI, then Apply. Or drive it headlessly:
grid.applyAdvancedFilter({
kind: 'group',
join: 'and',
children: [
{
kind: 'group',
join: 'or',
children: [
{ kind: 'condition', column: 'region', operator: 'equals', value: 'EMEA' },
{ kind: 'condition', column: 'region', operator: 'equals', value: 'AMER' },
],
},
{ kind: 'condition', column: 'salary', operator: 'greaterThan', value: 80000 },
],
});
grid.clearAdvancedFilter(); // remove it
grid.advancedFilterModel; // the active model, or nullOperators come from the same operand tables as the header filters
(operatorsForType(type)), so semantics match. Evaluation is client-side via the
dataPipelineConfiguration.filter hook and composes with sort / pagination /
grouping. While active, the advanced filter owns column filtering (it replaces
the built-in filter). The model is plain JSON, so it round-trips through storage.
The pure evaluator (filterRows, isEmptyModel) is exported for headless use.
Columns tool panel
A drag-and-drop side panel that turns the advanced features into a no-code surface: toggle column visibility, pin, reorder, and search; drag fields into Row Groups / Values to group and aggregate; flip Pivot mode to drive pivot rows, values, and column labels. Wire it to a grid and it drives the grid's public properties directly.
<apex-grid-tool-panel id="panel"></apex-grid-tool-panel>document.getElementById('panel').grid = grid; // the only wiring neededCell range selection & status bar
Excel-style cell range selection is on by default; pair it with the status bar companion element to show live aggregates (count / sum / avg / min / max) over the current selection. Holding a drag at the top or bottom edge of the grid body auto-scrolls and keeps extending the selection past the visible rows.
<apex-grid-status-bar id="status"></apex-grid-status-bar>status.grid = grid;
grid.rangeSelection = true; // default; HTML attribute: range-selection
grid.selectRange(/* start, end */); // programmatic selection
// the grid emits 'apex-range-changed' with the current selection statsExcel (XLSX) export
Export to a real .xlsx file where numbers, booleans, and Date values keep
their native Excel cell types. Adds an "Export XLSX" entry to the toolbar's
export menu, alongside the community grid's CSV (CSV export stays free).
const bytes = grid.exportToXLSX({ filename: 'users', sheetName: 'Users' });
// options: { filename?, sheetName?, source?, columns? }, returns Uint8ArrayMaster / detail grids
Give each expandable row its own embedded child grid. Provide the child columns and a (sync or async) data getter; detail grids are cached per row.
grid.masterDetail = {
columns: [{ key: 'sku' }, { key: 'qty' }], // or a (row) => columns fn
getDetailData: (row) => fetchOrderLines(row.id), // T[] | Promise<T[]>
detailHeight: 240, // optional
};
grid.refreshDetail(row); // drop a row's cached detail grid to force a reloadIntegrated charts
Render the grid's current data as an ApexCharts chart. ApexCharts is dynamically imported, so it only loads when a chart is actually drawn.
The chart model is derived by intent: a selected cell range wins, otherwise the
current view (the flat grid, or a grouping/pivot aggregate). Friendly chart types
(column, bar, line, area, pie, donut, scatter, radar, combo, or
'auto') map to the right ApexCharts shape.
const model = grid.getRangeChartModel(); // from the active cell selection
// or grid.getViewChartModel() // from the current view (flat, or grouping/pivot)
// or grid.getChartModel() // selection if present, else view
const chart = grid.createRangeChart(container, {
type: 'column',
apexOptions: { /* deep-merged escape hatch */ },
});Each getter takes an optional ChartDefinition to control which
column is the category, which are the measures, and how rows are aggregated per
category.
<apex-grid-chart> panel
A built-in chart panel with a type gallery that live-redraws as the selection or
view changes. Set its grid property; choose mode="inline" (embedded) or
mode="dialog" (a floating, draggable panel, the default). It renders in light DOM
(ApexCharts cannot render inside a shadow root).
<apex-grid-chart mode="inline"></apex-grid-chart>chart.grid = grid;
chart.source = 'selection'; // 'auto' (default) | 'selection' | 'view'
chart.type = 'column'; // gallery-switchable; 'auto' uses the recommended type
chart.theme = 'grid'; // 'grid' (sync palette to grid theme) | 'light' | 'dark'With source="view" the panel becomes a live companion: it charts the whole
current view and redraws automatically as you sort, filter, or edit the grid, with no
cell selection required.
The enterprise grid also adds a "Create chart" toolbar button that opens the
panel in a dialog. Each click mints an independent chart panel, so several charts
(each a frozen snapshot of the data at creation) can sit side by side without
disturbing one another. The panel and toolbar button require <apex-grid-chart> to be
registered (the /define entry does this for you).
Ways to start a chart, and a few panel niceties:
- On a selection a floating Chart button appears over the grid, and Alt+F1 charts the current cell range from the keyboard.
- On a grouped / pivoted view the right-click menu offers "Chart this view" (it charts the group/pivot model); with a plain selection it offers "Chart range".
- The type gallery is grouped (cartesian / circular / statistical) with an icon per type; Auto is a "Suggested" badge. A dialog chart's heading auto-titles from the mapping ("Revenue by Region") and is double-click-to-rename.
Data mapping
ChartDefinition (the panel's definition property, or an argument to the model
getters) controls how the grid maps to the chart. Every field is optional: the empty
default keeps the automatic mapping (first non-numeric column is the category, every
numeric column a series, summed per category), so a mapping UI can seed itself from the
default and override only what changes.
chart.definition = {
category: 'region', // column key for the X axis
measures: ['revenue', 'deals'],// column keys plotted as series
aggregation: 'sum', // 'sum' | 'avg' | 'count' | 'min' | 'max' | 'median'
// (or a per-measure map { revenue: 'sum', deals: 'avg' })
secondaryMeasures: ['deals'], // draw these on a secondary (opposite) value axis
};The panel surfaces all of this in a Data popover in the toolbar: a category
picker, a checkbox per numeric column (with a per-series 2nd axis toggle for
measures on a different scale, e.g. revenue vs. headcount), and the aggregation. The
columns it offers come from grid.getChartFields() ({ key, label, numeric }[]), so a
custom mapping UI can read the same list. The popover is hidden for snapshot charts
(staticModel) and while grouping/pivot is active (those views carry their own
aggregation).
Calculated fields
A calculated field plots a series from a formula over the other columns, with no
extra grid column. The formula uses the enterprise formula engine with A1
references, where the letters map to the numeric columns in display order (A1 = first
numeric column, B1 = second, …; row is always 1 — one aggregated value per column
per category):
chart.definition = {
category: 'department',
measures: ['salary'],
calculatedFields: [{ name: 'Bonus %', formula: 'B1 / A1 * 100' }], // bonus / salary * 100
secondaryMeasures: ['Bonus %'], // a calc field can sit on the secondary axis (by its name)
};Each field is evaluated per category over the aggregated values (aggregate-then-
evaluate, i.e. the ratio of totals — the convention in Excel PivotTable calculated
fields, Power BI measures, and Tableau). The Data popover has an editor (name + a
live-validated formula input with an A1 = column legend); calc fields ride through
toJSON / restore in the definition. isValidChartFormula(formula) and
computeCalculatedSeries(...) are exported for building your own UI.
Format popover
The toolbar's Format popover surfaces the handful of options users change most
often, layered over apexOptions so it only touches the keys you set. All are
available programmatically via the format property (ChartFormat):
chart.format = {
colors: ['#2563eb', '#16a34a'], // per-series, in series order
legend: true,
dataLabels: false,
gridlines: true,
numberFormat: 'currency', // 'none' | 'currency' | 'percent' | 'thousands'
axisTitles: { x: 'Department', y: 'Salary (USD)' },
referenceLine: 100_000, // dashed target/threshold on the value axis
referenceBand: { from: 80_000, to: 100_000 }, // shaded target range
trendline: true, // least-squares trend overlay on the first series
forecast: 3, // project N future periods as a continuation line
forecastBand: true, // add a ~95% prediction band around the forecast
};trendline, forecast, and forecastBand are also exposed as the pure functions
linearTrend(), linearForecast(), and linearForecastBand(), and ChartFormat ↔
ApexCharts translation as formatToApexOptions(), so you can drive them without the
panel.
Export
The toolbar's Export menu (and the matching methods) save the chart as a raster PNG, a scalable SVG, or copy it to the clipboard as an image:
await chart.exportImage('png'); // or 'svg'
await chart.copyImage(); // PNG to the clipboardSave & restore
toJSON() returns a plain-JSON ChartConfig (type, source, mapping, format, heading,
cross-filter, and any frozen snapshot) that an app can persist anywhere and reapply
with restore(). apexOptions is excluded (it is author code and may hold functions),
so bind it and grid separately.
const config = chart.toJSON(); // JSON-safe; JSON.stringify(chart) works too
localStorage.setItem('myChart', JSON.stringify(config));
// later
chart.grid = grid;
chart.restore(JSON.parse(localStorage.getItem('myChart')!));Cross-filtering
Set cross-filter (the attribute or the crossFilter property) to turn the panel
into a filter surface: clicking a chart segment filters the underlying grid rows to
that category, and clicking it again toggles the filter off. The chart reads the
grid's full, unfiltered data, so every category stays on the chart instead of
collapsing to the filtered subset.
<apex-grid-chart cross-filter></apex-grid-chart>chart.crossFilter = true;
chart.selectCategory(0); // programmatic form of clicking the first segmentThe filter is applied with a self-contained, type-independent equality operation
(a logic predicate rather than a named operand), so it matches regardless of the
category column's declared type. Turning crossFilter off (or disconnecting the
panel) drops any filter it applied.
Context menu
A right-click context menu on cells and column headers, also opened from each
column header's kebab (three-dot) button so the actions are discoverable without a
right-click. The kebab and the right-click menu show the same items. Enabled by
default; set context-menu="false" (the attribute) to turn it off, or assign a
ContextMenuConfig (via the contextMenu property) to replace the items.
grid.contextMenu = false; // disable
grid.contextMenu = { // or replace the items
items: (target) => [
{ id: 'sort-asc', label: 'Sort ascending', run: () => grid.sort([{ key: target.column.key, direction: 'ascending' }]) },
{ id: 'copy', label: 'Copy', separatorBefore: true, run: (t) => copyCell(t) },
],
};ContextMenuConfig<T> is { items?: ContextMenuItem<T>[] | ((target: ContextMenuTarget<T>) => ContextMenuItem<T>[]) },
where a ContextMenuItem<T> is { id, label, run?, disabled?, submenu?, separatorBefore? }
(provide a submenu for a nested menu, or a run callback for a leaf). The target
is { kind: 'cell' | 'header', column, row?, rowIndex? }.
The built-in default items are sort ascending / descending / clear sort, pin to
start / pin to end / unpin, hide column, and (on cell targets) copy. On header
targets, when the grouping module is present, the menu also offers group by this
column, un-group all, and expand / collapse all groups. When you leave
contextMenu at its default (rather than supplying your own items), the menu also
appends a "Chart range" submenu whose entries (Column, Bar, Line, Area, Pie, Donut,
Combo, Auto) chart the current cell-range selection in the chart dialog.
To tweak the items per target without replacing the whole set, listen for the
cancellable apex-context-menu-opening event: its detail.items array is prefilled
with the resolved items (the defaults, unless you supplied your own) and is mutable, so
you can push, splice, or preventDefault() before the menu opens.
grid.addEventListener('apex-context-menu-opening', (event) => {
event.detail.items.push({ id: 'custom', label: 'My action', separatorBefore: true, run: () => {} });
});The feature is the contextMenuModule (id context-menu); the /define entry
includes it.
Infinite (server-side) row model
Stream large remote datasets without loading every row into memory. The grid fetches fixed-size blocks on scroll and delegates sort, filter, and quick-search to your backend.
grid.infiniteRowModel = {
datasource: {
async getRows({ startRow, endRow, sortModel, filterModel, quickFilter }) {
const { rows, total } = await fetchPage(startRow, endRow, sortModel, filterModel, quickFilter);
return { rows, rowCount: total }; // omit rowCount for "infinite" mode
},
},
blockSize: 100, // default 100
};
grid.isRowLoading(row); // true for not-yet-loaded placeholder rows (render skeletons)
grid.refreshRows(); // refetch after a server-side mutationThe grid emits apex-rows-loaded (detail: { rowCount, exact, loadedBlocks,
blockSize }) so you can show live load status.
Server-side grouping + aggregation
For the analytics case, serverSideRowModel groups and aggregates on the
server: the grid asks for one group level at a time and lazily fetches a
group's children when it is expanded. Group rows show server-computed aggregates
under an auto group column.
grid.serverSideRowModel = {
datasource: {
async getRows({ groupKeys, rowGroupCols, valueCols, sortModel, filterModel, quickFilter }) {
// groupKeys is the value path of the parent being expanded ([] = top level).
// Return group rows (with the grouped value + aggregates) when
// groupKeys.length < rowGroupCols.length, else the leaf rows.
return { rows: await fetchLevel(groupKeys, rowGroupCols, valueCols, sortModel, filterModel, quickFilter) };
},
},
rowGroupCols: ['region', 'department'],
valueCols: { salary: ['sum'] },
};
grid.expandServerGroup(['EMEA']); // programmatic expand/collapse
grid.collapseServerGroup(['EMEA']);
grid.refreshServerSide(); // reload after a server-side mutation
grid.isServerSideRowModel; // booleanGroup rows carry aria-level / aria-expanded for screen readers. The grid
emits apex-server-rows-loaded. Mutually exclusive with infiniteRowModel and
client groupBy / pivotOn (the server owns shaping).
Server-side pivot. Add pivotCols to pivot on the server: the grid passes
pivotCols + pivotMode to getRows, and the server returns the generated
value columns in pivotResultFields (plus optional pivotResultGroups for
spanning headers), which the grid installs.
grid.serverSideRowModel = {
datasource,
rowGroupCols: ['region'],
pivotCols: ['department'], // ⇒ pivotMode; server returns pivotResultFields
};Intra-group pagination. By default a group's children load in one request.
Set blockSize to load them a window at a time: the grid passes startRow /
endRow to getRows, sizes the group from the returned rowCount, renders
not-yet-loaded rows as placeholders, and fetches the next block as you scroll
into it. Use grid.isRowLoading(row) to detect a placeholder (e.g. for a
skeleton).
grid.serverSideRowModel = {
datasource, // getRows now receives { startRow, endRow, ... }
rowGroupCols: ['region', 'department'],
valueCols: { salary: ['sum'] },
blockSize: 100, // children fetched 100 at a time; return `rowCount`
};AI Toolkit
Drive the grid in natural language. A prompt becomes a schema-validated state
change applied through setState() with a one-click undo, plus a read-only Q&A
mode. By default it runs on a built-in deterministic rule engine: offline, no
key, no network. You can add an optional LLM (a first-class Anthropic/Claude
reasoner is included) that the grid escalates to only when the rule engine is not
confident. The whole toolkit is an enterprise feature; it builds on the community
grid's getSchema() / setState() foundation.
What it understands
The rule engine covers a documented set of commands and questions. Use these and they work instantly and offline; anything outside the set escalates to your LLM reasoner (if you configured one) or is reported honestly (see below). It never guesses silently.
- Sort: "sort by revenue, highest first", "order by name", "reverse the sort".
- Group / ungroup: "group by region", "ungroup".
- Filter: "filter status = open", "only show EMEA", "remove rows where salary is under 70000", "keep only rows with amount over 100". Removal keeps the complement (it inverts the operand, or reports that it cannot).
- Columns: "hide the notes column", "show salary", "pin name to the left".
- Search: "search Acme".
- Pivot / aggregate: "pivot on region", "sum of revenue".
- Pagination / export: "page 2", "page size 50", "export as csv".
- Reset / undo: "reset", "undo".
- Read-only questions: "how many rows", "highest / lowest / average salary", "min, max and median of bonus", "average salary by department", "who has the highest salary", "top 5 by revenue", "which region has the highest total sales".
Compound requests are supported in one sentence: "group by department, then sort by salary and remove all rows under 70000" applies three steps atomically with a single undo.
Running a prompt
const result = await grid.runPrompt('group by region, then sort by revenue, highest first');
if (result.mode === 'control') {
console.log(result.applied); // e.g. ['group', 'sort']
console.log(result.warnings); // anything dropped or unmapped, each with a reason
result.undo(); // one-click revert
}
// Read-only question; the grid is not changed.
const answer = await grid.runPrompt('which region has the highest average revenue?', { mode: 'ask' });
// answer.mode === 'ask'; answer.answer is the text replyrunPrompt validates every planned change against the schema (dropping anything
out of vocabulary, with a reported reason), applies it via the defensive
setState(), and returns an idempotent undo() that restores the prior
snapshot. Ask mode never mutates the grid.
Honest fallback (no silent no-ops)
When a prompt cannot be mapped to a grid action and no LLM reasoner is
configured, runPrompt abstains rather than silently doing nothing or
guessing:
const result = await grid.runPrompt('teleport the widget sideways');
// result.mode === 'ask', result.abstained === true
// result.answer is a short "I could not turn that into a grid action" noteThe panel renders this as a distinct message (with a hint) instead of an empty result. Confidence is calibrated on how much the prompt actually grounds against your schema, so a near-miss command scores low and either escalates to your LLM reasoner or abstains, instead of returning a confident wrong answer.
Optional LLM escalation
Leave runPrompt as is for the offline rule engine. To also handle prompts the
rules cannot map, assign a Reasoner. The grid tries the rule engine first and
escalates only when it is unsure, so simple requests stay instant and offline.
import { createClaudeReasoner, createLLMReasoner } from 'apex-grid-enterprise';
// Production: your backend holds the key and calls Anthropic; the browser never sees it.
grid.aiReasoner = createClaudeReasoner({ endpoint: '/api/grid-ai' });
// Development only: call Anthropic from the browser. This exposes the key to the page.
grid.aiReasoner = createClaudeReasoner({ apiKey: '...', dangerouslyAllowBrowser: true });
// Or bring any provider by implementing a single completion function.
grid.aiReasoner = createLLMReasoner({ complete: async (req) => ({ patch: /* ... */ }) });The Claude direct transport dynamically imports @anthropic-ai/sdk (an optional
peer dependency), defaults to claude-opus-4-8 (configurable), and uses tool use
so the model returns a state patch shaped by the grid's schema (toJSONSchema).
The proxy transport POSTs { prompt, mode, schema, data } to your endpoint,
which returns { patch?, answer? }. Prefer the proxy for production. Install the
SDK only when you use the direct transport:
npm install @anthropic-ai/sdkThe AI runtime is loaded on demand, on the first runPrompt / previewPrompt, so
a grid that never runs a prompt bundles none of it.
Prompt panel
<apex-grid-ai> is a ready-made prompt UI. Bind it to a grid and it drives
runPrompt for you, showing what changed (with an Undo button), the answer in
ask mode, or the honest fallback when a prompt could not be mapped.
<apex-grid-ai mode="inline"></apex-grid-ai>document.querySelector('apex-grid-ai').grid = grid;mode="inline" renders in place; mode="dialog" (the default) is a floating,
draggable panel. The enterprise grid also adds an "Ask AI" toolbar button
that opens the panel in a dialog (the /define entry registers the element).
How it stays safe
The control path is guarded in layers: an LLM is constrained by the grid's schema
(toJSONSchema), anything out of vocabulary is stripped before it is applied
(each drop reported), the defensive setState() drops and reports the rest, and
every change is one click undoable. Ask mode is read-only, and an unmappable
prompt abstains instead of acting.
Formulas
Spreadsheet-style formulas in a cell. Mark a column allowFormula (and
editable); a cell whose edited text starts with = is parsed, stored, and its
computed result becomes the cell value. Because the value stays canonical in
row[key], sorting, filtering, aggregation, export, and charts all keep working
on the result.
grid.columns = [
{ key: 'qty', headerText: 'Qty', type: 'number', editable: true },
{ key: 'price', headerText: 'Price', type: 'currency', editable: true },
{ key: 'total', headerText: 'Total', type: 'currency', editable: true, allowFormula: true },
];
// Type "=B1*C1" into a Total cell, or set it programmatically:
grid.setFormula(grid.data[0], 'total', '=B1*C1');
grid.getFormula(grid.data[0], 'total'); // '=B1*C1'
grid.clearFormula(grid.data[0], 'total');
grid.recalculateFormulas();References (A1 over the data)
Columns map to letters by configuration order (A is the first column,
including hidden ones); rows are 1-based over the source data. So B2 is the
second data row, second column. References bind to the data, not the rendered
view, so a formula keeps its meaning across sort, filter, column reorder, and
paging. Ranges (A1:B3) expand to value lists for functions.
References are relative by default; a $ fixes an axis ($A$1 fixes both,
$A1 the column only, A$1 the row only). Relative references shift when a
formula is filled or pasted to another cell; absolute axes stay put. A reference
always resolves to a concrete cell, so recalculation itself is unaffected by
relative-ness.
Identity: formula attachment is durable when you set a grid rowId (it survives
reload through getState / setState); without one it is positional.
Authoring
While editing a formula (after typing =):
- Autocomplete: typing a function name shows a suggestion list (built-ins +
your custom functions). Up/Down to move, Enter/Tab or click to accept (inserts
NAME()with the caret between the parentheses), Escape to dismiss. - Click-to-insert: click any grid cell to insert its reference at the caret;
Shift-click inserts an absolute
$A$1. - Drag-to-insert a range: press on a cell and drag across the grid to insert
a live
A1:C3range reference; a dashed marquee tracks the cells as you drag. Clicking again re-picks the reference (replaces it); type an operator to add a second one. Escape backs out of a just-picked reference without leaving the edit. - F4 absolute/relative: with the caret on a reference, press F4 to cycle its
$markers:A1 -> $A$1 -> A$1 -> $A1 -> A1.
Point-and-click reference entry is mouse-driven: while editing, the arrow keys always move the text caret and never start referencing cells.
Drag-to-fill and paste rewrite a formula's relative references by the
row/column delta to the new cell (absolute $ axes are preserved):
// Drag the fill handle, or do it in code: copy total[0]'s formula down.
grid.selectRange({ row: 0, column: 'total' });
grid.fillTo({ row: 4, column: 'total' }); // =B1*C1 becomes =B2*C2, =B3*C3, ...Copying a range and pasting it back inside the same grid re-offsets the source formulas (a plain-text clipboard cannot carry formulas, so cross-app paste stays literal).
Reference highlighting
While a formula cell is being edited, every cell the formula references lights up in
the grid, and each distinct reference gets its own color (a small palette cycles), so
=B2*C2 highlights B2 and C2 differently, spreadsheet-style. The highlight tracks the
text as you type (invalid mid-edit input keeps the last good highlight) and clears when
the editor closes. It is applied as a data-formula-ref cell decoration, so you can
theme the colors with CSS.
To make those references easy to read, spreadsheet coordinates (a row-number gutter
plus A / B / C column letters) are shown by default for any grid that has
allowFormula columns. Reserving the gutter up front means entering a formula never
shifts the layout: the gutter would otherwise pop in on the first edit and nudge every
column. An explicit coordinateHints you set afterwards is left untouched.
Functions
Built in:
- Math:
SUM,AVERAGE(aliasAVG),MIN,MAX,COUNT,COUNTA,ROUND,ROUNDUP,ROUNDDOWN,ABS,MOD,POWER,SQRT,INT,SIGN. - Logical:
IF,AND,OR,NOT. - Text:
CONCAT(aliasCONCATENATE),LEN,LEFT,RIGHT,MID,TRIM,UPPER,LOWER.
Operators: + - * / ^ %, comparisons = <> < > <= >=, and text concatenation
with &. IF short-circuits, so IF(B1=0, 0, A1/B1) is safe. Register your own:
grid.registerFormulaFunction('TAX', (args) =>
typeof args[0] === 'number' ? args[0] * 0.2 : 0
);
// =TAX(B1)Error values
Errors are first-class cell values that render as their code and are excluded
from numeric aggregates: #REF! (bad reference), #NAME? (unknown function),
#DIV/0!, #VALUE! (type error), and #CYCLE! (circular reference). An error
operand propagates.
Recalculation and persistence
Editing any referenced cell recomputes its dependents in dependency order (a
real dependency graph with cycle detection, not a full sweep); undo or redo of a
value edit recomputes too. Formulas serialize under modules.enterprise.formulas
in getState() and restore (and recompute) on setState().
Show formulas
Set showFormulas (the show-formulas attribute or the property) to display
each allowFormula cell's source instead of its computed value, the spreadsheet
"show formulas" view. The computed values are untouched, so turning it off
restores the normal display; a user-provided cellTemplate is never overridden.
grid.showFormulas = true; // reveal sources; set back to false to restoreExport formulas
exportToCSV and exportToXLSX accept a formulas option: when set, cells that
hold a formula export their source (=A1*B1) rather than the computed value;
other cells export normally.
grid.exportToCSV({ filename: 'budget', formulas: true });
grid.exportToXLSX({ filename: 'budget', formulas: true });Deferred
Range+criteria functions (SUMIF / COUNTIF / AVERAGEIF) need a different
argument-grouping convention and are deferred to a later tier, along with array
formulas and cross-sheet references.
Licensing
Licensing is offline and non-hostile: without a valid key the grid keeps working but renders a watermark and logs a console notice, with no network calls and no hard blocking. Set the key once (globally) before or after the grid renders:
import { ApexGridEnterprise } from 'apex-grid-enterprise';
ApexGridEnterprise.setLicense('APEX-…'); // removes the watermark on all instancesLicenseManager (re-exported from apex-commons) is also available for advanced
use, but setLicense is the supported entry point.
Keys are ECDSA-signed. Verification is asynchronous (WebCrypto has no synchronous
API) while the watermark decision is synchronous, so a structurally valid key is
accepted provisionally and the grid re-checks when the signature verdict settles:
a tampered key lifts the watermark for a microtask and then gets it back. The
overlay itself is apex-commons' shared Watermark, the same one the other Apex
products paint, and it keeps the license-watermark part for styling.
See also
apex-grid: the community grid with column configuration, virtualization, sorting, filtering, pagination, pinning, reordering, resizing, inline editing, selection, tree data, CSV export, theming, and accessibility.
