Skip to content

Formula (calculated) columns for the data grids #17550

Description

@ChronosSF

Component / Area

Grid

Is your feature request related to a problem?

The grid has no concept of a column whose value is derived rather than stored, and no way for an end user
to create one. Every value path in the data pipeline resolves a column against the record by field path —
resolveNestedPath(record, columnFieldPath(field)) in
filtering-strategy.ts:270,
grid-sorting-strategy.ts:103,
merge-strategy.ts:120,
tree-grid-filtering-strategy.ts:34
and pipes.ts:336.

Today a "calculated column" is faked in one of two ways, and both are lossy:

  1. formatter (column.component.ts:735) —
    changes only what is rendered. Sorting, filtering, grouping, summaries, search and export all still see the
    underlying (usually undefined) field value. It is also declared /* blazorOnlyScript */, so it does not
    cross over to the Web Components / Blazor wrappers.
  2. Pre-computing the value into the data source. This works, but it forces the app to re-derive on every
    edit, breaks down with remote data and batch editing, bloats the row model, and — critically — makes the set
    of derived columns something only a developer can change, at build time.

The escape hatch of "unbound column + custom strategies" exists but is unergonomic: the user must supply a
sortStrategy, a filters operand, a groupingComparer, a mergingComparer and a summaries operand
separately and consistently just to make one derived column behave like a real one. See
grid.groupby.spec.ts:885
(should group unbound column with custom grouping strategy) for how much scaffolding that takes.

Both workarounds miss the part users actually ask for. Someone coming from Excel does not want an API to
declare Price * Quantity — they want to click a button in the grid, type =[Price] * [Quantity], and get a
new column that sorts, filters, groups, summarizes and exports like any other. A formula column that only
a developer can author is a fraction of the feature.

Describe the solution you'd like

Introduce a formula column: a column whose value is produced by a declarative expression evaluated against
the row, which participates in the full data pipeline as if it were a stored field, and which end users can
create, edit and remove at runtime through built-in grid UI
.

Scope of the MVP

The MVP is deliberately end-to-end — engine and UI — because a developer-only API does not solve the problem
that motivates the request.

Engine and data pipeline:

  • Expression-backed column value, resolved before sorting/filtering/grouping/summaries rather than at render time.
  • A safe, serializable expression language (no eval, no Function constructor).
  • Cross-column references ([Price] * [Quantity]) and a function library.
  • Dependency tracking and incremental recalculation on cell/row edits, transactions and data source changes.
  • Read-only cells by default; editing the formula, not the value.
  • Participation in: sorting, filtering (including Excel-style filtering value lists), grouping, summaries,
    search, clipboard copy, Excel/CSV export as computed values, and column state persistence.

