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.
Create and execute
Section titled “Create and execute”Headless controller
Create controller idle/autoLoad, refresh and render model without grid
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.
Subscribe to committed and pending state
Section titled “Subscribe to committed and pending state”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 and transactions
Section titled “Commands and transactions”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.
Source changes and lifetime
Section titled “Source changes and lifetime”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.
Public queries and operation results
Section titled “Public queries and operation results”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.
| Operation | Purpose |
|---|---|
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.