Configure the dialog users create and edit calculated columns in, and customise the columns they create.
The Calculated Column dialog has three fields: Title, Type and Expression. Configure the dialog with the calculatedColumns grid option. Set it to true to enable the feature with the default dialog, or to an options object to enable the feature and customise the dialog.
Opening the Dialog Copy Link
Users open the dialog from the grid's menus; see Letting Users Add Columns. To open it from your own interface, call one of these grid API methods:
openCalculatedColumnDialog(column)to edit or view an existing calculated column, bringing its dialog forward if already open.openAddCalculatedColumnDialog(sourceColumn?)to add one after a source column, or at the end when omitted.
Both methods focus the dialog and use the configured apply mode. Adding is unavailable in pivot mode.
// Call from an application button.
function onAddCalculatedColumn() {
gridApi.openAddCalculatedColumnDialog();
}Closing the dialog returns focus to the calculated column's header, scrolling it into view if needed. If a deferred Add was not applied, focus returns to the source column when supplied. Focus is not moved if you have already focused another element.
Opens and focuses the dialog for an existing calculated column.
Read-only calculated columns open in view mode. Reuses and brings forward an already-open dialog.
On close, returns focus to the column's header, scrolling it into view if needed.
Does nothing if Calculated Columns are disabled or the key does not resolve to a calculated column. |
Opens the Add Calculated Column dialog, optionally after a source column as with the column menu.
Without a source column, the new column is appended. Unknown or non-primary source columns are ignored.
Uses the configured apply mode: live creates the column immediately, deferred creates it on Apply.
Moves focus into the dialog. Does not open in pivot mode.
On close, returns focus to the new column's header, or the source column if no column was created. |
Referencing Columns Copy Link
The dialog shows references by header name, for example [Revenue]. It translates these display references back to colId references before updating the column definition. Some headers need more than the header name:
- Duplicate headers under column groups use the shortest unique group path, such as
[2025 Q4]and[2026 Q4]. - When two columns have the same header and the same group hierarchy, the dialog appends the
colId, for example[2025 Q4 (revenue-q4)]. This keeps the reference stable when columns are reordered. Give your columns distinct headers or group hierarchies to avoid this. - A literal
]in a header name is escaped as]]. For example, the headerRevenue [USD]appears as[Revenue [USD]]]. The Columns picker inserts the escaped reference automatically; storedcolIdreferences are unchanged.
The following example has duplicate Q1, Q2, Q3 and Q4 headers under 2025 and 2026 column groups. The year groups start collapsed and show a calculated Total column; expanding a group reveals the quarter columns. The dialog shows grouped references such as [2025 Q4] and [2026 Q4], while the source code stores stable colId references such as [q4_2025] and [q4_2026].
import { Component } from "@angular/core";
import { AgGridAngular } from "ag-grid-angular";
import {
CalculatedColumnsGridOption,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
DataTypeDefinitions,
GridApi,
GridOptions,
GridReadyEvent,
ModuleRegistry,
NumberFilterModule,
ValueFormatterLiteParams,
enableDevValidations,
} from "ag-grid-community";
import { CalculatedColumnsModule, ColumnMenuModule } from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
ModuleRegistry.registerModules([
ClientSideRowModelModule,
CalculatedColumnsModule,
ColumnMenuModule,
NumberFilterModule,
]);
import { QuarterlyRevenueRow } from "./interfaces";
@Component({
selector: "my-app",
standalone: true,
imports: [AgGridAngular],
template: `<div style="height: 100%; box-sizing: border-box">
<ag-grid-angular
style="width: 100%; height: 100%;"
[columnDefs]="columnDefs"
[rowData]="rowData"
[dataTypeDefinitions]="dataTypeDefinitions"
[calculatedColumns]="calculatedColumns"
[defaultColDef]="defaultColDef"
/>
</div> `,
})
export class AppComponent {
columnDefs: (ColDef | ColGroupDef)[] = [
{ field: "product", pinned: "left", minWidth: 180, flex: 1.4 },
{
headerName: "2025",
openByDefault: false,
children: [
quarterColumn("q1_2025"),
quarterColumn("q2_2025"),
quarterColumn("q3_2025"),
quarterColumn("q4_2025"),
{
colId: "total_2025",
headerName: "Total",
columnGroupShow: "closed",
calculatedExpression: "[q1_2025] + [q2_2025] + [q3_2025] + [q4_2025]",
cellDataType: "currency",
},
],
},
{
headerName: "2026",
openByDefault: false,
children: [
quarterColumn("q1_2026"),
quarterColumn("q2_2026"),
quarterColumn("q3_2026"),
quarterColumn("q4_2026"),
{
colId: "total_2026",
headerName: "Total",
columnGroupShow: "closed",
calculatedExpression: "[q1_2026] + [q2_2026] + [q3_2026] + [q4_2026]",
cellDataType: "currency",
},
],
},
{
headerName: "Change",
children: [
{
colId: "q4Change",
headerName: "Q4 Change",
calculatedExpression: "([q4_2026] - [q4_2025]) / [q4_2025]",
cellDataType: "percentage",
sortable: true,
filter: "agNumberColumnFilter",
},
{
colId: "yearChange",
headerName: "Year Change",
calculatedExpression:
"([q1_2026] + [q2_2026] + [q3_2026] + [q4_2026]) - ([q1_2025] + [q2_2025] + [q3_2025] + [q4_2025])",
cellDataType: "currency",
sortable: true,
filter: "agNumberColumnFilter",
},
],
},
];
rowData: QuarterlyRevenueRow[] | null = [
{
product: "Solar panel kit",
q1_2025: 35000,
q2_2025: 38000,
q3_2025: 42000,
q4_2025: 44000,
q1_2026: 48000,
q2_2026: 50000,
q3_2026: 55000,
q4_2026: 58000,
},
{
product: "Smart thermostat",
q1_2025: 18000,
q2_2025: 20000,
q3_2025: 22000,
q4_2025: 25000,
q1_2026: 24000,
q2_2026: 26000,
q3_2026: 27000,
q4_2026: 31000,
},
{
product: "Battery pack",
q1_2025: 29000,
q2_2025: 31000,
q3_2025: 35000,
q4_2025: 39000,
q1_2026: 41000,
q2_2026: 43000,
q3_2026: 46000,
q4_2026: 52000,
},
{
product: "EV charger",
q1_2025: 22000,
q2_2025: 24000,
q3_2025: 26000,
q4_2025: 28000,
q1_2026: 30000,
q2_2026: 32000,
q3_2026: 34000,
q4_2026: 36000,
},
{
product: "Heat pump",
q1_2025: 36000,
q2_2025: 38500,
q3_2025: 41000,
q4_2025: 43500,
q1_2026: 46000,
q2_2026: 48500,
q3_2026: 51000,
q4_2026: 53500,
},
{
product: "Inverter unit",
q1_2025: 21000,
q2_2025: 22500,
q3_2025: 24000,
q4_2025: 25500,
q1_2026: 27000,
q2_2026: 28500,
q3_2026: 30000,
q4_2026: 31500,
},
{
product: "Wind turbine kit",
q1_2025: 52000,
q2_2025: 55000,
q3_2025: 58000,
q4_2025: 61000,
q1_2026: 64000,
q2_2026: 67000,
q3_2026: 70000,
q4_2026: 73000,
},
{
product: "Solar tile roof",
q1_2025: 45000,
q2_2025: 47800,
q3_2025: 50600,
q4_2025: 53400,
q1_2026: 56200,
q2_2026: 59000,
q3_2026: 61800,
q4_2026: 64600,
},
{
product: "Power optimiser",
q1_2025: 15000,
q2_2025: 16100,
q3_2025: 17200,
q4_2025: 18300,
q1_2026: 19400,
q2_2026: 20500,
q3_2026: 21600,
q4_2026: 22700,
},
{
product: "Charge controller",
q1_2025: 12500,
q2_2025: 13400,
q3_2025: 14300,
q4_2025: 15200,
q1_2026: 16100,
q2_2026: 17000,
q3_2026: 17900,
q4_2026: 18800,
},
{
product: "Energy monitor",
q1_2025: 11000,
q2_2025: 11800,
q3_2025: 12600,
q4_2025: 13400,
q1_2026: 14200,
q2_2026: 15000,
q3_2026: 15800,
q4_2026: 16600,
},
{
product: "Storage cabinet",
q1_2025: 17000,
q2_2025: 18200,
q3_2025: 19400,
q4_2025: 20600,
q1_2026: 21800,
q2_2026: 23000,
q3_2026: 24200,
q4_2026: 25400,
},
{
product: "Microinverter",
q1_2025: 14000,
q2_2025: 15000,
q3_2025: 16000,
q4_2025: 17000,
q1_2026: 18000,
q2_2026: 19000,
q3_2026: 20000,
q4_2026: 21000,
},
{
product: "Heat recovery unit",
q1_2025: 28000,
q2_2025: 29900,
q3_2025: 31800,
q4_2025: 33700,
q1_2026: 35600,
q2_2026: 37500,
q3_2026: 39400,
q4_2026: 41300,
},
{
product: "Hybrid boiler",
q1_2025: 34000,
q2_2025: 36300,
q3_2025: 38600,
q4_2025: 40900,
q1_2026: 43200,
q2_2026: 45500,
q3_2026: 47800,
q4_2026: 50100,
},
{
product: "Smart meter",
q1_2025: 9000,
q2_2025: 9700,
q3_2025: 10400,
q4_2025: 11100,
q1_2026: 11800,
q2_2026: 12500,
q3_2026: 13200,
q4_2026: 13900,
},
{
product: "Insulation pack",
q1_2025: 10500,
q2_2025: 11250,
q3_2025: 12000,
q4_2025: 12750,
q1_2026: 13500,
q2_2026: 14250,
q3_2026: 15000,
q4_2026: 15750,
},
{
product: "EV cable set",
q1_2025: 7500,
q2_2025: 8050,
q3_2025: 8600,
q4_2025: 9150,
q1_2026: 9700,
q2_2026: 10250,
q3_2026: 10800,
q4_2026: 11350,
},
{
product: "Solar pump",
q1_2025: 16000,
q2_2025: 17150,
q3_2025: 18300,
q4_2025: 19450,
q1_2026: 20600,
q2_2026: 21750,
q3_2026: 22900,
q4_2026: 24050,
},
{
product: "Backup generator",
q1_2025: 40000,
q2_2025: 42600,
q3_2025: 45200,
q4_2025: 47800,
q1_2026: 50400,
q2_2026: 53000,
q3_2026: 55600,
q4_2026: 58200,
},
];
dataTypeDefinitions: DataTypeDefinitions = {
currency: {
baseDataType: "number",
extendsDataType: "number",
valueFormatter: currencyFormatter,
},
percentage: {
baseDataType: "number",
extendsDataType: "number",
valueFormatter: percentageFormatter,
},
};
calculatedColumns: CalculatedColumnsGridOption = {
dataTypes: ["currency", "percentage", "number", "text", "boolean"],
};
defaultColDef: ColDef = {
minWidth: 120,
flex: 1,
};
}
const formatter = (
params: ValueFormatterLiteParams<QuarterlyRevenueRow, number>,
format: (value: number) => string,
): string => {
const { value } = params;
if (value == null) {
return "";
}
if (String(value).startsWith("#")) {
return String(value);
}
return format(value);
};
const currencyFormatter = (
params: ValueFormatterLiteParams<QuarterlyRevenueRow, number>,
) => formatter(params, (value) => `$${value.toLocaleString()}`);
const percentageFormatter = (
params: ValueFormatterLiteParams<QuarterlyRevenueRow, number>,
) =>
formatter(
params,
(value) =>
`${(value * 100).toLocaleString(undefined, { maximumFractionDigits: 1 })}%`,
);
const quarterColumn = (
field: QuarterField,
): ColDef<QuarterlyRevenueRow, number> => ({
field,
colId: field,
headerName: field.slice(0, 2).toUpperCase(),
columnGroupShow: "open",
cellDataType: "currency",
});
Expression Editor Copy Link
The expression editor highlights references, functions and operators. Built-in errors mark the field invalid and have underlines, tooltips and accessible descriptions. Reference checks validate columns, not row availability. When validation runs, and what happens to invalid expressions, depends on the apply mode.
Expression Pickers Copy Link
Buttons beside the expression open the Columns, Functions and Operators pickers. Use expressionPickers to control which pickers appear. By default all three do:
<ag-grid-angular
[calculatedColumns]="calculatedColumns"
/* other grid options ... */ />
this.calculatedColumns = {
expressionPickers: ['columns'],
};
type CalculatedColumnExpressionPicker =
'columns'
| 'functions'
| 'operators'This only controls the expression picker buttons. Use an empty array or null to hide all picker buttons. Inline autocomplete while typing in the expression editor remains available. The example in Column Highlighting shows only the Columns picker.
Column Highlighting Copy Link
While a column's dialog is open, the grid highlights the column's header and cells. Customise the highlight colour with the --ag-calculated-column-highlight-color CSS variable.
Use suppressColumnHighlighting to turn the highlight off. The default is false:
<ag-grid-angular
[calculatedColumns]="calculatedColumns"
/* other grid options ... */ />
this.calculatedColumns = {
suppressColumnHighlighting: true,
};The following example sets both expressionPickers: ['columns'] and suppressColumnHighlighting: true:
import { Component } from "@angular/core";
import { AgGridAngular } from "ag-grid-angular";
import {
CalculatedColumnsGridOption,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
DataTypeDefinitions,
GridApi,
GridOptions,
GridReadyEvent,
ModuleRegistry,
NumberFilterModule,
ValueFormatterLiteParams,
enableDevValidations,
} from "ag-grid-community";
import { CalculatedColumnsModule, ColumnMenuModule } from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
ModuleRegistry.registerModules([
ClientSideRowModelModule,
CalculatedColumnsModule,
ColumnMenuModule,
NumberFilterModule,
]);
import { SalesRow } from "./interfaces";
@Component({
selector: "my-app",
standalone: true,
imports: [AgGridAngular],
template: `<div style="height: 100%; box-sizing: border-box">
<ag-grid-angular
style="width: 100%; height: 100%;"
[columnDefs]="columnDefs"
[rowData]="rowData"
[dataTypeDefinitions]="dataTypeDefinitions"
[calculatedColumns]="calculatedColumns"
[defaultColDef]="defaultColDef"
/>
</div> `,
})
export class AppComponent {
columnDefs: ColDef[] = [
{ field: "product", flex: 1.3 },
{ field: "revenue", cellDataType: "currency" },
{ field: "cost", cellDataType: "currency" },
{
colId: "profit",
headerName: "Profit",
calculatedExpression: "[revenue] - [cost]",
cellDataType: "currency",
filter: "agNumberColumnFilter",
},
];
rowData: SalesRow[] | null = [
{ product: "Solar panel kit", revenue: 142000, cost: 96000 },
{ product: "Smart thermostat", revenue: 78000, cost: 52000 },
{ product: "Battery pack", revenue: 126000, cost: 101000 },
{ product: "EV charger", revenue: 92000, cost: 61000 },
{ product: "Heat pump", revenue: 168000, cost: 119000 },
{ product: "Inverter unit", revenue: 88000, cost: 57000 },
{ product: "Wind turbine kit", revenue: 232000, cost: 171000 },
{ product: "Solar tile roof", revenue: 198000, cost: 144000 },
{ product: "Power optimiser", revenue: 64000, cost: 41000 },
{ product: "Charge controller", revenue: 53000, cost: 33000 },
{ product: "Energy monitor", revenue: 47000, cost: 29000 },
{ product: "Storage cabinet", revenue: 71000, cost: 52000 },
{ product: "Microinverter", revenue: 59000, cost: 37000 },
{ product: "Heat recovery unit", revenue: 124000, cost: 88000 },
{ product: "Hybrid boiler", revenue: 156000, cost: 117000 },
{ product: "Smart meter", revenue: 39000, cost: 24000 },
{ product: "Insulation pack", revenue: 44000, cost: 27000 },
{ product: "EV cable set", revenue: 31000, cost: 18000 },
{ product: "Solar pump", revenue: 67000, cost: 45000 },
{ product: "Backup generator", revenue: 173000, cost: 131000 },
];
dataTypeDefinitions: DataTypeDefinitions = {
currency: {
baseDataType: "number",
extendsDataType: "number",
valueFormatter: currencyFormatter,
},
};
calculatedColumns: CalculatedColumnsGridOption = {
// Show only the Columns picker beside the expression.
expressionPickers: ["columns"],
// Don't highlight the column while its dialog is open.
suppressColumnHighlighting: true,
dataTypes: ["currency", "number", "text", "boolean"],
};
defaultColDef: ColDef = {
flex: 1,
minWidth: 130,
};
}
const currencyFormatter = (
params: ValueFormatterLiteParams<SalesRow, number>,
): string => {
const { value } = params;
if (value == null) {
return "";
}
if (String(value).startsWith("#")) {
return String(value);
}
return `$${value.toLocaleString()}`;
};
In the example, edit Profit. The dialog shows only the Columns picker, and the Profit column is not highlighted while the dialog is open. In the other examples on this page, the column being edited is highlighted.
Data Types Copy Link
Use dataTypes to control which cell data types appear in the dialog's Type dropdown. The default is ['text', 'number', 'date', 'boolean']. The list can include built-in cell data types and custom types registered with dataTypeDefinitions:
<ag-grid-angular
[dataTypeDefinitions]="dataTypeDefinitions"
[calculatedColumns]="calculatedColumns"
[columnDefs]="columnDefs"
/* other grid options ... */ />
this.dataTypeDefinitions = {
currency: {
extendsDataType: 'number',
baseDataType: 'number',
valueFormatter: params =>
params.value == null ? '' : `$${params.value.toLocaleString()}`,
},
};
this.calculatedColumns = {
dataTypes: ['currency', 'number', 'boolean'],
};
this.columnDefs = [
{ field: 'revenue', cellDataType: 'currency' },
{ field: 'cost', cellDataType: 'currency' },
{
colId: 'profit',
headerName: 'Profit',
calculatedExpression: '[revenue] - [cost]',
cellDataType: 'currency',
},
];- Give your own columns the same types, as the snippet does for
revenueandcost, so a user who picks Currency in the dialog gets the same formatting. - A
valueFormatteron a column definition cannot be selected in the dialog, so it is not available to users. - Built-in types use the grid's locale text, while custom type names are converted to readable labels in the dialog.
- If a selected value does not match a registered Cell Data Type, the grid's normal
cellDataTypevalidation applies when the calculated column is created.
import { Component } from "@angular/core";
import { AgGridAngular } from "ag-grid-angular";
import {
CalculatedColumnsGridOption,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
DataTypeDefinitions,
GridApi,
GridOptions,
GridReadyEvent,
ModuleRegistry,
NumberFilterModule,
ValueFormatterLiteParams,
enableDevValidations,
} from "ag-grid-community";
import { CalculatedColumnsModule, ColumnMenuModule } from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
ModuleRegistry.registerModules([
ClientSideRowModelModule,
CalculatedColumnsModule,
ColumnMenuModule,
NumberFilterModule,
]);
import { SalesRow } from "./interfaces";
@Component({
selector: "my-app",
standalone: true,
imports: [AgGridAngular],
template: `<div style="height: 100%; box-sizing: border-box">
<ag-grid-angular
style="width: 100%; height: 100%;"
[columnDefs]="columnDefs"
[rowData]="rowData"
[dataTypeDefinitions]="dataTypeDefinitions"
[calculatedColumns]="calculatedColumns"
[defaultColDef]="defaultColDef"
/>
</div> `,
})
export class AppComponent {
columnDefs: ColDef[] = [
{ field: "product", flex: 1.3 },
{ field: "revenue", cellDataType: "currency" },
{ field: "cost", cellDataType: "currency" },
{
colId: "profit",
headerName: "Profit",
calculatedExpression: "[revenue] - [cost]",
cellDataType: "currency",
filter: "agNumberColumnFilter",
},
];
rowData: SalesRow[] | null = [
{ product: "Solar panel kit", revenue: 142000, cost: 96000 },
{ product: "Smart thermostat", revenue: 78000, cost: 52000 },
{ product: "Battery pack", revenue: 126000, cost: 101000 },
{ product: "EV charger", revenue: 92000, cost: 61000 },
{ product: "Heat pump", revenue: 168000, cost: 119000 },
{ product: "Inverter unit", revenue: 88000, cost: 57000 },
{ product: "Wind turbine kit", revenue: 232000, cost: 171000 },
{ product: "Solar tile roof", revenue: 198000, cost: 144000 },
{ product: "Power optimiser", revenue: 64000, cost: 41000 },
{ product: "Charge controller", revenue: 53000, cost: 33000 },
{ product: "Energy monitor", revenue: 47000, cost: 29000 },
{ product: "Storage cabinet", revenue: 71000, cost: 52000 },
{ product: "Microinverter", revenue: 59000, cost: 37000 },
{ product: "Heat recovery unit", revenue: 124000, cost: 88000 },
{ product: "Hybrid boiler", revenue: 156000, cost: 117000 },
{ product: "Smart meter", revenue: 39000, cost: 24000 },
{ product: "Insulation pack", revenue: 44000, cost: 27000 },
{ product: "EV cable set", revenue: 31000, cost: 18000 },
{ product: "Solar pump", revenue: 67000, cost: 45000 },
{ product: "Backup generator", revenue: 173000, cost: 131000 },
];
dataTypeDefinitions: DataTypeDefinitions = {
currency: {
baseDataType: "number",
extendsDataType: "number",
valueFormatter: currencyFormatter,
},
};
calculatedColumns: CalculatedColumnsGridOption = {
dataTypes: ["currency", "number", "boolean"],
};
defaultColDef: ColDef = {
flex: 1,
minWidth: 130,
};
}
const currencyFormatter = (
params: ValueFormatterLiteParams<SalesRow, number>,
): string => {
const { value } = params;
if (value == null) {
return "";
}
if (String(value).startsWith("#")) {
return String(value);
}
return `$${value.toLocaleString()}`;
};
In the example, edit Profit and open the Type dropdown. It lists Currency, Number and Boolean.
Applying Changes Copy Link
The applyMode option sets when dialog edits reach the column. The default is 'live':
| Live (default) | Deferred (applyMode: 'deferred') | |
|---|---|---|
| Title, type and expression | Update the column as you type | Held until the user clicks Apply; the dialog shows Apply and Cancel buttons |
| Add Calculated Column | Creates the column and opens the dialog over it | Creates the column when the user clicks Apply |
| Invalid expressions | Still apply, and render formula errors in cells until fixed | Validated as you edit and on Apply, and not applied while invalid |
In live mode:
- Empty expressions remain valid.
- Custom validation and reference restrictions still apply.
- Closing the dialog keeps the latest state; remove the column with Remove Calculated Column.
- Expression edits containing references excluded by
isColumnReferenceableare saved but blocked from evaluation, preserving the full text for correction.
Deferred Mode Copy Link
Set applyMode: 'deferred' to hold changes until the user clicks Apply:
<ag-grid-angular
[calculatedColumns]="calculatedColumns"
/* other grid options ... */ />
this.calculatedColumns = {
applyMode: 'deferred',
};
type CalculatedColumnApplyMode = 'live' | 'deferred'In deferred edit dialogs, validation runs as you edit and on Apply, not when the dialog opens.
import { Component } from "@angular/core";
import { AgGridAngular } from "ag-grid-angular";
import {
CalculatedColumnsGridOption,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
DataTypeDefinitions,
GridApi,
GridOptions,
GridReadyEvent,
ModuleRegistry,
NumberEditorModule,
NumberFilterModule,
ValueFormatterLiteParams,
enableDevValidations,
} from "ag-grid-community";
import { CalculatedColumnsModule, ColumnMenuModule } from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
ModuleRegistry.registerModules([
ClientSideRowModelModule,
CalculatedColumnsModule,
ColumnMenuModule,
NumberEditorModule,
NumberFilterModule,
]);
import { SalesRow } from "./interfaces";
@Component({
selector: "my-app",
standalone: true,
imports: [AgGridAngular],
template: `<div style="height: 100%; box-sizing: border-box">
<ag-grid-angular
style="width: 100%; height: 100%;"
[columnDefs]="columnDefs"
[rowData]="rowData"
[dataTypeDefinitions]="dataTypeDefinitions"
[calculatedColumns]="calculatedColumns"
[defaultColDef]="defaultColDef"
/>
</div> `,
})
export class AppComponent {
columnDefs: ColDef[] = [
{ field: "product", flex: 1 },
{
field: "revenue",
editable: true,
cellDataType: "currency",
},
{
field: "cost",
editable: true,
cellDataType: "currency",
},
{
colId: "profit",
headerName: "Profit",
calculatedExpression: "[revenue] - [cost]",
cellDataType: "currency",
sortable: true,
filter: "agNumberColumnFilter",
},
];
rowData: SalesRow[] | null = [
{ product: "Solar panel kit", revenue: 142000, cost: 96000 },
{ product: "Smart thermostat", revenue: 78000, cost: 52000 },
{ product: "Battery pack", revenue: 126000, cost: 101000 },
{ product: "EV charger", revenue: 92000, cost: 61000 },
{ product: "Heat pump", revenue: 168000, cost: 119000 },
{ product: "Inverter unit", revenue: 88000, cost: 57000 },
{ product: "Wind turbine kit", revenue: 232000, cost: 171000 },
{ product: "Solar tile roof", revenue: 198000, cost: 144000 },
{ product: "Power optimiser", revenue: 64000, cost: 41000 },
{ product: "Charge controller", revenue: 53000, cost: 33000 },
{ product: "Energy monitor", revenue: 47000, cost: 29000 },
{ product: "Storage cabinet", revenue: 71000, cost: 52000 },
{ product: "Microinverter", revenue: 59000, cost: 37000 },
{ product: "Heat recovery unit", revenue: 124000, cost: 88000 },
{ product: "Hybrid boiler", revenue: 156000, cost: 117000 },
{ product: "Smart meter", revenue: 39000, cost: 24000 },
{ product: "Insulation pack", revenue: 44000, cost: 27000 },
{ product: "EV cable set", revenue: 31000, cost: 18000 },
{ product: "Solar pump", revenue: 67000, cost: 45000 },
{ product: "Backup generator", revenue: 173000, cost: 131000 },
];
dataTypeDefinitions: DataTypeDefinitions = {
currency: {
baseDataType: "number",
extendsDataType: "number",
valueFormatter: currencyFormatter,
},
};
calculatedColumns: CalculatedColumnsGridOption = {
applyMode: "deferred",
dataTypes: ["currency", "number", "text", "boolean"],
};
defaultColDef: ColDef = {
flex: 1,
minWidth: 130,
};
}
const currencyFormatter = (
params: ValueFormatterLiteParams<SalesRow, number>,
): string => {
const { value } = params;
if (value == null) {
return "";
}
if (String(value).startsWith("#")) {
return String(value);
}
return `$${value.toLocaleString()}`;
};
In the example, edit Profit and change its expression. The cells change only when you click Apply.
Debouncing Live Edits Copy Link
Set applyDebounce to wait for a quiet period before applying live dialog edits. This delays both the cell repaint and calculated-column change events:
<ag-grid-angular
[calculatedColumns]="calculatedColumns"
/* other grid options ... */ />
this.calculatedColumns = {
applyDebounce: 400,
};- The default is
0, which applies on the next animation frame. - Closing the dialog applies any pending edit immediately; removing the column cancels it.
- State and column-definition APIs report the last applied values while an edit is pending.
- This option has no effect in deferred mode.
Customising User-Created Columns Copy Link
Use processColDef to customise calculated columns that users create in the dialog, for example to add a filter or allow aggregation. It receives the column's own definition, as set in the dialog, and returns the definition to use. The returned definition takes precedence over defaultColDef and column types:
<ag-grid-angular
[calculatedColumns]="calculatedColumns"
/* other grid options ... */ />
this.calculatedColumns = {
processColDef: (params) => ({
...params.colDef,
filter: true,
enableValue: params.colDef.cellDataType === 'number',
}),
};Spread params.colDef to preserve the user's settings. Omitted properties use normal column defaults, except calculatedExpression, which retains the user's expression.
In the example, the Profit column is restored from Grid State as if a user had created it, so it has a filter button and can be aggregated from the column menu.
import { Component } from "@angular/core";
import { AgGridAngular } from "ag-grid-angular";
import {
AutoGroupColumnDef,
CalculatedColumnsGridOption,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
GridApi,
GridOptions,
GridReadyEvent,
GridState,
GridStateModule,
ModuleRegistry,
NumberFilterModule,
TextFilterModule,
enableDevValidations,
} from "ag-grid-community";
import {
CalculatedColumnsModule,
ColumnMenuModule,
RowGroupingModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
ModuleRegistry.registerModules([
ClientSideRowModelModule,
GridStateModule,
CalculatedColumnsModule,
ColumnMenuModule,
RowGroupingModule,
NumberFilterModule,
TextFilterModule,
]);
import { SalesRow } from "./interfaces";
@Component({
selector: "my-app",
standalone: true,
imports: [AgGridAngular],
template: `<div style="height: 100%; box-sizing: border-box">
<ag-grid-angular
style="width: 100%; height: 100%;"
[columnDefs]="columnDefs"
[rowData]="rowData"
[calculatedColumns]="calculatedColumns"
[initialState]="initialState"
[defaultColDef]="defaultColDef"
[autoGroupColumnDef]="autoGroupColumnDef"
[groupDefaultExpanded]="groupDefaultExpanded"
/>
</div> `,
})
export class AppComponent {
columnDefs: ColDef[] = [
{ field: "productType", rowGroup: true, hide: true },
{ field: "product", flex: 1.4 },
{ field: "revenue", aggFunc: "sum" },
{ field: "cost", aggFunc: "sum" },
];
rowData: SalesRow[] | null = [
{
productType: "Solar",
product: "Solar panel kit",
revenue: 142000,
cost: 96000,
},
{
productType: "Solar",
product: "Smart thermostat",
revenue: 78000,
cost: 52000,
},
{
productType: "Charging",
product: "Battery pack",
revenue: 126000,
cost: 101000,
},
{
productType: "Charging",
product: "EV charger",
revenue: 92000,
cost: 61000,
},
{
productType: "Heating",
product: "Heat pump",
revenue: 168000,
cost: 119000,
},
{
productType: "Heating",
product: "Hybrid boiler",
revenue: 156000,
cost: 117000,
},
{
productType: "Heating",
product: "Heat recovery unit",
revenue: 124000,
cost: 88000,
},
{
productType: "Storage",
product: "Storage cabinet",
revenue: 71000,
cost: 52000,
},
{
productType: "Storage",
product: "Lithium rack",
revenue: 143000,
cost: 112000,
},
{
productType: "Storage",
product: "Flow battery",
revenue: 187000,
cost: 149000,
},
{
productType: "Wind",
product: "Wind turbine kit",
revenue: 232000,
cost: 171000,
},
{
productType: "Wind",
product: "Micro turbine",
revenue: 76000,
cost: 49000,
},
{
productType: "Monitoring",
product: "Energy monitor",
revenue: 47000,
cost: 29000,
},
{
productType: "Monitoring",
product: "Smart meter",
revenue: 39000,
cost: 24000,
},
{
productType: "Efficiency",
product: "Insulation pack",
revenue: 44000,
cost: 27000,
},
{
productType: "Efficiency",
product: "LED retrofit",
revenue: 36000,
cost: 21000,
},
];
calculatedColumns: CalculatedColumnsGridOption = {
dataTypes: ["number", "text"],
// Applied to every calculated column created in the dialog: add a filter, and allow
// number columns to be aggregated from the column menu.
processColDef: (params) => ({
...params.colDef,
filter: true,
enableValue: params.colDef.cellDataType === "number",
}),
};
initialState: GridState = {
userColumns: [
{
colId: "profit",
created: true,
parentGroupId: null,
properties: [
{ property: "headerName", value: "Profit" },
{ property: "calculatedExpression", value: "[revenue] - [cost]" },
{ property: "cellDataType", value: "number" },
],
},
],
};
defaultColDef: ColDef = {
flex: 1,
minWidth: 130,
};
autoGroupColumnDef: AutoGroupColumnDef = {
headerName: "Product Type",
minWidth: 180,
};
groupDefaultExpanded = -1;
}
Properties available on the CalculatedColumnProcessColDefParams<TData = any, TContext = any> interface.
The column's own definition: the properties set in the Calculated Column dialog, before defaultColDef and column types are merged in. A fresh copy is passed on every call.
|
The column the calculated column was added from, i.e. the one its menu was opened on. null when it was added from no column, or when that column is gone. A column restored from Grid State is not tied to the column it was added from: it gets the last column of its group, or null if it is not in a group.
|
Validation of colDef.calculatedExpression, including syntax, direct column references and saved dialog rejections from getValidationErrors or isColumnReferenceable. Does not rerun application callbacks or check row-specific evaluation errors. In 'live' apply mode, the expression may be incomplete.
|
The grid api. |
Application context as set on gridOptions.context. |
params.expressionValidation includes syntax and reference errors plus saved dialog rejections from getValidationErrors and isColumnReferenceable, matching expressionValidation on the calculatedColumnChanged event. It does not rerun these callbacks or check row-specific evaluation errors.
When the Callback Runs Copy Link
- The callback runs each time the column is built, including when dialog edits are applied and on restore from Grid State.
- It does not run for calculated columns declared in
columnDefs. - The dialog and Grid State keep the user's values, not the callback's.
colId,editable,suppressPaste,field,valueGetter,valueSetter,cellEditorandcellEditorSelectorare always overridden.- Prefer
initial*properties, such asinitialWidth, to stateful ones. - The definitions from
api.getColumnDefs()include the callback's output; see Managing Columns with Column Definitions.
Making Created Columns Read-Only Copy Link
processColDef can return calculatedColumnReadOnly: true to make user-created columns read-only. In live mode, calculatedColumnReadOnly: true locks the column when the initial Add dialog closes, even if the expression is empty or invalid.
To leave blank columns editable, return the full definition with the flag true only when there is an expression: { ...params.colDef, calculatedColumnReadOnly: !!params.colDef.calculatedExpression?.trim() }.
A non-empty expression may still be unfinished; use applyMode: 'deferred' to require Apply before creating and locking the column.
See Read-Only Calculated Columns for what a read-only column allows.