UI (part of the MVP, not a follow-up):

  • A formula editor dialog — expression input with syntax validation, a column reference picker, a function
    list, a result data type selector, a header/name input, and a live preview against the first few rows.
  • An entry point to create a calculated column — a grid toolbar action, mirroring
    igx-grid-toolbar-advanced-filtering.
  • Edit and delete of an existing formula column from its header menu / column actions.
  • Error presentation — an error value in the offending cell (#DIV/0!-style) plus an indicator on the
    column header, and inline validation in the editor before the formula is committed.
  • Full keyboard, screen reader, theming and localization parity with the rest of the grid UI (see
    non-functional requirements — these are acceptance criteria, not polish).

Explicitly out of scope for the MVP — call this out in the issue so it is not assumed:

  • Cross-row / cross-sheet references (SUM(A1:A10), references to other rows). The MVP is strictly
    row-scoped, plus column-level aggregates only if Q4 below is answered yes.
  • Circular reference resolution beyond detection and clear error reporting.
  • Server-side formula evaluation for remote data (see risk R3).
  • Export of live = worksheet formulas to Excel (values only in the MVP).
  • Tree Grid aggregate references, Pivot calculated measures, Grid Lite support.

Functional requirements — engine

# Requirement
F1 A column can declare a formula instead of (or in addition to) a field.
F2 The formula is evaluated per record and the result is what the whole pipeline sees, not just the renderer.
F3 Formulas may reference other columns, including other formula columns, by header or field name.
F4 Formula columns are sortable, filterable, groupable and summarizable with no extra strategy code.
F5 The result has a dataType (inferred from the expression, overridable), so date/number/currency pipeArgs, editors and filtering operands work unchanged.
F6 Cells are read-only by default; editable on a formula column means "edit the formula", not "edit the value".
F7 Editing a dependency recalculates dependents — through cell edit, row edit, batch editing/transactions and data reassignment.
F8 Evaluation errors are contained: a bad row yields an error value in that cell, never a thrown exception that kills the pipeline.
F9 Circular dependencies are detected at definition time and reported, not left to blow the stack.
F10 Formula definitions — including ones created at runtime by the user — round-trip through IgxGridStateDirective.
F11 Excel and CSV export contain the computed values.
F12 The API is expressible for the Web Components and Blazor wrappers — i.e. the primary form must be a string, not a callback.

Functional requirements — UI

# Requirement
U1 The grid exposes a toolbar action that opens the formula editor and adds a new calculated column on commit.
U2 The editor validates the expression as it is typed — unknown column, unknown function, arity mismatch, syntax error — and blocks commit with a message pointing at the offending token.
U3 The editor offers autocomplete for column references and function names, reusing IgxAutocompleteDirective.
U4 The editor shows a live preview of the computed result over the first N visible rows before the column is created.
U5 The user can set the new column's header, result data type and format (pipeArgs) from the editor.
U6 An existing formula column can be edited or deleted from its header context menu / column actions, without losing grid state.
U7 Cells whose evaluation failed render a themed error state with the error kind; the header carries an indicator when any row in the column errored.
U8 The whole feature can be disabled, or restricted to a set of columns, by the developer (allowFormulaColumns) — apps that must not let users derive new data can turn it off.
U9 User-created columns are indistinguishable from developer-declared ones for every other grid feature (pinning, hiding, moving, resizing, selection, export).
U10 All editor strings come from the grid resource strings and ship translations.

Non-functional requirements

  • Virtualization-safe. Evaluation must not run over the whole data set on every change detection cycle.
    Memoize per record and formula version; evaluate visible rows eagerly and aggregates lazily.
  • Zoneless-safe. No NG0100 (see the in-flight NG0100-zoneless-audit.md work) — recalculation must not
    mutate bound state during a render pass.
  • No eval / new Function. The evaluator must be a real tokenizer plus AST walker so the feature works
    under a strict CSP. This is a hard requirement given SECURITY.md, and it is sharper now that formulas are
    authored by end users rather than only by developers.
  • Accessible. The editor is a dialog: focus trap, labelled controls, aria-describedby on validation
    errors, full keyboard operation, and screen-reader-announced error states in cells — same bar as the
    advanced filtering dialog.
  • Themed. Theme files for all supported themes, light and dark, following the
    query builder theme layout
    (_base.scss, _derived.scss, light/, dark/, shared/).
  • Localized. New strings added to
    grid-resources.ts and translated in
    projects/igniteui-angular-i18n/.
  • Tree-shakable. Apps that never use a formula column must pay for neither the parser nor the editor UI —
    the editor should be an opt-in import like the toolbar actions are.

Proposed API or Usage

<!-- Developer-declared formula column -->
<igx-column header="Total"
            [formula]="'[Price] * [Quantity] * (1 - [Discount])'"
            dataType="currency"
            [pipeArgs]="{ digitsInfo: '1.2-2', currencyCode: 'EUR' }">
</igx-column>

<igx-column header="Status" [formula]="statusFormula" dataType="string"></igx-column>

<!-- End-user authoring: toolbar entry point, mirroring the advanced filtering action -->
<igx-grid [data]="data" [allowFormulaColumns]="true">
    <igx-grid-toolbar>
        <igx-grid-toolbar-actions>
            <igx-grid-toolbar-formula-column></igx-grid-toolbar-formula-column>
        </igx-grid-toolbar-actions>
    </igx-grid-toolbar>
</igx-grid>



// statusFormula = `IF([Stock] = 0, 'Out of stock', IF([Stock] < 10, 'Low', 'OK'))`

// --- Programmatic / column definition object ---
grid.columns = [
    { field: 'Price', dataType: 'number' },
    { field: 'Quantity', dataType: 'number' },
    {
        header: 'Total',
        // string form: serializable, works in WC/Blazor, round-trips through grid state,
        // and is the same shape the UI produces
        formula: '[Price] * [Quantity]',
        dataType: 'currency'
    },
    {
        header: 'Margin %',
        // escape hatch: full TS, Angular-only, /* blazorOnlyScript */, not authorable from the UI
        formulaFn: (row: any) => (row.Total - row.Cost) / row.Total,
        dependsOn: ['Total', 'Cost'],   // explicit deps when they cannot be parsed
        dataType: 'percent'
    }
];

// --- Runtime authoring (what the UI calls under the hood) ---
grid.addFormulaColumn({ header: 'Total', formula: '[Price] * [Quantity]', dataType: 'currency' });
grid.getColumnByName('Total').formula = '[Price] * [Quantity] * 1.2';   // recalculates dependents

// --- Validation, usable standalone by an app building its own editor ---
const result = grid.formulaEngine.validate('[Price] * [Qty]');
// { valid: false, error: 'UNKNOWN_COLUMN', token: 'Qty', position: 10 }

// --- Events ---
grid.formulaColumnAdded.subscribe((e: IFormulaColumnEventArgs) => { /* ... */ });
grid.formulaColumnEdited.subscribe((e: IFormulaColumnEventArgs) => { /* ... */ });
grid.formulaError.subscribe((e: IFormulaErrorEventArgs) => { /* ... */ });


### New / changed public surface (sketch)

| Member | Location | Notes |
|---|---|---|
| `formula: string` | `IgxColumnComponent` | Primary, serializable form; what the UI writes. |
| `formulaFn?: (rowData) => any` | `IgxColumnComponent` | Angular-only escape hatch, `/* blazorOnlyScript */`. |
| `dependsOn?: string[]` | `IgxColumnComponent` | Explicit dependencies for `formulaFn`. |
| `isFormulaColumn: boolean` (readonly) | `ColumnType` | Consumed by editing, export, state and the header menu. |
| `formulaErrorTemplate` | `IgxColumnComponent` | Themed error cell override. |
| `allowFormulaColumns: boolean` | grid base | Master switch for end-user authoring (U8). |
| `addFormulaColumn(def)` / `removeFormulaColumn(field)` | grid base | Runtime API behind the UI. |
| `formulaEngine: IgxFormulaEngine` | grid base | Parse / validate / evaluate; injectable and swappable. |
| `IgxFormulaEditorComponent` | new, `grids/core/src/formula/` | The dialog; usable standalone. |
| `IgxGridToolbarFormulaColumnComponent` | `grids/core/src/toolbar/` | Entry point, mirrors the advanced filtering action. |
| `formula?: string` on `IColumnState` | [state-base.directive.ts:61](projects/igniteui-angular/grids/core/src/state-base.directive.ts#L61) | Persistence of user-created columns. |
| `formulaColumnAdded` / `formulaColumnEdited` / `formulaError` | grid base | Events and diagnostics. |

### Suggested expression language (MVP)

- **Operators:** `+ - * / %`, `^`, comparison `= <> < <= > >=`, logical `AND` / `OR` / `NOT`, string concat `&`.
- **References:** `[Column Header]` or `[field.nested.path]`.
- **Literals:** number, single-quoted string, `TRUE` / `FALSE`, `NULL`.
- **Functions (starter set, Excel-named):**
  - Math: `ABS ROUND ROUNDUP ROUNDDOWN CEILING FLOOR MIN MAX SUM POWER SQRT MOD`
  - Logic: `IF IFS AND OR NOT ISBLANK ISERROR IFERROR`
  - Text: `CONCAT LEFT RIGHT MID LEN UPPER LOWER TRIM SUBSTITUTE TEXT`
  - Date: `TODAY NOW YEAR MONTH DAY DATEDIF EDATE`
- **Error values:** `#DIV/0!`, `#VALUE!`, `#REF!`, `#CIRCULAR!`  mirroring Excel so the semantics are familiar.

The function registry must be public and extensible, since the editor's function list and autocomplete are
generated from it: registering a custom function must surface it in the UI with no extra work.

Describe alternatives you've considered

  • formatter only — rejected: display-only, invisible to sorting, filtering, grouping, summaries and
    export, and unusable as a target for end-user authoring.
  • Pre-computing in the data source — the current guidance; works, but pushes recalculation, edit tracking
    and remote-data consistency onto every app, and cannot support user-created columns at all.
  • Unbound column plus custom strategies — possible today, but requires a matched set of sorting, filtering,
    grouping, merging and summary strategies per column; too much ceremony for a common need, and impossible to
    drive from a dialog.
  • Engine-only MVP, UI in a later release — considered and rejected: the primary ask is end-user authoring,
    and shipping the engine alone would mean every customer builds their own editor against a parser API that was
    never designed to support one. Deferring the UI would also let the parser ship without structured, positioned
    errors, which is exactly the thing an editor needs.
  • A pipe-based derived data source — recomputes the whole array on any change and defeats virtualization.

How important is this feature to you?

Important — affects my workflow

Additional context

No response

Metadata

Metadata

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions