Skip to content

Headless Controller

Use createPivotController to execute a portable report and build an authoring interface without creating a RevoGrid element. The controller owns accepted report state, pending changes, execution, and successful authoring history.

The @revolist/pivot/headless subpath supports ESM imports. It shares the package root’s runtime modules to preserve callback and report identity; it does not promise a smaller dependency footprint. The existing Pivot plugin remains the grid integration API. For a custom constructor on an existing grid, use its getAuthoringController() facade; do not mirror it with another headless owner. See Custom Integrations for complete checked recipes.

Interactive example

Headless controller

Create controller idle/autoLoad, refresh and render model without grid

The example loads when visible.

import { createPivotController, type PivotReport } from '@revolist/pivot/headless';
const report: PivotReport = {
kind: 'revogrid:pivot.report',
version: 1,
fields: [
{ id: 'region', sourceProp: 'region' },
{ id: 'revenue', sourceProp: 'sales' },
],
rows: ['region'],
columns: [],
filters: [],
measures: [{ id: 'revenue-total', fieldId: 'revenue', aggregator: 'sum' }],
};
const controller = createPivotController({
report,
source: [{ region: 'North', sales: 10 }, { region: 'North', sales: 5 }],
});
await controller.refresh();
const snapshot = controller.getSnapshot();
console.log(snapshot.view?.model.source);

Creation validates the report and runtime bindings synchronously. It starts idle unless autoLoad: true is supplied. Use the runtime registries described in Portable Reports for custom callbacks, bound sources, and server adapters. There is no public executor override.

getSnapshot() returns the same reference until state changes. subscribe() does not call its listener immediately; read the initial snapshot yourself.

During loading, report and view retain the last usable pair. pendingReport exposes the accepted candidate. An execution failure sets status: 'error' and an error diagnostic while preserving that usable pair.

const unsubscribe = controller.subscribe(() => {
const state = controller.getSnapshot();
console.log(state.status, state.pendingReport, state.error);
});
const outcome = await controller.dispatch({
type: 'measure.configure',
measureId: 'revenue-total',
patch: { label: 'Revenue' },
});
if (outcome.status === 'failed') console.error(outcome.error.message);
unsubscribe();

Report data and owned view containers are readonly. Application records, runtime handles, and custom aggregate payloads remain application-owned. Nested opaque payloads are not recursively frozen; do not use them to change report state.

Commands address stable field and measure IDs. Repeated uses of one source field can therefore be configured independently. Moves use the destination index after removing the source item. Filters are independent of row/column assignment.

await controller.transaction([
{ type: 'measure.add', fieldId: 'revenue', measureId: 'average-revenue', aggregator: 'avg' },
{ type: 'measure.configure', measureId: 'average-revenue', patch: { label: 'Average revenue' } },
{ type: 'totals.configure', patch: { grandTotal: true } },
]);
// Delete an optional setting explicitly.
await controller.dispatch({
type: 'measure.configure',
measureId: 'average-revenue',
unset: ['label'],
});
await controller.undo();
await controller.redo();

A transaction validates its final references and commits as one history entry. Accepted edits queued within one microtask compose on pending intent and can share one execution. Superseded operations settle as superseded; invalid commands throw before changing state. A failed operation does not advance the history cursor. historyLimit defaults to 100; zero disables undo history. reset() restores the initial report as an authoring operation.

Call replaceSource(records) when replacing application records, or refresh() after mutating records in place. Source, runtime replacement, and refresh are execution barriers and preserve accepted pending authoring choices.

If new source execution fails, sourceRevision describes the current input intent while viewSourceRevision identifies the input behind the retained view. replaceRuntime(runtime) explicitly replaces execution bindings and executes; this differs from the plugin’s nonexecuting setPivotReportRuntime defaults.

cancel() promptly settles pending work even when a remote adapter ignores its abort signal. Late results cannot publish. dispose() cancels work and removes subscriptions. Call it when the owning component is destroyed.

For persistence, use getReport() or exportReportJson(). Runtime callbacks and source records remain outside the JSON report and must be reattached by the app.

The optional constructor query and replaceQuery(query) describe execution scope separately from the authored report. sourceFilter uses tuple predicates before aggregation; having addresses measure IDs after aggregation. sort uses active axis fields and optional bySummary measure IDs. Bare string/number selectors address physical properties; { kind: 'field', fieldId } addresses a report field. The Portable Reports query example shows the complete typed shape and version admission rules.

rowOffset, rowLimit, columnOffset, and columnLimit are nonnegative safe integers. Zero means an empty analytical window; omitted local windows include the complete result. replaceQuery({}) clears public query intent, preserving report member selections. Query changes are outside authoring undo/redo history, and snapshot.query describes the last committed view.

OperationPurpose
replaceReport(report)Validate and execute another report in the existing owner.
dispatch(command) / transaction(commands)Apply one authoring edit or one atomic batch.
replaceQuery(query)Replace the public execution scope.
replaceSource(records) / replaceRuntime(runtime)Replace application inputs and execute.
refresh()Execute current intent, including in-place record changes.
undo() / redo() / reset()Navigate successful authored changes or restore the initial report.
getReport() / exportReportJson()Read portable authored state for application-owned persistence.
cancel() / dispose()Cancel pending execution or end the owner’s lifetime.

Async execution operations resolve to a PivotControllerOutcome: committed, noop, prevented, superseded, cancelled, or failed. Only committed publishes a new revision. For failed, inspect outcome.error and the retained snapshot. Invalid command/query/report inputs can throw during validation before execution; handle both exceptions and failed outcomes in application controls. Subscriptions take no snapshot argument: read controller.getSnapshot() inside the listener and unsubscribe on cleanup.