Control what users can change: make calculated columns read-only, hide their menu items, restrict which columns they can reference and validate what users enter.
Read-Only Calculated Columns Copy Link
Set calculatedColumnReadOnly: true to prevent users from changing a column's title, type or expression in the dialog. Users can choose View Calculated Column to inspect its definition. Remove Calculated Column is disabled in the column and cell context menus.
const gridOptions = {
calculatedColumns: true,
columnDefs: [
{ field: 'revenue' },
{ field: 'cost' },
{
colId: 'grossMargin',
calculatedExpression: '([revenue] - [cost]) / [revenue]',
cellDataType: 'number',
calculatedColumnReadOnly: true,
},
],
// other grid options ...
}The example keeps Gross Margin read-only, while Profit and user-created calculated columns remain editable.
import {
ClientSideRowModelModule,
ColDef,
GridOptions,
ModuleRegistry,
createGrid,
enableDevValidations,
} from "ag-grid-community";
import {
CalculatedColumnsModule,
ColumnMenuModule,
ContextMenuModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
ClientSideRowModelModule,
CalculatedColumnsModule,
ColumnMenuModule,
ContextMenuModule,
]);
interface SalesRow {
product: string;
revenue: number;
cost: number;
}
const columnDefs: ColDef<SalesRow>[] = [
{ field: "product" },
{ field: "revenue" },
{ field: "cost" },
{
colId: "grossMargin",
headerName: "Gross Margin",
calculatedExpression: "([revenue] - [cost]) / [revenue]",
cellDataType: "number",
calculatedColumnReadOnly: true,
valueFormatter: ({ value }) =>
typeof value === "number" ? `${(value * 100).toFixed(1)}%` : "",
},
{
colId: "profit",
headerName: "Profit",
calculatedExpression: "[revenue] - [cost]",
cellDataType: "number",
},
];
const gridOptions: GridOptions<SalesRow> = {
calculatedColumns: true,
columnDefs,
defaultColDef: { flex: 1, minWidth: 130 },
rowData: [
{ 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 },
],
};
createGrid(document.querySelector<HTMLElement>("#myGrid")!, gridOptions);
If a column becomes read-only while its edit dialog is open, the dialog closes and any pending edits are discarded. Changes already applied are kept.
This setting restricts UI changes only: updating columnDefs and restoring Grid State can still change or remove the column. Dialog-created columns ignore defaultColDef.calculatedColumnReadOnly, but processColDef can make them read-only.
Hiding Menu Items Copy Link
The calculated column actions are built-in menu items. To stop users adding, editing or removing calculated columns from the menus, filter them out of the default items with getColumnMenuItems and getContextMenuItems:
calculatedColumn: Add Calculated Column, in the column menu.editCalculatedColumn: Edit Calculated Column, or View Calculated Column when the column is read-only, in the column menu.removeCalculatedColumn: Remove Calculated Column, in the column menu and the cell context menu.
const gridOptions = {
calculatedColumns: true,
getColumnMenuItems: ({ defaultItems }) =>
defaultItems.filter(
item => item !== 'editCalculatedColumn' && item !== 'removeCalculatedColumn'
),
getContextMenuItems: ({ defaultItems }) =>
defaultItems?.filter(item => item !== 'removeCalculatedColumn'),
// other grid options ...
}As with read-only columns, this only removes the menu entries. Updating columnDefs and restoring Grid State can still change or remove the column.
Restricting Column References Copy Link
Return false from isColumnReferenceable to exclude a column from the Columns picker, inline autocomplete and newly entered references. Use calculatedColumn to identify the column being edited, or sourceColumn for the column whose menu initiated Add Calculated Column. sourceColumn is only provided when adding a column; it is null when editing an existing column.
This example allows Salary references only when editing Bonus:
const gridOptions = {
calculatedColumns: {
isColumnReferenceable: ({ colDef, calculatedColumn }) => {
if (colDef.field === 'salary') {
return calculatedColumn?.getColId() === 'bonus';
}
return true;
},
},
// other grid options ...
}Properties available on the IsColumnReferenceableParams<TData = any, TContext = any> interface.
The candidate source column. |
The candidate source column's definition. |
The column being edited, including a newly created column in live mode. null when adding in deferred mode. |
The column whose menu initiated Add Calculated Column. Only provided while adding a column, in either apply mode. null when editing an existing column or when there is no source column.
|
The grid api. |
Application context as set on gridOptions.context. |
- The callback runs when the dialog opens.
- Existing permitted expressions can remain unchanged, and programmatic expressions are unrestricted unless explicitly revalidated with
refreshCalculatedColumns(). - Restricted edits disable Apply in deferred mode. In live mode, they are saved but display
#REF!until corrected; reopening the dialog or restoring Grid State preserves the error.
This is a UI restriction, not a security boundary.
import {
ClientSideRowModelModule,
GridOptions,
ModuleRegistry,
createGrid,
enableDevValidations,
} from "ag-grid-community";
import { CalculatedColumnsModule, ColumnMenuModule } from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
ClientSideRowModelModule,
CalculatedColumnsModule,
ColumnMenuModule,
]);
interface Employee {
employee: string;
salary: number;
sales: number;
}
const gridOptions: GridOptions<Employee> = {
columnDefs: [
{ field: "employee" },
{ field: "salary" },
{ field: "sales" },
{
colId: "bonus",
headerName: "Bonus",
calculatedExpression: "[salary] * 0.1",
cellDataType: "number",
},
{
colId: "target",
headerName: "Sales Target",
calculatedExpression: "[sales] * 1.2",
cellDataType: "number",
},
],
rowData: [
{ employee: "Alex Morgan", salary: 50000, sales: 100000 },
{ employee: "Sam Taylor", salary: 60000, sales: 125000 },
{ employee: "Jamie Lee", salary: 55000, sales: 110000 },
],
calculatedColumns: {
isColumnReferenceable: ({ colDef, calculatedColumn }) => {
if (colDef.field === "salary") {
return calculatedColumn?.getColId() === "bonus";
}
return true;
},
},
defaultColDef: { flex: 1, minWidth: 120 },
};
createGrid(document.querySelector<HTMLElement>("#myGrid")!, gridOptions);
In the example, edit Bonus and Sales Target. Only Bonus can reference Salary.
Validating Dialog Fields Copy Link
Use getValidationErrors to validate the dialog's title, type and expression. Return messages keyed by headerName, cellDataType or calculatedExpression, or null to accept the draft:
- Omitted fields and empty arrays are valid.
- Return or spread
internalErrorsto retain built-in feedback. - The callback runs while typing, so keep it quick.
- Return translated messages; the grid shows them as you supply them.
const gridOptions = {
calculatedColumns: {
applyMode: 'deferred',
getValidationErrors: ({ colDef, internalErrors }) => ({
...internalErrors,
headerName: (colDef.headerName?.length ?? 0) > 30
? ['Use a title of at most 30 characters.']
: internalErrors?.headerName,
}),
},
// other grid options ...
}In the example, edit Sales Bonus and enter a title longer than 30 characters or an expression using [Salary] to see field-specific errors.
import {
ClientSideRowModelModule,
GridOptions,
ModuleRegistry,
createGrid,
enableDevValidations,
} from "ag-grid-community";
import { CalculatedColumnsModule, ColumnMenuModule } from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
enableDevValidations();
}
ModuleRegistry.registerModules([
ClientSideRowModelModule,
CalculatedColumnsModule,
ColumnMenuModule,
]);
interface Employee {
employee: string;
salary: number;
sales: number;
}
const gridOptions: GridOptions<Employee> = {
columnDefs: [
{ field: "employee" },
{ field: "salary" },
{ field: "sales" },
{
colId: "bonus",
headerName: "Sales Bonus",
calculatedExpression: "[sales] * 0.1",
cellDataType: "number",
},
],
rowData: [
{ employee: "Alex Morgan", salary: 50000, sales: 100000 },
{ employee: "Sam Taylor", salary: 60000, sales: 125000 },
{ employee: "Jamie Lee", salary: 55000, sales: 110000 },
],
calculatedColumns: {
applyMode: "deferred",
getValidationErrors: ({ colDef, referencedColumns, internalErrors }) => ({
...internalErrors,
headerName:
(colDef.headerName?.length ?? 0) > 30
? ["Use a title of at most 30 characters."]
: internalErrors?.headerName,
calculatedExpression: referencedColumns.some(
(column) => column.getColId() === "salary",
)
? [
"Salary cannot be used in this report.",
"Choose a sales-based expression.",
]
: internalErrors?.calculatedExpression,
}),
},
defaultColDef: { flex: 1, minWidth: 120 },
};
createGrid(document.querySelector<HTMLElement>("#myGrid")!, gridOptions);
Properties available on the CalculatedColumnValidationParams<TData = any, TContext = any> interface.
A copy of the draft definition, with recognised references converted to stored [colId] references. |
The expression displayed in the dialog, using column header references. |
Unique columns directly referenced by the expression, including hidden columns. For invalid syntax, includes recognised complete bracket references.
|
Built-in validation errors, or null. Include these in the callback result to retain built-in validation. |
The calculated column, or null when adding a column in deferred mode. |
The column whose menu initiated Add Calculated Column. Only provided while adding a column, in either apply mode. null when editing an existing column or when there is no source column.
|
The grid api. |
Application context as set on gridOptions.context. |
Editing the title or type revalidates the expression too, even when the expression is unchanged. Opening an editable live dialog rechecks expression rejections, even without edits.
This is a dialog guard, not a security boundary: columnDefs and Grid State restores do not run the callback. Rejections restored from Grid State remain blocked.
How Rejected Edits Behave Copy Link
Any field error blocks Apply in deferred mode. In live mode, the outcome depends on the field:
| Rejected edit | Deferred mode | Live mode |
|---|---|---|
Title or type rejected by getValidationErrors | Blocks Apply | Keeps its last applied value, while other fields apply |
Expression rejected by getValidationErrors | Blocks Apply | Saved, and displays #ERROR!, including when the callback returns built-in expression errors |
Expression referencing a column restricted by isColumnReferenceable | Disables Apply | Saved, and displays #REF! until corrected. Reference restrictions remain enforced, and use #REF! instead of #ERROR!, even when getValidationErrors is set |
Refreshing Calculated Columns Copy Link
Call refreshCalculatedColumns() after application validation rules change to revalidate expressions, update saved errors and recalculate cells and dependants without opening the dialog.
api.refreshCalculatedColumns({ columns: ['profit'] });
// Omit columns to refresh all calculated columns.
api.refreshCalculatedColumns();The refresh explicitly applies the current getValidationErrors and isColumnReferenceable rules:
- It includes read-only and programmatically defined columns.
- Applied expressions are checked with
sourceColumn: null. - Removed rules clear their saved rejections.
- Open dialogs also revalidate their drafts, without applying them or restarting the debounce.
- Validation changes emit
calculatedColumnChangedwithsource: 'api'.
Revalidate committed calculated expressions against the current getValidationErrors and
isColumnReferenceable callbacks, update saved errors and recalculate values and dependants.
Applies to read-only columns too. Does not apply pending dialog edits. Empty expressions are allowed.
Open dialogs revalidate their drafts without restarting pending live updates.
Validation changes raise calculatedColumnChanged with source: 'api'. |