diff --git a/community-modules/locale/src/en-US.ts b/community-modules/locale/src/en-US.ts
index 71078f3eaab..5e303abc051 100644
--- a/community-modules/locale/src/en-US.ts
+++ b/community-modules/locale/src/en-US.ts
@@ -848,6 +848,7 @@ export const AG_GRID_LOCALE_EN = {
calculatedColumnExpressionAmbiguousReference:
'Ambiguous column reference "${variable}". Use the Columns list or a more specific group path.',
calculatedColumnExpressionUnknownReference: 'Unknown column reference "${variable}".',
+ calculatedColumnExpressionEmpty: 'Enter an expression.',
calculatedColumnApply: 'Apply',
calculatedColumnCancel: 'Cancel',
diff --git a/documentation/ag-grid-docs/src/components/reference-documentation/components/InterfaceDocumentation.astro b/documentation/ag-grid-docs/src/components/reference-documentation/components/InterfaceDocumentation.astro
index b308d3eb0f7..55b0b60c8d1 100644
--- a/documentation/ag-grid-docs/src/components/reference-documentation/components/InterfaceDocumentation.astro
+++ b/documentation/ag-grid-docs/src/components/reference-documentation/components/InterfaceDocumentation.astro
@@ -5,6 +5,9 @@ import { getInterfaceDocumentationModel } from '../utils/getInterfaceDocumentati
import { getOverrides } from '../utils/getOverrides';
import { getJsonFile } from '@utils/pages';
import CodeShiki from '@ag-website-shared/components/code/CodeShiki';
+import { codeToHtml } from 'shiki';
+import agDocsTheme from '@ag-website-shared/components/code/theme.json';
+import { extractDecorations } from '@ag-website-shared/components/code/keepMarkup';
const { interfaceName, overrideSrc, names, exclude, config } = Astro.props;
@@ -23,11 +26,21 @@ const model = getInterfaceDocumentationModel({
codeLookup,
interfaceLookup,
});
+
+let preHighlightedHtml: string | undefined;
+if (model.type === 'code') {
+ const { cleanCode, decorations } = extractDecorations(model.code.trimEnd());
+ preHighlightedHtml = await codeToHtml(cleanCode, {
+ lang: 'typescript',
+ theme: agDocsTheme,
+ decorations,
+ } as any);
+}
---
{
model.type === 'code' ? (
-
+
) : (
= {
columnDefs,
rowData,
calculatedColumns: {
- columnHighlighting: true,
+ suppressColumnHighlighting: true,
},
defaultColDef: {
flex: 1,
diff --git a/documentation/ag-grid-docs/src/content/docs/calculated-columns/_examples/calculated-columns-row-groups/main.ts b/documentation/ag-grid-docs/src/content/docs/calculated-columns/_examples/calculated-columns-row-groups/main.ts
index 9b2115b7bdd..c096ff53e2e 100644
--- a/documentation/ag-grid-docs/src/content/docs/calculated-columns/_examples/calculated-columns-row-groups/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/calculated-columns/_examples/calculated-columns-row-groups/main.ts
@@ -6,11 +6,12 @@ import {
ValidationModule,
createGrid,
} from 'ag-grid-community';
-import { CalculatedColumnsModule, RowGroupingModule } from 'ag-grid-enterprise';
+import { CalculatedColumnsModule, ColumnMenuModule, RowGroupingModule } from 'ag-grid-enterprise';
ModuleRegistry.registerModules([
ClientSideRowModelModule,
CalculatedColumnsModule,
+ ColumnMenuModule,
RowGroupingModule,
NumberFilterModule,
...(process.env.NODE_ENV !== 'production' ? [ValidationModule] : []),
diff --git a/documentation/ag-grid-docs/src/content/docs/calculated-columns/_examples/calculated-columns/main.ts b/documentation/ag-grid-docs/src/content/docs/calculated-columns/_examples/calculated-columns/main.ts
index 88ae57e7792..d6b828f926f 100644
--- a/documentation/ag-grid-docs/src/content/docs/calculated-columns/_examples/calculated-columns/main.ts
+++ b/documentation/ag-grid-docs/src/content/docs/calculated-columns/_examples/calculated-columns/main.ts
@@ -7,11 +7,12 @@ import {
ValidationModule,
createGrid,
} from 'ag-grid-community';
-import { CalculatedColumnsModule } from 'ag-grid-enterprise';
+import { CalculatedColumnsModule, ColumnMenuModule } from 'ag-grid-enterprise';
ModuleRegistry.registerModules([
ClientSideRowModelModule,
CalculatedColumnsModule,
+ ColumnMenuModule,
NumberEditorModule,
NumberFilterModule,
...(process.env.NODE_ENV !== 'production' ? [ValidationModule] : []),
diff --git a/documentation/ag-grid-docs/src/content/docs/calculated-columns/index.mdoc b/documentation/ag-grid-docs/src/content/docs/calculated-columns/index.mdoc
index 8a0feb24bb9..e5be609b040 100644
--- a/documentation/ag-grid-docs/src/content/docs/calculated-columns/index.mdoc
+++ b/documentation/ag-grid-docs/src/content/docs/calculated-columns/index.mdoc
@@ -7,7 +7,7 @@ Calculated Columns let you add read-only values to the grid without storing thos
## Enabling Calculated Columns
-Calculated columns are enabled by registering `CalculatedColumnsModule` and setting `calculatedExpression` on a column definition. In source code and Grid API calls, bracket references such as `[revenue]` resolve to the same-row value from the column with that `colId`.
+Calculated columns are enabled by registering `CalculatedColumnsModule` and setting `calculatedExpression` on a column definition. In source code, bracket references such as `[revenue]` resolve to the same-row value from the column with that `colId`.
```{% frameworkTransform=true %}
const gridOptions = {
@@ -110,23 +110,23 @@ This only controls the expression picker buttons. Use an empty array or `null` t
{% gridExampleRunner title="Dialog Expression Pickers" name="calculated-columns-dialog-helper-lists" exampleHeight=350 /%}
-### columnHighlighting
+### suppressColumnHighlighting
-Use `columnHighlighting` to highlight the calculated column while its edit dialog is open:
+Calculated columns are highlighted by default while their edit dialog is open. Use `suppressColumnHighlighting` to disable this highlight:
```{% frameworkTransform=true %}
const gridOptions = {
theme: myTheme,
calculatedColumns: {
- columnHighlighting: true,
+ suppressColumnHighlighting: true,
},
};
```
-When enabled, the highlighted header and cells highlight colour can also be customised with the `--ag-calculated-column-highlight-color` CSS variable.
+When highlighting is not suppressed, the header and cells highlight colour can be customised with the `--ag-calculated-column-highlight-color` CSS variable.
-{% gridExampleRunner title="Dialog Column Highlighting" name="calculated-columns-dialog-highlighting" exampleHeight=350 /%}
+{% gridExampleRunner title="Suppress Dialog Column Highlighting" name="calculated-columns-dialog-highlighting" exampleHeight=350 /%}
## Advanced Calculated Columns
@@ -161,32 +161,40 @@ const gridOptions = {
Header cells and grid cells for calculated columns include the `ag-calculated-column` CSS class, which can be used for custom styling.
-Calculated columns can also be managed from the Grid API. API-created columns are appended to the current column tree; use the column moving API afterwards if you need a specific display position. The original application-provided `columnDefs` are not mutated, but `api.getColumnDefs()` includes dynamic calculated columns for persistence.
+Calculated columns are managed through column definitions, like other columns. To add, update or remove a calculated column from application code, update the `columnDefs` grid option.
-A column added from the header menu is placed after the column it was created from, where that anchor is a leaf column. When the anchor is a column-group header or the auto-group column, or when the anchor column is later removed, the calculated column falls back to the end of the column tree.
-
-Replacing the grid's columns with `setColumnDefs` (or `updateGridOptions({ columnDefs })`) is a full reset: it clears calculated columns added through the API or the dialog. To keep user-created calculated columns across a replacement, read the current definitions from `api.getColumnDefs()` and include them in the new array.
+A column added from the header menu is placed after the column it was created from. The dialog manages calculated columns inside the grid without mutating the `columnDefs` array supplied by the application. To persist user-created calculated columns, read the complete current definitions from `api.getColumnDefs()`.
```{% frameworkTransform=true %}
const calculatedColumns = api
.getColumns()
.filter(column => column.getColDef().calculatedExpression != null);
-api.addCalculatedColumn({
- colId: 'profitMargin',
- calculatedExpression: '([revenue] - [cost]) / [revenue]',
- cellDataType: 'number',
- valueFormatter: params => `${(params.value * 100).toFixed(1)}%`,
-});
+api.setGridOption('columnDefs', [
+ ...(api.getColumnDefs() ?? []),
+ {
+ colId: 'profitMargin',
+ calculatedExpression: '([revenue] - [cost]) / [revenue]',
+ cellDataType: 'number',
+ valueFormatter: params => `${(params.value * 100).toFixed(1)}%`,
+ },
+]);
-api.updateCalculatedColumn('profitMargin', {
- calculatedExpression: '([revenue] - [cost]) / [revenue]',
- valueFormatter: params => `${(params.value * 100).toFixed(2)}%`,
-});
+api.setGridOption(
+ 'columnDefs',
+ (api.getColumnDefs() ?? []).map(colDef =>
+ colDef.colId === 'profitMargin'
+ ? { ...colDef, calculatedExpression: '([revenue] - [cost]) / [revenue]' }
+ : colDef
+ )
+);
api.openCalculatedColumnDialog('profitMargin');
-api.removeCalculatedColumn('profitMargin');
+api.setGridOption(
+ 'columnDefs',
+ (api.getColumnDefs() ?? []).filter(colDef => colDef.colId !== 'profitMargin')
+);
```
Calculated Column events also use stored `colId` references in their expression payloads. For example, an event raised from the dialog still reports `[revenue]`, not `[Revenue]`.
@@ -199,7 +207,7 @@ Calculated Column events also use stored `colId` references in their expression
### Grid API
-{% apiDocumentation source="grid-api/api.json" section="calculatedColumns" names=["addCalculatedColumn", "updateCalculatedColumn", "removeCalculatedColumn", "openCalculatedColumnDialog"] /%}
+{% apiDocumentation source="grid-api/api.json" section="calculatedColumns" names=["openCalculatedColumnDialog"] /%}
### Grid Options
diff --git a/packages/ag-grid-community/src/api/gridApi.ts b/packages/ag-grid-community/src/api/gridApi.ts
index f236e60d2a5..c78f6052554 100644
--- a/packages/ag-grid-community/src/api/gridApi.ts
+++ b/packages/ag-grid-community/src/api/gridApi.ts
@@ -38,7 +38,6 @@ import type {
} from '../interfaces/autoSize';
import type { CsvExportParams } from '../interfaces/exportParams';
import type { GridState, GridStateKey } from '../interfaces/gridState';
-import type { CalculatedColumnDef, CalculatedColumnUpdate } from '../interfaces/iCalculatedColumns';
import type { RenderedRowEvent } from '../interfaces/iCallbackParams';
import type {
EditingCellPosition,
@@ -1780,31 +1779,6 @@ export interface _FormulaGridApi {
/** @internal AG_GRID_INTERNAL - Not for public use. Can change / be removed at any time. */
export interface _CalculatedColumnsGridApi {
- /**
- * Add a new calculated column to the end of the column definitions.
- * The `calculatedExpression` should reference other columns by `colId`, e.g. `[revenue] - [cost]`.
- * @agModule `CalculatedColumnsModule`
- */
- addCalculatedColumn(colDef: CalculatedColumnDef): void;
-
- /**
- * Update an existing calculated column.
- * The `calculatedExpression` should reference other columns by `colId`, e.g. `[revenue] - [cost]`.
- * No-op if the supplied column key does not resolve to a calculated column.
- * @agModule `CalculatedColumnsModule`
- */
- updateCalculatedColumn(
- column: ColKey,
- colDef: CalculatedColumnUpdate
- ): void;
-
- /**
- * Remove an existing calculated column.
- * No-op if the supplied column key does not resolve to a calculated column.
- * @agModule `CalculatedColumnsModule`
- */
- removeCalculatedColumn(column: ColKey): void;
-
/**
* Open the Calculated Column dialog for an existing calculated column.
* No-op if the supplied column key does not resolve to a calculated column.
diff --git a/packages/ag-grid-community/src/api/gridApiFunctions.ts b/packages/ag-grid-community/src/api/gridApiFunctions.ts
index 584a4c3db4a..80f9bf9b680 100644
--- a/packages/ag-grid-community/src/api/gridApiFunctions.ts
+++ b/packages/ag-grid-community/src/api/gridApiFunctions.ts
@@ -379,9 +379,6 @@ export const gridApiFunctionsMap: Record =
}),
...mod<_CalculatedColumnsGridApi>('CalculatedColumns', {
- addCalculatedColumn: 0,
- updateCalculatedColumn: 0,
- removeCalculatedColumn: 0,
openCalculatedColumnDialog: 0,
}),
diff --git a/packages/ag-grid-community/src/columns/columnStateUtils.ts b/packages/ag-grid-community/src/columns/columnStateUtils.ts
index 7574667ca6e..2277d810ec0 100644
--- a/packages/ag-grid-community/src/columns/columnStateUtils.ts
+++ b/packages/ag-grid-community/src/columns/columnStateUtils.ts
@@ -106,7 +106,7 @@ export function _applyColumnState(
colIds[i] = state[i].colId;
}
if (calculatedColsSvc?.restoreDynamicColumnDefs(colIds)) {
- colModel.refreshDynamicColumns(source);
+ calculatedColsSvc.refreshProjectedColumns(source);
}
}
@@ -301,7 +301,7 @@ export function _resetColumnState(beans: BeanCollection, source: ColumnEventType
const { colModel, autoColSvc, selectionColSvc, eventSvc, gos, calculatedColsSvc } = beans;
if (calculatedColsSvc?.resetDynamicColumnDefs(true)) {
- colModel.refreshDynamicColumns(source);
+ calculatedColsSvc.refreshProjectedColumns(source);
}
const primaryCols = colModel.getColDefCols();
diff --git a/packages/ag-grid-community/src/interfaces/iCalculatedColumns.ts b/packages/ag-grid-community/src/interfaces/iCalculatedColumns.ts
index b975757ca42..515776a7d70 100644
--- a/packages/ag-grid-community/src/interfaces/iCalculatedColumns.ts
+++ b/packages/ag-grid-community/src/interfaces/iCalculatedColumns.ts
@@ -1,6 +1,7 @@
import type { Bean } from '../context/bean';
import type { AgColumn } from '../entities/agColumn';
-import type { ColDef, ColGroupDef, ColKey } from '../entities/colDef';
+import type { ColDef, ColGroupDef } from '../entities/colDef';
+import type { ColumnEventType } from '../events';
export type CalculatedColumnExpressionPicker = 'columns' | 'functions' | 'operators';
@@ -17,10 +18,10 @@ export interface CalculatedColumnsOptions {
*/
expressionPickers?: CalculatedColumnExpressionPicker[] | null;
/**
- * Highlight the calculated column currently being edited by the dialog.
+ * Suppress highlighting the calculated column currently being edited by the dialog.
* @default false
*/
- columnHighlighting?: boolean;
+ suppressColumnHighlighting?: boolean;
}
export type CalculatedColumnDef = ColDef & {
@@ -33,14 +34,13 @@ export type CalculatedColumnUpdate = Partial Validations = () => {
return 'calculatedColumns should be an object.';
}
- const { dataTypes, expressionPickers, columnHighlighting } = calculatedColumns;
+ const { dataTypes, expressionPickers, suppressColumnHighlighting } = calculatedColumns;
if (dataTypes != null) {
if (!Array.isArray(dataTypes) || dataTypes.some((dataType) => typeof dataType !== 'string')) {
return 'calculatedColumns.dataTypes should be an array of strings.';
@@ -201,8 +201,8 @@ const GRID_OPTION_VALIDATIONS: () => Validations = () => {
return "calculatedColumns.expressionPickers should contain only 'columns', 'functions' or 'operators'.";
}
}
- if (columnHighlighting != null && typeof columnHighlighting !== 'boolean') {
- return 'calculatedColumns.columnHighlighting should be a boolean.';
+ if (suppressColumnHighlighting != null && typeof suppressColumnHighlighting !== 'boolean') {
+ return 'calculatedColumns.suppressColumnHighlighting should be a boolean.';
}
return null;
diff --git a/packages/ag-grid-enterprise/src/calculatedColumns/calculatedColumnsApi.ts b/packages/ag-grid-enterprise/src/calculatedColumns/calculatedColumnsApi.ts
index 50160f33505..519b37905fe 100644
--- a/packages/ag-grid-enterprise/src/calculatedColumns/calculatedColumnsApi.ts
+++ b/packages/ag-grid-enterprise/src/calculatedColumns/calculatedColumnsApi.ts
@@ -1,17 +1,5 @@
-import type { BeanCollection, CalculatedColumnDef, CalculatedColumnUpdate, ColKey } from 'ag-grid-community';
-
-export function addCalculatedColumn(beans: BeanCollection, colDef: CalculatedColumnDef): void {
- beans.calculatedColsSvc?.addCalculatedColumn(colDef);
-}
-
-export function updateCalculatedColumn(beans: BeanCollection, column: ColKey, colDef: CalculatedColumnUpdate): void {
- beans.calculatedColsSvc?.updateCalculatedColumn(column, colDef);
-}
-
-export function removeCalculatedColumn(beans: BeanCollection, column: ColKey): void {
- beans.calculatedColsSvc?.removeCalculatedColumn(beans.colModel.getColDefColOrCol(column));
-}
+import type { BeanCollection, ColKey } from 'ag-grid-community';
export function openCalculatedColumnDialog(beans: BeanCollection, column: ColKey): void {
- beans.calculatedColsSvc?.openCalculatedColumnDialog(beans.colModel.getColDefColOrCol(column), 'edit');
+ beans.calculatedColsSvc?.openCalculatedColumnDialog(beans.colModel.getColDefColOrCol(column), 'edit', false);
}
diff --git a/packages/ag-grid-enterprise/src/calculatedColumns/calculatedColumnsModule.ts b/packages/ag-grid-enterprise/src/calculatedColumns/calculatedColumnsModule.ts
index 86515060eb3..99866ee166b 100644
--- a/packages/ag-grid-enterprise/src/calculatedColumns/calculatedColumnsModule.ts
+++ b/packages/ag-grid-enterprise/src/calculatedColumns/calculatedColumnsModule.ts
@@ -4,12 +4,7 @@ import { ColumnApiModule, TooltipModule, _PopupModule } from 'ag-grid-community'
import { FormulaModule } from '../formula/formulaModule';
import { VERSION } from '../version';
import calculatedColumnsCSS from './calculatedColumns.css';
-import {
- addCalculatedColumn,
- openCalculatedColumnDialog,
- removeCalculatedColumn,
- updateCalculatedColumn,
-} from './calculatedColumnsApi';
+import { openCalculatedColumnDialog } from './calculatedColumnsApi';
import { CalculatedColumnsService } from './calculatedColumnsService';
/**
@@ -20,9 +15,6 @@ export const CalculatedColumnsModule: _ModuleWithApi<_CalculatedColumnsGridApi();
// guards the first validation pass so we don't emit spurious change events before the baseline exists.
private validationStatesInitialised = false;
- // re-entry counter: when > 0, projection-triggered refreshes skip validation checks.
+ // guards the first lifecycle pass so static columnDefs do not emit spurious created events.
+ private lifecycleInitialised = false;
+ private knownCalculatedColumns = new Map();
+ // re-entry counter: when > 0, projection-triggered refreshes skip validation/lifecycle checks.
private suppressValidationChecks = 0;
private readonly openDialogsByColId = new Map();
public postConstruct(): void {
this.addManagedEventListeners({
- newColumnsLoaded: (event) => this.checkValidationStates(event.source),
+ newColumnsLoaded: (event) => {
+ this.checkColumnLifecycle(event.source);
+ this.checkValidationStates(event.source);
+ },
gridColumnsChanged: () => this.refreshCalculatedColumnSpans(),
columnMoved: (event) => this.releaseVisibleAnchors(event.columns),
});
@@ -135,7 +145,7 @@ export class CalculatedColumnsService extends BeanStub implements NamedBean, ICa
public isHighlightedColumn(column: AgColumn | null): boolean {
return (
column != null &&
- this.gos.get('calculatedColumns')?.columnHighlighting === true &&
+ this.gos.get('calculatedColumns')?.suppressColumnHighlighting !== true &&
this.openDialogsByColId.get(column.colId)?.highlight === true
);
}
@@ -175,39 +185,7 @@ export class CalculatedColumnsService extends BeanStub implements NamedBean, ICa
}
}
- public addCalculatedColumn(colDef: CalculatedColumnDef, source: 'api' | 'calculatedColumn' = 'api'): void {
- if (!_isStringLargerThan(colDef.calculatedExpression, 0, true)) {
- _warnOnce('addCalculatedColumn: calculatedExpression is required and cannot be empty.');
- return;
- }
- if (
- !this.validateColumnReferences(colDef.calculatedExpression) ||
- !this.validateFormulaExpression(colDef.calculatedExpression)
- ) {
- return;
- }
-
- const colId = colDef.colId ?? this.createUniqueColId();
- const nextColDef = this.toCalculatedColDef({ ...colDef, colId });
- this.removeInactiveDynamicColumn(colId);
- this.dynamicColumns.push({ colId, colDef: nextColDef });
- this.refreshDynamicColumns(source);
-
- const column = this.beans.colModel.getColById(colId);
- if (column) {
- this.dispatchCreatedOrRemovedEvent(
- 'calculatedColumnCreated',
- this.getEventCommonParams(column, colDef.calculatedExpression, source)
- );
- }
- this.checkValidationStates(source, true);
- }
-
- public updateCalculatedColumn(
- column: ColKey,
- colDef: CalculatedColumnUpdate,
- source: 'api' | 'calculatedColumn' = 'api'
- ): void {
+ private updateCalculatedColumn(column: ColKey, colDef: CalculatedColumnUpdate): void {
const targetColumn = this.beans.colModel.getColDefColOrCol(column);
if (targetColumn?.colDef.calculatedExpression == null) {
return;
@@ -239,16 +217,16 @@ export class CalculatedColumnsService extends BeanStub implements NamedBean, ICa
});
this.dynamicSuppressions.delete(targetColId);
}
- this.refreshDynamicColumns(source);
+ this.refreshDynamicColumns('calculatedColumn');
const nextColumn = this.beans.colModel.getColById(targetColId) ?? targetColumn;
const newExpression = nextColumn.colDef.calculatedExpression ?? oldExpression;
if (colDef.calculatedExpression !== undefined && oldExpression !== newExpression) {
this.dispatchExpressionChangedEvent(
- this.getEventCommonParams(nextColumn, newExpression, source),
+ this.getEventCommonParams(nextColumn, newExpression, 'calculatedColumn'),
oldExpression
);
}
- this.checkValidationStates(source, true);
+ this.checkValidationStates('calculatedColumn', true);
this.refreshCalculatedColumn(targetColId);
}
@@ -294,42 +272,47 @@ export class CalculatedColumnsService extends BeanStub implements NamedBean, ICa
return true;
}
- public openCalculatedColumnDialog(column: AgColumn | null, mode: 'add' | 'edit'): void {
+ public openCalculatedColumnDialog(column: AgColumn | null, mode: 'add' | 'edit', focusDialog = true): void {
if (mode === 'add') {
const colId = this.createUniqueColId();
const headerName = this.getLocaleTextFunc()('calculatedColumnDefaultTitle', 'New title');
const draft: CalculatedColumnDraft = { colId, headerName, ...this.getDefaultDraft() };
- this.showDialog(draft, (nextDraft) => {
- const isDynamicAnchor = column != null && this.getDynamicColumn(column.colId) != null;
- const anchorColDef = isDynamicAnchor ? undefined : column?.getUserProvidedColDef();
- const nextColDef = this.toColDef(nextDraft);
- const columnGroupShow = column?.colDef.columnGroupShow;
-
- if (columnGroupShow != null) {
- nextColDef.columnGroupShow = columnGroupShow;
- }
-
- const shouldUseColumnAsAnchor =
- anchorColDef == null || isDynamicAnchor || this.gos.get('maintainColumnOrder');
- this.removeInactiveDynamicColumn(nextDraft.colId);
- this.dynamicColumns.push({
- colId: nextDraft.colId,
- colDef: nextColDef,
- anchorColId: column?.colId,
- anchorColDef,
- visibleAnchorColId: shouldUseColumnAsAnchor ? column?.colId : undefined,
- });
- this.refreshDynamicColumns('calculatedColumn');
- this.focusCalculatedColumn(nextDraft.colId);
- const newColumn = this.beans.colModel.getColById(nextDraft.colId);
- if (newColumn) {
- this.dispatchCreatedOrRemovedEvent(
- 'calculatedColumnCreated',
- this.getEventCommonParams(newColumn, nextDraft.calculatedExpression, 'calculatedColumn')
- );
- }
- this.checkValidationStates('calculatedColumn', true);
- });
+ this.showDialog(
+ draft,
+ (nextDraft) => {
+ const isDynamicAnchor = column != null && this.getDynamicColumn(column.colId) != null;
+ const anchorColDef = isDynamicAnchor ? undefined : column?.getUserProvidedColDef();
+ const nextColDef = this.toColDef(nextDraft);
+ const columnGroupShow = column?.colDef.columnGroupShow;
+
+ if (columnGroupShow != null) {
+ nextColDef.columnGroupShow = columnGroupShow;
+ }
+
+ const shouldUseColumnAsAnchor =
+ anchorColDef == null || isDynamicAnchor || this.gos.get('maintainColumnOrder');
+ this.removeInactiveDynamicColumn(nextDraft.colId);
+ this.dynamicColumns.push({
+ colId: nextDraft.colId,
+ colDef: nextColDef,
+ anchorColId: column?.colId,
+ anchorColDef,
+ visibleAnchorColId: shouldUseColumnAsAnchor ? column?.colId : undefined,
+ });
+ this.refreshDynamicColumns('calculatedColumn');
+ this.focusCalculatedColumn(nextDraft.colId);
+ const newColumn = this.beans.colModel.getColById(nextDraft.colId);
+ if (newColumn) {
+ this.dispatchCreatedOrRemovedEvent(
+ 'calculatedColumnCreated',
+ this.getEventCommonParams(newColumn, nextDraft.calculatedExpression, 'calculatedColumn')
+ );
+ }
+ this.checkValidationStates('calculatedColumn', true);
+ },
+ undefined,
+ focusDialog
+ );
return;
}
@@ -341,13 +324,14 @@ export class CalculatedColumnsService extends BeanStub implements NamedBean, ICa
draft,
(nextDraft) => {
const { colId: _, ...update } = this.toColDef(nextDraft);
- this.updateCalculatedColumn(column.colId, update, 'calculatedColumn');
+ this.updateCalculatedColumn(column.colId, update);
},
- column
+ column,
+ focusDialog
);
}
- public removeCalculatedColumn(column: AgColumn | null, source: 'api' | 'calculatedColumn' = 'api'): void {
+ public removeCalculatedColumn(column: AgColumn | null): void {
if (column?.colDef.calculatedExpression == null) {
return;
}
@@ -364,12 +348,12 @@ export class CalculatedColumnsService extends BeanStub implements NamedBean, ICa
targetColDef: column.getUserProvidedColDef(),
});
}
- this.refreshDynamicColumns(source);
+ this.refreshDynamicColumns('calculatedColumn');
this.dispatchCreatedOrRemovedEvent(
'calculatedColumnRemoved',
- this.getEventCommonParams(column, expression, source)
+ this.getEventCommonParams(column, expression, 'calculatedColumn')
);
- this.checkValidationStates(source, true);
+ this.checkValidationStates('calculatedColumn', true);
}
public createProjectedColumnDefs(
@@ -481,16 +465,22 @@ export class CalculatedColumnsService extends BeanStub implements NamedBean, ICa
private refreshDynamicColumns(source: ColumnEventType): void {
const columnGroupState = this.beans.colGroupSvc?.getColumnGroupState();
+ this.refreshProjectedColumns(source);
+
+ if (columnGroupState?.length) {
+ this.beans.colGroupSvc?.setColumnGroupState(columnGroupState, source);
+ }
+ }
+
+ // re-runs the column projection with lifecycle/validation checks suppressed, so column-state
+ // operations (reset/restore of dynamic calc cols) do not emit spurious created/removed events.
+ public refreshProjectedColumns(source: ColumnEventType): void {
this.suppressValidationChecks++;
try {
this.beans.colModel.refreshDynamicColumns(source);
} finally {
this.suppressValidationChecks--;
}
-
- if (columnGroupState?.length) {
- this.beans.colGroupSvc?.setColumnGroupState(columnGroupState, source);
- }
}
private createUniqueColId(): string {
@@ -683,11 +673,14 @@ export class CalculatedColumnsService extends BeanStub implements NamedBean, ICa
private showDialog(
draft: CalculatedColumnDraft,
onApply: (draft: CalculatedColumnDraft) => void,
- columnToHighlight?: AgColumn | null
+ columnToHighlight?: AgColumn | null,
+ focusDialog = true
): void {
const openDialogState = this.openDialogsByColId.get(draft.colId);
if (openDialogState) {
- openDialogState.dialog.getGui().focus({ preventScroll: true });
+ if (focusDialog) {
+ openDialogState.dialog.getGui().focus({ preventScroll: true });
+ }
return;
}
@@ -701,6 +694,14 @@ export class CalculatedColumnsService extends BeanStub implements NamedBean, ICa
const getValidatedExpression = (
nextDraft: CalculatedColumnDraft
): { valid: true; expression: string } | { valid: false; error: string } => {
+ // An empty expression is "incomplete", not a malformed formula — surface a calc-column
+ // message rather than the formula parser's "Formulas must begin with =." error.
+ if (!_isStringLargerThan(nextDraft.calculatedExpression, 0, true)) {
+ return {
+ valid: false,
+ error: this.getLocaleTextFunc()('calculatedColumnExpressionEmpty', 'Enter an expression.'),
+ };
+ }
const result = mapper.toInternalExpression(nextDraft.calculatedExpression);
if ('error' in result) {
return {
@@ -767,6 +768,9 @@ export class CalculatedColumnsService extends BeanStub implements NamedBean, ICa
state.close = () => dialog.close();
this.openDialogsByColId.set(draft.colId, { dialog, highlight: columnToHighlight != null });
this.refreshCalculatedColumnHighlight(columnToHighlight ?? null);
+ if (focusDialog) {
+ dialog.getGui().focus({ preventScroll: true });
+ }
const destroyDialogMouseListeners = this.addManagedElementListeners(dialog.getGui(), {
mousedown: () => form.hideSuggestions(),
});
@@ -860,7 +864,7 @@ export class CalculatedColumnsService extends BeanStub implements NamedBean, ICa
};
}
- private toCalculatedColDef(colDef: CalculatedColumnDef | ColDef): ColDef {
+ private toCalculatedColDef(colDef: ColDef): ColDef {
// strip fields that conflict with calculatedExpression invariants (see colDefValidations.ts).
const sanitised: ColDef = { ...colDef };
const invariantProperties: (keyof ColDef)[] = [
@@ -886,6 +890,53 @@ export class CalculatedColumnsService extends BeanStub implements NamedBean, ICa
return this.getFormulaExpressionError(expression) == null ? 'valid' : 'invalidExpression';
}
+ private checkColumnLifecycle(source: ColumnEventType): void {
+ const previousColumns = this.knownCalculatedColumns;
+ const nextColumns = new Map();
+ const shouldDispatch = this.lifecycleInitialised && this.suppressValidationChecks === 0;
+
+ for (const column of this.beans.colModel.getCols() ?? []) {
+ const expression = column.colDef.calculatedExpression;
+ if (expression == null) {
+ continue;
+ }
+
+ const colId = column.colId;
+ nextColumns.set(colId, { column, expression });
+
+ if (!shouldDispatch) {
+ continue;
+ }
+
+ const previousColumn = previousColumns.get(colId);
+ if (previousColumn == null) {
+ this.dispatchCreatedOrRemovedEvent(
+ 'calculatedColumnCreated',
+ this.getEventCommonParams(column, expression, source)
+ );
+ } else if (previousColumn.expression !== expression) {
+ this.dispatchExpressionChangedEvent(
+ this.getEventCommonParams(column, expression, source),
+ previousColumn.expression
+ );
+ }
+ }
+
+ if (shouldDispatch) {
+ for (const [colId, previousColumn] of previousColumns) {
+ if (!nextColumns.has(colId)) {
+ this.dispatchCreatedOrRemovedEvent(
+ 'calculatedColumnRemoved',
+ this.getEventCommonParams(previousColumn.column, previousColumn.expression, source)
+ );
+ }
+ }
+ }
+
+ this.knownCalculatedColumns = nextColumns;
+ this.lifecycleInitialised = true;
+ }
+
private checkValidationStates(source: ColumnEventType, forceDispatch = false): void {
if (this.suppressValidationChecks > 0) {
return;
@@ -906,7 +957,10 @@ export class CalculatedColumnsService extends BeanStub implements NamedBean, ICa
nextStates.set(colId, state);
const previousState = previousStates.get(colId);
- if (shouldDispatch && previousState !== undefined && previousState !== state) {
+ const hasPreviousState = previousState !== undefined;
+ const stateChanged =
+ (hasPreviousState && previousState !== state) || (!hasPreviousState && state !== 'valid');
+ if (shouldDispatch && stateChanged) {
const valid = state === 'valid';
this.dispatchValidationStateChangedEvent(
this.getEventCommonParams(column, expression, source),
diff --git a/packages/ag-grid-enterprise/src/menu/menuItemMapper.ts b/packages/ag-grid-enterprise/src/menu/menuItemMapper.ts
index 0f72f0542dc..d5878f4da73 100644
--- a/packages/ag-grid-enterprise/src/menu/menuItemMapper.ts
+++ b/packages/ag-grid-enterprise/src/menu/menuItemMapper.ts
@@ -500,7 +500,7 @@ export class MenuItemMapper extends BeanStub implements NamedBean {
? {
name: localeTextFunc('calculatedColumnRemove', 'Remove Calculated Column'),
icon: _createIconNoSpan('calculatedColumnRemove', beans, null),
- action: () => calculatedColsSvc.removeCalculatedColumn(column, 'calculatedColumn'),
+ action: () => calculatedColsSvc.removeCalculatedColumn(column),
}
: null;
case 'sortUnSort':
diff --git a/testing/behavioural/src/columns/row-span.test.ts b/testing/behavioural/src/columns/row-span.test.ts
index 3f2220d7b96..d3957adea1b 100644
--- a/testing/behavioural/src/columns/row-span.test.ts
+++ b/testing/behavioural/src/columns/row-span.test.ts
@@ -1,4 +1,4 @@
-import type { GridApi, GridOptions, IRowNode } from 'ag-grid-community';
+import type { ColDef, ColGroupDef, GridApi, GridOptions, IRowNode } from 'ag-grid-community';
import {
CellSpanModule,
ClientSideRowModelModule,
@@ -48,6 +48,31 @@ describe('row spanning', () => {
return gridsManager.createGrid('myGrid', { enableCellSpan: true, ...options });
}
+ function addCalculatedColumnDef(api: GridApi, colDef: ColDef): void {
+ api.setGridOption('columnDefs', [...(api.getColumnDefs() ?? []), colDef]);
+ }
+
+ function updateCalculatedColumnDef(api: GridApi, colId: string, colDefUpdate: ColDef): void {
+ api.setGridOption('columnDefs', updateColumnDef(api.getColumnDefs() ?? [], colId, colDefUpdate));
+ }
+
+ function updateColumnDef(
+ columnDefs: (ColDef | ColGroupDef)[],
+ colId: string,
+ colDefUpdate: ColDef
+ ): (ColDef | ColGroupDef)[] {
+ const nextColumnDefs: (ColDef | ColGroupDef)[] = [];
+ for (let i = 0, len = columnDefs.length; i < len; ++i) {
+ const colDef = columnDefs[i];
+ if ('children' in colDef) {
+ nextColumnDefs.push({ ...colDef, children: updateColumnDef(colDef.children, colId, colDefUpdate) });
+ } else {
+ nextColumnDefs.push((colDef.colId ?? colDef.field) === colId ? { ...colDef, ...colDefUpdate } : colDef);
+ }
+ }
+ return nextColumnDefs;
+ }
+
// ── Basic / documented behaviour ────────────────────────────────────────────────────────────
describe('basic', () => {
@@ -633,7 +658,7 @@ describe('row spanning', () => {
└── LEAF id:2 athlete:"B"
`);
- api.addCalculatedColumn({ colId: 'copy', calculatedExpression: '[athlete]' });
+ addCalculatedColumnDef(api, { colId: 'copy', calculatedExpression: '[athlete]' });
await settle();
// regression guard: 'copy' must span 0-1 (by evaluated value), not 0-2.
await new GridColumns(api, 'calc-add columns after').checkColumns(`
@@ -661,7 +686,7 @@ describe('row spanning', () => {
{ a: 'Y', b: 'Q' },
],
});
- api.addCalculatedColumn({ colId: 'calc', calculatedExpression: '[a]', spanRows: true } as any);
+ addCalculatedColumnDef(api, { colId: 'calc', calculatedExpression: '[a]', spanRows: true } as any);
await settle();
await new GridRows(api, 'calc-update rows by a').check(`
ROOT id:ROOT_NODE_ID
@@ -670,7 +695,7 @@ describe('row spanning', () => {
└── LEAF id:2 a:"Y" b:"Q" calc:"Y"
`);
- api.updateCalculatedColumn('calc', { calculatedExpression: '[b]' });
+ updateCalculatedColumnDef(api, 'calc', { calculatedExpression: '[b]' });
await settle();
await new GridRows(api, 'calc-update rows by b').check(`
ROOT id:ROOT_NODE_ID
diff --git a/testing/behavioural/src/formulas/calculated-columns-ordering.test.ts b/testing/behavioural/src/formulas/calculated-columns-ordering.test.ts
index 2a54bb15532..1c5250dd6c4 100644
--- a/testing/behavioural/src/formulas/calculated-columns-ordering.test.ts
+++ b/testing/behavioural/src/formulas/calculated-columns-ordering.test.ts
@@ -61,6 +61,27 @@ describe('calculated columns - display ordering', () => {
return gridsManager.createGrid(id, { getRowId: (params) => params.data?.id, ...opts });
}
+ function addCalculatedColumnDef(api: GridApi, colDef: ColDef): void {
+ api.setGridOption('columnDefs', [...(api.getColumnDefs() ?? []), colDef]);
+ }
+
+ function removeColumnDef(api: GridApi, colId: string): void {
+ api.setGridOption('columnDefs', removeColumnDefFromDefs(api.getColumnDefs() ?? [], colId));
+ }
+
+ function removeColumnDefFromDefs(columnDefs: (ColDef | ColGroupDef)[], colId: string): (ColDef | ColGroupDef)[] {
+ const nextColumnDefs: (ColDef | ColGroupDef)[] = [];
+ for (let i = 0, len = columnDefs.length; i < len; ++i) {
+ const colDef = columnDefs[i];
+ if ('children' in colDef) {
+ nextColumnDefs.push({ ...colDef, children: removeColumnDefFromDefs(colDef.children, colId) });
+ } else if ((colDef.colId ?? colDef.field) !== colId) {
+ nextColumnDefs.push(colDef);
+ }
+ }
+ return nextColumnDefs;
+ }
+
function order(api: GridApi): string[] {
return api.getAllGridColumns()!.map((col) => col.getColId());
}
@@ -227,7 +248,7 @@ describe('calculated columns - display ordering', () => {
});
expect(order(api)).toEqual(['revenue', 'profit', 'cost']);
- api.removeCalculatedColumn('profit');
+ removeColumnDef(api, 'profit');
await asyncSetTimeout(1);
// The group's only child is gone — the group must be pruned, not left empty in the colId tree.
@@ -251,7 +272,7 @@ describe('calculated columns - display ordering', () => {
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
columnDefs: [{ field: 'revenue' }, { field: 'cost' }],
});
- api.addCalculatedColumn({
+ addCalculatedColumnDef(api, {
colId: 'profit',
calculatedExpression: '[revenue] - [cost]',
cellDataType: 'number',
@@ -271,13 +292,13 @@ describe('calculated columns - display ordering', () => {
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
columnDefs: [{ field: 'revenue' }, { field: 'cost' }],
});
- api.addCalculatedColumn({
+ addCalculatedColumnDef(api, {
colId: 'profit',
calculatedExpression: '[revenue] - [cost]',
cellDataType: 'number',
});
await asyncSetTimeout(1);
- api.addCalculatedColumn({
+ addCalculatedColumnDef(api, {
colId: 'margin',
calculatedExpression: '[profit] / [revenue]',
cellDataType: 'number',
@@ -301,7 +322,7 @@ describe('calculated columns - display ordering', () => {
});
api.moveColumns(['c'], 0);
expect(order(api)).toEqual(['c', 'a', 'b']);
- api.addCalculatedColumn({ colId: 'sum', calculatedExpression: '[a] + [b]', cellDataType: 'number' });
+ addCalculatedColumnDef(api, { colId: 'sum', calculatedExpression: '[a] + [b]', cellDataType: 'number' });
await asyncSetTimeout(1);
expect(order(api)).toEqual(['c', 'a', 'b', 'sum']);
await new GridColumns(api, 'addCalculatedColumn appends after a manual reorder, preserving the reorder')
@@ -484,7 +505,7 @@ describe('calculated columns - display ordering', () => {
api.moveColumns(['c'], 0);
expect(order(api)).toEqual(['c', 'a', first, second, 'b']);
- api.removeCalculatedColumn(first);
+ removeColumnDef(api, first);
await asyncSetTimeout(1);
expect(order(api)).toEqual(['c', 'a', second, 'b']);
await new GridColumns(api, 'removing an anchor preserves the user reorder and keeps the dependent in place')
@@ -824,7 +845,7 @@ describe('calculated columns - display ordering', () => {
const second = await addViaDialog(api, first, '[Revenue] - [Cost]');
expect(order(api)).toEqual(['revenue', first, second, 'cost']);
- api.removeCalculatedColumn(first);
+ removeColumnDef(api, first);
await asyncSetTimeout(1);
// Only `first` is removed; `second` lost its anchor but keeps its displayed slot (order
// maintained) rather than jumping to the end.
@@ -851,7 +872,7 @@ describe('calculated columns - display ordering', () => {
const second = await addViaDialog(api, first, '[Revenue] - [Cost]');
expect(order(api)).toEqual(['revenue', first, second, 'cost', 'tax']);
- api.removeCalculatedColumn(first);
+ removeColumnDef(api, first);
await asyncSetTimeout(1);
// `second` is in the MIDDLE (cost + tax follow it) — it stays between revenue and cost, and the
// trailing columns are untouched.
@@ -873,13 +894,13 @@ describe('calculated columns - display ordering', () => {
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
columnDefs: [{ field: 'revenue' }, { field: 'cost' }],
});
- api.addCalculatedColumn({
+ addCalculatedColumnDef(api, {
colId: 'profit',
calculatedExpression: '[revenue] - [cost]',
cellDataType: 'number',
});
await asyncSetTimeout(1);
- api.addCalculatedColumn({
+ addCalculatedColumnDef(api, {
colId: 'margin',
calculatedExpression: '[profit] / [revenue]',
cellDataType: 'number',
@@ -887,7 +908,7 @@ describe('calculated columns - display ordering', () => {
await asyncSetTimeout(1);
expect(order(api)).toEqual(['revenue', 'cost', 'profit', 'margin']);
- api.removeCalculatedColumn('profit');
+ removeColumnDef(api, 'profit');
await asyncSetTimeout(1);
expect(order(api)).toEqual(['revenue', 'cost', 'margin']);
await new GridColumns(api, 'removing a dynamic calc col leaves the remaining order intact').checkColumns(`
@@ -908,7 +929,7 @@ describe('calculated columns - display ordering', () => {
],
});
expect(order(api)).toEqual(['revenue', 'profit', 'cost']);
- api.removeCalculatedColumn('profit');
+ removeColumnDef(api, 'profit');
await asyncSetTimeout(1);
expect(order(api)).toEqual(['revenue', 'cost']);
await new GridColumns(api, 'removing a static calc col (suppression) leaves the remaining order intact')
@@ -927,7 +948,7 @@ describe('calculated columns - display ordering', () => {
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
columnDefs: baseDefs,
});
- api.addCalculatedColumn({
+ addCalculatedColumnDef(api, {
colId: 'profit',
calculatedExpression: '[revenue] - [cost]',
cellDataType: 'number',
@@ -950,7 +971,7 @@ describe('calculated columns - display ordering', () => {
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
columnDefs: [{ field: 'revenue' }, { field: 'cost' }],
});
- api.addCalculatedColumn({
+ addCalculatedColumnDef(api, {
colId: 'profit',
calculatedExpression: '[revenue] - [cost]',
cellDataType: 'number',
@@ -984,7 +1005,7 @@ describe('calculated columns - display ordering', () => {
rowNumbers: true,
});
await asyncSetTimeout(1);
- api.addCalculatedColumn({
+ addCalculatedColumnDef(api, {
colId: 'profit',
calculatedExpression: '[revenue] - [cost]',
cellDataType: 'number',
diff --git a/testing/behavioural/src/formulas/calculated-columns-pivot.test.ts b/testing/behavioural/src/formulas/calculated-columns-pivot.test.ts
index d0ca215704f..204679eee82 100644
--- a/testing/behavioural/src/formulas/calculated-columns-pivot.test.ts
+++ b/testing/behavioural/src/formulas/calculated-columns-pivot.test.ts
@@ -1,6 +1,6 @@
import { vi } from 'vitest';
-import type { GridApi, GridOptions, Module } from 'ag-grid-community';
+import type { ColDef, GridApi, GridOptions, Module } from 'ag-grid-community';
import { ClientSideRowModelModule, ValidationModule } from 'ag-grid-community';
import { CalculatedColumnsModule, FormulaModule, PivotModule, RowGroupingModule } from 'ag-grid-enterprise';
@@ -30,6 +30,10 @@ describe('calculated columns - pivot mode', () => {
return gridsManager.createGrid(id, { getRowId: (params) => params.data?.id, ...opts });
}
+ function addCalculatedColumnDef(api: GridApi, colDef: ColDef): void {
+ api.setGridOption('columnDefs', [...(api.getColumnDefs() ?? []), colDef]);
+ }
+
function order(api: GridApi): string[] {
return api.getAllGridColumns()!.map((col) => col.getColId());
}
@@ -128,7 +132,7 @@ describe('calculated columns - pivot mode', () => {
await asyncSetTimeout(10);
const before = order(api);
- api.addCalculatedColumn({
+ addCalculatedColumnDef(api, {
colId: 'profit',
calculatedExpression: '[revenue] - [cost]',
cellDataType: 'number',
diff --git a/testing/behavioural/src/formulas/calculated-columns.test.ts b/testing/behavioural/src/formulas/calculated-columns.test.ts
index d55747bd5da..e2a00b8f4d4 100644
--- a/testing/behavioural/src/formulas/calculated-columns.test.ts
+++ b/testing/behavioural/src/formulas/calculated-columns.test.ts
@@ -2,7 +2,7 @@ import { waitFor } from '@testing-library/dom';
import type { MockInstance } from 'vitest';
import { vi } from 'vitest';
-import type { ColDef, ColGroupDef, GridOptions, Module } from 'ag-grid-community';
+import type { ColDef, ColGroupDef, GridApi, GridOptions, Module } from 'ag-grid-community';
import {
CellSpanModule,
ClientSideRowModelModule,
@@ -87,6 +87,45 @@ describe('ag-grid calculated columns', () => {
return gridsManager.createGrid(id, options);
}
+ function addCalculatedColumnDef(api: GridApi, colDef: ColDef): void {
+ api.setGridOption('columnDefs', [...(api.getColumnDefs() ?? []), colDef]);
+ }
+
+ function updateCalculatedColumnDef(api: GridApi, colId: string, colDefUpdate: ColDef): void {
+ api.setGridOption('columnDefs', updateColumnDef(api.getColumnDefs() ?? [], colId, colDefUpdate));
+ }
+
+ function removeColumnDef(api: GridApi, colId: string): void {
+ api.setGridOption('columnDefs', removeColumnDefFromDefs(api.getColumnDefs() ?? [], colId));
+ }
+
+ function updateColumnDef(
+ columnDefs: (ColDef | ColGroupDef)[],
+ colId: string,
+ colDefUpdate: ColDef
+ ): (ColDef | ColGroupDef)[] {
+ return columnDefs.map((colDef) => {
+ if ('children' in colDef) {
+ return { ...colDef, children: updateColumnDef(colDef.children, colId, colDefUpdate) };
+ }
+
+ return (colDef.colId ?? colDef.field) === colId ? { ...colDef, ...colDefUpdate } : colDef;
+ });
+ }
+
+ function removeColumnDefFromDefs(columnDefs: (ColDef | ColGroupDef)[], colId: string): (ColDef | ColGroupDef)[] {
+ const nextColumnDefs: (ColDef | ColGroupDef)[] = [];
+ for (let i = 0, len = columnDefs.length; i < len; ++i) {
+ const colDef = columnDefs[i];
+ if ('children' in colDef) {
+ nextColumnDefs.push({ ...colDef, children: removeColumnDefFromDefs(colDef.children, colId) });
+ } else if ((colDef.colId ?? colDef.field) !== colId) {
+ nextColumnDefs.push(colDef);
+ }
+ }
+ return nextColumnDefs;
+ }
+
function enableOffsetParentPolyfill(): void {
if (restoreOffsetParent) {
return;
@@ -202,6 +241,18 @@ describe('ag-grid calculated columns', () => {
return button!;
}
+ async function selectDataType(label: string): Promise {
+ getCalculatedColumnDialog()
+ .querySelector('.ag-select .ag-picker-field-wrapper')!
+ .dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
+ await asyncSetTimeout(1);
+ const option = Array.from(document.querySelectorAll('.ag-list-item')).find(
+ (element) => element.textContent?.trim() === label
+ );
+ expect(option).toBeTruthy();
+ option!.dispatchEvent(new MouseEvent('mousedown', { bubbles: true }));
+ }
+
function getSuggestionLabels(): string[] {
return Array.from(document.querySelectorAll('.ag-autocomplete-row-label')).map(
(element) => element.textContent?.trim() ?? ''
@@ -408,7 +459,7 @@ describe('ag-grid calculated columns', () => {
columnDefs: [{ field: 'athlete' }],
});
- api.addCalculatedColumn({ colId: 'athleteCopy', calculatedExpression: '[athlete]' });
+ addCalculatedColumnDef(api, { colId: 'athleteCopy', calculatedExpression: '[athlete]' });
await asyncSetTimeout(1);
await new GridRows(api, 'dynamic calculated span rows', gridRowsOpts).check(`
ROOT id:ROOT_NODE_ID
@@ -506,7 +557,7 @@ describe('ag-grid calculated columns', () => {
columnDefs: [{ field: 'revenue' }, { field: 'cost' }],
});
- api.addCalculatedColumn({
+ addCalculatedColumnDef(api, {
colId: 'profit',
headerName: 'Profit',
calculatedExpression: '[revenue] - [cost]',
@@ -520,7 +571,7 @@ describe('ag-grid calculated columns', () => {
└── LEAF id:r1 revenue:10 cost:3 profit:7
`);
- api.updateCalculatedColumn('profit', {
+ updateCalculatedColumnDef(api, 'profit', {
calculatedExpression: '[revenue] * [cost]',
});
await asyncSetTimeout(1);
@@ -530,7 +581,7 @@ describe('ag-grid calculated columns', () => {
└── LEAF id:r1 revenue:10 cost:3 profit:30
`);
- api.removeCalculatedColumn('profit');
+ removeColumnDef(api, 'profit');
await asyncSetTimeout(1);
await new GridColumns(api, 'removed calculated column').checkColumns(`
@@ -554,14 +605,14 @@ describe('ag-grid calculated columns', () => {
columnDefs,
});
- api.addCalculatedColumn({ colId: 'margin', calculatedExpression: '[profit] / [revenue]' });
+ addCalculatedColumnDef(api, { colId: 'margin', calculatedExpression: '[profit] / [revenue]' });
await asyncSetTimeout(1);
expect(columnDefs).toEqual([revenueColDef, costColDef, profitColDef]);
expect(columnDefs).toHaveLength(3);
expect(findColumnDef(api.getColumnDefs()!, 'margin')?.calculatedExpression).toBe('[profit] / [revenue]');
- api.updateCalculatedColumn('profit', { headerName: 'Profit', calculatedExpression: '[revenue] * [cost]' });
+ updateCalculatedColumnDef(api, 'profit', { headerName: 'Profit', calculatedExpression: '[revenue] * [cost]' });
await asyncSetTimeout(1);
expect(profitColDef).toEqual({
@@ -577,7 +628,7 @@ describe('ag-grid calculated columns', () => {
})
);
- api.removeCalculatedColumn('profit');
+ removeColumnDef(api, 'profit');
await asyncSetTimeout(1);
expect(columnDefs).toEqual([revenueColDef, costColDef, profitColDef]);
@@ -610,24 +661,32 @@ describe('ag-grid calculated columns', () => {
onCalculatedColumnRemoved: removed,
});
- api.addCalculatedColumn({ colId: 'margin', calculatedExpression: '[profit] / [revenue]' });
+ showColumnMenu(api, 'profit');
+ await asyncSetTimeout(10);
+ await clickColumnMenuItem('Add Calculated Column');
+ await asyncSetTimeout(1);
+ setExpression('[Profit] / [Revenue]');
+ clickDialogButton('Apply');
await asyncSetTimeout(1);
- expect(api.getColumn('margin')).toBeTruthy();
+ expect(api.getColumn('calculated_1')).toBeTruthy();
expect(api.getAllDisplayedColumns().map((column) => column.getColId())).toEqual([
'revenue',
'cost',
'profit',
- 'margin',
+ 'calculated_1',
]);
const columnState = api.getColumnState();
- api.updateCalculatedColumn('profit', {
- headerName: 'Updated Profit',
- calculatedExpression: '[revenue] * [cost]',
- });
+ api.openCalculatedColumnDialog('profit');
+ await asyncSetTimeout(1);
+ setExpression('[Revenue] * [Cost]');
+ clickDialogButton('Apply');
await asyncSetTimeout(1);
- api.removeCalculatedColumn('profit');
+
+ showColumnMenu(api, 'profit');
+ await asyncSetTimeout(10);
+ await clickColumnMenuItem('Remove Calculated Column');
await asyncSetTimeout(1);
expect(api.getColumn('profit')).toBeNull();
@@ -636,10 +695,10 @@ describe('ag-grid calculated columns', () => {
api.resetColumnState();
await asyncSetTimeout(1);
- expect(api.getColumn('margin')).toBeNull();
+ expect(api.getColumn('calculated_1')).toBeNull();
expect(api.getColumn('profit')).toBeTruthy();
expect(removed).toHaveBeenCalledTimes(1);
- expect(findColumnDef(api.getColumnDefs()!, 'margin')).toBeUndefined();
+ expect(findColumnDef(api.getColumnDefs()!, 'calculated_1')).toBeUndefined();
expect(findColumnDef(api.getColumnDefs()!, 'profit')).toEqual(
expect.objectContaining({
colId: 'profit',
@@ -652,13 +711,13 @@ describe('ag-grid calculated columns', () => {
expect(api.applyColumnState({ state: columnState, applyOrder: true })).toBe(true);
await asyncSetTimeout(1);
- expect(api.getColumn('margin')).toBeTruthy();
- expect(findColumnDef(api.getColumnDefs()!, 'margin')?.calculatedExpression).toBe('[profit] / [revenue]');
+ expect(api.getColumn('calculated_1')).toBeTruthy();
+ expect(findColumnDef(api.getColumnDefs()!, 'calculated_1')?.calculatedExpression).toBe('[profit] / [revenue]');
expect(api.getAllDisplayedColumns().map((column) => column.getColId())).toEqual([
'revenue',
'cost',
'profit',
- 'margin',
+ 'calculated_1',
]);
await new GridColumns(
api,
@@ -668,11 +727,11 @@ describe('ag-grid calculated columns', () => {
├── revenue "Revenue" width:200
├── cost "Cost" width:200
├── profit "Profit" width:200
- └── margin width:200
+ └── calculated_1 "New title" width:200
`);
});
- test('grid api updates calculated column cellDataType without keeping stale boolean renderer', async () => {
+ test('edit dialog updates calculated column cellDataType without keeping stale boolean renderer', async () => {
const api = createGrid('calculated-grid-api-cell-data-type', {
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
columnDefs: [
@@ -687,18 +746,20 @@ describe('ag-grid calculated columns', () => {
});
await asyncSetTimeout(1);
- api.updateCalculatedColumn('profitable', {
- calculatedExpression: '[revenue] > [cost]',
- cellDataType: 'boolean',
- });
+ api.openCalculatedColumnDialog('profitable');
+ await asyncSetTimeout(1);
+ setExpression('[revenue] > [cost]');
+ await selectDataType('Boolean');
+ clickDialogButton('Apply');
await asyncSetTimeout(1);
expect(api.getColumn('profitable')!.getColDef().cellRenderer).toBe('agCheckboxCellRenderer');
- api.updateCalculatedColumn('profitable', {
- calculatedExpression: 'IF([revenue] > [cost], "yes", "no")',
- cellDataType: 'text',
- });
+ api.openCalculatedColumnDialog('profitable');
+ await asyncSetTimeout(1);
+ setExpression('IF([revenue] > [cost], "yes", "no")');
+ await selectDataType('Text');
+ clickDialogButton('Apply');
await asyncSetTimeout(1);
await new GridRows(api, 'updated calculated column cell data type', gridRowsOpts).check(`
@@ -759,7 +820,7 @@ describe('ag-grid calculated columns', () => {
rowData: [{ id: 'r1', a: 1, b: 2, c: 3 }],
columnDefs: [{ field: 'a' }, { field: 'b' }, { field: 'c' }],
});
- api.addCalculatedColumn({ colId: 'sum', calculatedExpression: '[a] + [b]' });
+ addCalculatedColumnDef(api, { colId: 'sum', calculatedExpression: '[a] + [b]' });
await asyncSetTimeout(600);
const gridDiv = getGridElement(api)!;
@@ -1039,7 +1100,7 @@ describe('ag-grid calculated columns', () => {
await asyncSetTimeout(1);
const created = waitForEvent('calculatedColumnCreated', api);
- api.addCalculatedColumn({
+ addCalculatedColumnDef(api, {
colId: 'profit',
calculatedExpression: '[revenue] - [cost]',
cellDataType: 'number',
@@ -1313,6 +1374,33 @@ describe('ag-grid calculated columns', () => {
`);
});
+ test('clearing the expression shows an empty-expression message, not the formula error', async () => {
+ const api = createGrid('calculated-empty-expression', {
+ rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
+ columnDefs: [{ field: 'revenue' }, { field: 'cost' }],
+ });
+
+ showColumnMenu(api, 'revenue');
+ await asyncSetTimeout(10);
+ await clickColumnMenuItem('Add Calculated Column');
+ await asyncSetTimeout(1);
+
+ // Type a reference, then clear it back to empty (the reported scenario).
+ setExpression('[gold]');
+ setExpression('');
+
+ const input = getExpressionInput();
+ expect(input.validationMessage).toBe('Enter an expression.');
+ expect(input.validationMessage).not.toContain('begin with');
+ expect(input).toHaveClass('invalid');
+ expect(getDialogButton('Apply')).toBeDisabled();
+
+ // Applying an empty expression must not create a column.
+ clickDialogButton('Apply');
+ await asyncSetTimeout(1);
+ expect(api.getColumn('calculated_1')).toBeNull();
+ });
+
test('dialog column picker renders group path and leaf as fixed-height clickable rows', async () => {
const api = createGrid('calculated-dialog-column-picker-group-path', {
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
@@ -1797,7 +1885,7 @@ describe('ag-grid calculated columns', () => {
`);
});
- test('dispatches calculated column API lifecycle events', async () => {
+ test('dispatches calculated column columnDefs lifecycle events', async () => {
const created = vi.fn();
const changed = vi.fn();
const removed = vi.fn();
@@ -1808,17 +1896,17 @@ describe('ag-grid calculated columns', () => {
onCalculatedColumnExpressionChanged: changed,
onCalculatedColumnRemoved: removed,
});
- await new GridColumns(api, `dispatches calculated column API lifecycle events setup`).checkColumns(`
+ await new GridColumns(api, `dispatches calculated column columnDefs lifecycle events setup`).checkColumns(`
CENTER
├── revenue "Revenue" width:200
└── cost "Cost" width:200
`);
- await new GridRows(api, `dispatches calculated column API lifecycle events setup`).check(`
+ await new GridRows(api, `dispatches calculated column columnDefs lifecycle events setup`).check(`
ROOT id:ROOT_NODE_ID
└── LEAF id:r1 revenue:10 cost:3
`);
- api.addCalculatedColumn({ colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
+ addCalculatedColumnDef(api, { colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
await asyncSetTimeout(1);
expect(created).toHaveBeenCalledWith(
expect.objectContaining({
@@ -1828,11 +1916,11 @@ describe('ag-grid calculated columns', () => {
})
);
- api.updateCalculatedColumn('profit', { headerName: 'Profit' });
+ updateCalculatedColumnDef(api, 'profit', { headerName: 'Profit' });
await asyncSetTimeout(1);
expect(changed).not.toHaveBeenCalled();
- api.updateCalculatedColumn('profit', { calculatedExpression: '[revenue] * [cost]' });
+ updateCalculatedColumnDef(api, 'profit', { calculatedExpression: '[revenue] * [cost]' });
await asyncSetTimeout(1);
expect(changed).toHaveBeenCalledWith(
expect.objectContaining({
@@ -1844,7 +1932,7 @@ describe('ag-grid calculated columns', () => {
);
const removedColumn = api.getColumn('profit');
- api.removeCalculatedColumn('profit');
+ removeColumnDef(api, 'profit');
await asyncSetTimeout(1);
expect(removed).toHaveBeenCalledWith(
expect.objectContaining({
@@ -1853,13 +1941,13 @@ describe('ag-grid calculated columns', () => {
source: 'api',
})
);
- await new GridRows(api, `dispatches calculated column API lifecycle events final state`).check(`
+ await new GridRows(api, `dispatches calculated column columnDefs lifecycle events final state`).check(`
ROOT id:ROOT_NODE_ID
└── LEAF id:r1 revenue:10 cost:3
`);
});
- test('addCalculatedColumn / updateCalculatedColumn / removeCalculatedColumn dispatch newColumnsLoaded', async () => {
+ test('calculated column columnDefs mutations dispatch newColumnsLoaded', async () => {
const newColumnsLoaded = vi.fn();
const api = createGrid('calc-col-newColumnsLoaded', {
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
@@ -1870,17 +1958,17 @@ describe('ag-grid calculated columns', () => {
await asyncSetTimeout(1);
newColumnsLoaded.mockClear();
- api.addCalculatedColumn({ colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
+ addCalculatedColumnDef(api, { colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
await asyncSetTimeout(1);
expect(newColumnsLoaded).toHaveBeenCalledTimes(1);
newColumnsLoaded.mockClear();
- api.updateCalculatedColumn('profit', { calculatedExpression: '[revenue] * [cost]' });
+ updateCalculatedColumnDef(api, 'profit', { calculatedExpression: '[revenue] * [cost]' });
await asyncSetTimeout(1);
expect(newColumnsLoaded).toHaveBeenCalledTimes(1);
newColumnsLoaded.mockClear();
- api.removeCalculatedColumn('profit');
+ removeColumnDef(api, 'profit');
await asyncSetTimeout(1);
expect(newColumnsLoaded).toHaveBeenCalledTimes(1);
});
@@ -1895,15 +1983,15 @@ describe('ag-grid calculated columns', () => {
columnDefs: [{ field: 'revenue' }, { field: 'cost' }],
});
- api.addCalculatedColumn({ colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
+ addCalculatedColumnDef(api, { colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
await asyncSetTimeout(1);
- api.removeCalculatedColumn('profit');
+ removeColumnDef(api, 'profit');
await asyncSetTimeout(1);
expect(api.getColumn('profit')).toBeNull();
// Re-add the SAME colId. Must NOT resurrect the destroyed AgColumn from the first add.
- api.addCalculatedColumn({ colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
+ addCalculatedColumnDef(api, { colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
await asyncSetTimeout(1);
expect(api.getColumn('profit')).toBeTruthy();
@@ -1932,22 +2020,22 @@ describe('ag-grid calculated columns', () => {
columnDefs: [{ field: 'revenue' }, { field: 'cost' }],
onNewColumnsLoaded: newColumnsLoaded,
});
- api.addCalculatedColumn({ colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
+ addCalculatedColumnDef(api, { colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
await asyncSetTimeout(1);
newColumnsLoaded.mockClear();
// Same expression — should be a no-op.
- api.updateCalculatedColumn('profit', { calculatedExpression: '[revenue] - [cost]' });
+ updateCalculatedColumnDef(api, 'profit', { calculatedExpression: '[revenue] - [cost]' });
await asyncSetTimeout(1);
expect(newColumnsLoaded).not.toHaveBeenCalled();
// Different expression — should fire.
- api.updateCalculatedColumn('profit', { calculatedExpression: '[revenue] * [cost]' });
+ updateCalculatedColumnDef(api, 'profit', { calculatedExpression: '[revenue] * [cost]' });
await asyncSetTimeout(1);
expect(newColumnsLoaded).toHaveBeenCalledTimes(1);
});
- test('updateCalculatedColumn invalidates the formula service per-cell cache', async () => {
+ test('calculated column columnDefs updates invalidate the formula service per-cell cache', async () => {
const api = createGrid('calc-col-formula-cache-invalidation', {
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
columnDefs: [
@@ -1961,16 +2049,16 @@ describe('ag-grid calculated columns', () => {
const rowNode = api.getRowNode('r1')!;
expect(api.getCellValue({ rowNode, colKey: 'result', useFormatter: false })).toBe(7);
- api.updateCalculatedColumn('result', { calculatedExpression: '[revenue] * [cost]' });
+ updateCalculatedColumnDef(api, 'result', { calculatedExpression: '[revenue] * [cost]' });
await asyncSetTimeout(1);
expect(api.getCellValue({ rowNode, colKey: 'result', useFormatter: false })).toBe(30);
- api.updateCalculatedColumn('result', { calculatedExpression: '[revenue] + [cost]' });
+ updateCalculatedColumnDef(api, 'result', { calculatedExpression: '[revenue] + [cost]' });
await asyncSetTimeout(1);
expect(api.getCellValue({ rowNode, colKey: 'result', useFormatter: false })).toBe(13);
});
- test('updateCalculatedColumn applies column-state changes (width, pinned, hide) to the live column', async () => {
+ test('calculated column columnDefs updates apply column-state changes (width, pinned, hide) to the live column', async () => {
const api = createGrid('calc-col-state-update', {
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
columnDefs: [
@@ -1986,9 +2074,7 @@ describe('ag-grid calculated columns', () => {
expect(profit.isPinned()).toBe(false);
expect(profit.isVisible()).toBe(true);
- // Static calc col → `dynamicOverrides` → builder.applyOverride. Must re-sync runtime state
- // from the merged colDef, same as the normal column reuse path.
- api.updateCalculatedColumn('profit', { width: 250, pinned: 'left', hide: true });
+ updateCalculatedColumnDef(api, 'profit', { width: 250, pinned: 'left', hide: true });
await asyncSetTimeout(1);
const updatedProfit = api.getColumn('profit')!;
@@ -1996,14 +2082,13 @@ describe('ag-grid calculated columns', () => {
expect(updatedProfit.getPinned()).toBe('left');
expect(updatedProfit.isVisible()).toBe(false);
- // Dynamic (API-added) calc col → `applyColDefTo` reuse path. Same invariant.
- api.addCalculatedColumn({ colId: 'margin', calculatedExpression: '[revenue] - [cost]', width: 120 });
+ addCalculatedColumnDef(api, { colId: 'margin', calculatedExpression: '[revenue] - [cost]', width: 120 });
await asyncSetTimeout(1);
const margin = api.getColumn('margin')!;
expect(margin.getActualWidth()).toBe(120);
- api.updateCalculatedColumn('margin', { width: 260, pinned: 'right' });
+ updateCalculatedColumnDef(api, 'margin', { width: 260, pinned: 'right' });
await asyncSetTimeout(1);
const updatedMargin = api.getColumn('margin')!;
@@ -2011,7 +2096,7 @@ describe('ag-grid calculated columns', () => {
expect(updatedMargin.getPinned()).toBe('right');
await new GridColumns(
api,
- 'updateCalculatedColumn applies column-state changes (width, pinned, hide) to the live column'
+ 'calculated column columnDefs updates apply column-state changes (width, pinned, hide) to the live column'
).checkColumns(`
CENTER
├── revenue "Revenue" width:200
@@ -2021,10 +2106,10 @@ describe('ag-grid calculated columns', () => {
`);
});
- test('does not dispatch calculated column lifecycle events for rejected API mutations', async () => {
+ test('dispatches lifecycle events for invalid calculated column columnDefs mutations', async () => {
const created = vi.fn();
const changed = vi.fn();
- const api = createGrid('calculated-rejected-api-events', {
+ const api = createGrid('calculated-invalid-coldef-events', {
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
columnDefs: [
{ field: 'revenue' },
@@ -2036,38 +2121,45 @@ describe('ag-grid calculated columns', () => {
});
await new GridColumns(
api,
- `does not dispatch calculated column lifecycle events for rejected API mutations setup`
+ `dispatches lifecycle events for invalid calculated column columnDefs mutations setup`
).checkColumns(`
CENTER
├── revenue "Revenue" width:200
├── cost "Cost" width:200
└── profit width:200
`);
- await new GridRows(api, `does not dispatch calculated column lifecycle events for rejected API mutations setup`)
+ await new GridRows(api, `dispatches lifecycle events for invalid calculated column columnDefs mutations setup`)
.check(`
ROOT id:ROOT_NODE_ID
└── LEAF id:r1 revenue:10 cost:3 profit:7
`);
- const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
- try {
- api.addCalculatedColumn({ colId: 'bad', calculatedExpression: '[missing] + 1' });
- api.updateCalculatedColumn('profit', { calculatedExpression: '[missing] + 1' });
- await asyncSetTimeout(1);
+ addCalculatedColumnDef(api, { colId: 'bad', calculatedExpression: '[missing] + 1' });
+ updateCalculatedColumnDef(api, 'profit', { calculatedExpression: '[missing] + 1' });
+ await asyncSetTimeout(1);
- expect(api.getColumn('bad')).toBeNull();
- expect(created).not.toHaveBeenCalled();
- expect(changed).not.toHaveBeenCalled();
- expect(api.getColumn('profit')?.getColDef().calculatedExpression).toBe('[revenue] - [cost]');
- } finally {
- consoleWarnSpy.mockRestore();
- }
+ expect(api.getColumn('bad')).toBeTruthy();
+ expect(created).toHaveBeenCalledWith(
+ expect.objectContaining({
+ column: api.getColumn('bad'),
+ expression: '[missing] + 1',
+ source: 'api',
+ })
+ );
+ expect(changed).toHaveBeenCalledWith(
+ expect.objectContaining({
+ column: api.getColumn('profit'),
+ oldExpression: '[revenue] - [cost]',
+ expression: '[missing] + 1',
+ source: 'api',
+ })
+ );
await new GridRows(
api,
- `does not dispatch calculated column lifecycle events for rejected API mutations final state`
+ `dispatches lifecycle events for invalid calculated column columnDefs mutations final state`
).check(`
ROOT id:ROOT_NODE_ID
- └── LEAF id:r1 revenue:10 cost:3 profit:7
+ └── LEAF id:r1 revenue:10 cost:3 profit:"#PARSE!" bad:"#PARSE!"
`);
});
@@ -2518,11 +2610,8 @@ describe('ag-grid calculated columns', () => {
}
});
- test('calculated columns add calculated column classes and opt-in edit highlighting', async () => {
+ test('calculated columns add calculated column classes and edit highlighting by default', async () => {
const api = createGrid('calculated-column-classes', {
- calculatedColumns: {
- columnHighlighting: true,
- },
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
columnDefs: [
{ field: 'revenue' },
@@ -2552,6 +2641,7 @@ describe('ag-grid calculated columns', () => {
await clickColumnMenuItem('Edit Calculated Column');
await asyncSetTimeout(1);
+ expect(document.activeElement?.closest('.ag-dialog')).toBeTruthy();
expect(gridDiv.querySelector('[col-id="profit"].ag-header-cell')).toHaveClass(
'ag-calculated-column-highlighted'
);
@@ -2570,7 +2660,7 @@ describe('ag-grid calculated columns', () => {
);
});
- test('toggling columnHighlighting while the dialog is open updates the highlight live', async () => {
+ test('toggling suppressColumnHighlighting while the dialog is open updates the highlight live', async () => {
const api = createGrid('calculated-column-highlight-toggle', {
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
columnDefs: [
@@ -2590,25 +2680,27 @@ describe('ag-grid calculated columns', () => {
const header = () => gridDiv.querySelector('[col-id="profit"].ag-header-cell');
const cell = () => gridDiv.querySelector('[row-index="0"] [col-id="profit"]');
- // Highlighting is off by default, so an open edit dialog shows no highlight.
- expect(header()).not.toHaveClass('ag-calculated-column-highlighted');
- expect(cell()).not.toHaveClass('ag-calculated-column-highlighted');
-
- // Enabling it while the dialog is open highlights the edited column immediately.
- api.setGridOption('calculatedColumns', { columnHighlighting: true });
- await asyncSetTimeout(1);
+ // Highlighting is on by default, so an open edit dialog highlights the edited column.
expect(header()).toHaveClass('ag-calculated-column-highlighted');
expect(cell()).toHaveClass('ag-calculated-column-highlighted');
- // Disabling it again removes the highlight without closing the dialog.
- api.setGridOption('calculatedColumns', { columnHighlighting: false });
+ // Suppressing it removes the highlight without closing the dialog.
+ api.setGridOption('calculatedColumns', { suppressColumnHighlighting: true });
await asyncSetTimeout(1);
expect(header()).not.toHaveClass('ag-calculated-column-highlighted');
expect(cell()).not.toHaveClass('ag-calculated-column-highlighted');
+
+ api.setGridOption('calculatedColumns', { suppressColumnHighlighting: false });
+ await asyncSetTimeout(1);
+ expect(header()).toHaveClass('ag-calculated-column-highlighted');
+ expect(cell()).toHaveClass('ag-calculated-column-highlighted');
});
- test('calculated column edit highlighting is disabled by default', async () => {
+ test('calculated column edit highlighting can be suppressed', async () => {
const api = createGrid('calculated-column-highlight-disabled', {
+ calculatedColumns: {
+ suppressColumnHighlighting: true,
+ },
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
columnDefs: [
{ field: 'revenue' },
@@ -2637,9 +2729,6 @@ describe('ag-grid calculated columns', () => {
test('adding a calculated column does not highlight the new column', async () => {
const api = createGrid('calculated-column-add-no-highlight', {
- calculatedColumns: {
- columnHighlighting: true,
- },
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
columnDefs: [{ field: 'revenue' }, { field: 'cost' }],
});
@@ -2665,9 +2754,6 @@ describe('ag-grid calculated columns', () => {
test('multiple open calculated column dialogs highlight each edited column', async () => {
const api = createGrid('calculated-column-multi-highlight', {
- calculatedColumns: {
- columnHighlighting: true,
- },
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
columnDefs: [
{ field: 'revenue' },
@@ -2724,9 +2810,6 @@ describe('ag-grid calculated columns', () => {
test('openCalculatedColumnDialog opens the edit dialog for an existing calculated column', async () => {
const api = createGrid('calculated-column-open-dialog-api', {
- calculatedColumns: {
- columnHighlighting: true,
- },
rowData: [{ id: 'r1', revenue: 10, cost: 3 }],
columnDefs: [
{ field: 'revenue' },
@@ -2746,6 +2829,7 @@ describe('ag-grid calculated columns', () => {
const dialog = getCalculatedColumnDialog();
expect(dialog).toBeTruthy();
expect(dialog.querySelector('input')!.value).toBe('Profit');
+ expect(document.activeElement?.closest('.ag-dialog')).toBeNull();
expect(document.activeElement?.closest('[col-id="profit"].ag-header-cell')).toBeNull();
const gridDiv = document.querySelector('#calculated-column-open-dialog-api')!;
@@ -2823,7 +2907,7 @@ describe('ag-grid calculated columns', () => {
validationGridsManager.createGrid('calculated-option-validation', {
calculatedColumns: {
- columnHighlighting: true,
+ suppressColumnHighlighting: true,
},
rowData: [{ revenue: 10 }],
columnDefs: [{ field: 'revenue' }],
@@ -2971,7 +3055,7 @@ describe('ag-grid calculated columns', () => {
// Add a calculated column. Its round-trip through `updateGridOptions({ columnDefs })`
// must not reset the reorder.
- api.addCalculatedColumn({ colId: 'sum', calculatedExpression: '[a] + [b] + [c]' });
+ addCalculatedColumnDef(api, { colId: 'sum', calculatedExpression: '[a] + [b] + [c]' });
await asyncSetTimeout(0);
expect(api.getAllGridColumns()!.map((col) => col.getColId())).toEqual(['c', 'a', 'b', 'sum']);
@@ -3007,7 +3091,7 @@ describe('ag-grid calculated columns', () => {
await asyncSetTimeout(0);
// Add a calculated column at top level (no target column passed).
- api.addCalculatedColumn({ colId: 'sum', calculatedExpression: '[a] + [b] + [c]' });
+ addCalculatedColumnDef(api, { colId: 'sum', calculatedExpression: '[a] + [b] + [c]' });
await asyncSetTimeout(0);
// After the round-trip: `c` stays first, group G still wraps [a, b], sum at the end.
@@ -3047,7 +3131,7 @@ describe('ag-grid calculated columns', () => {
await asyncSetTimeout(0);
expect(api.getAllGridColumns()!.map((col) => col.getColId())).toEqual(['c', 'a', 'b']);
- api.addCalculatedColumn({ colId: 'sum', calculatedExpression: '[a] + [b] + [c]' });
+ addCalculatedColumnDef(api, { colId: 'sum', calculatedExpression: '[a] + [b] + [c]' });
await asyncSetTimeout(0);
expect(api.getAllGridColumns()!.map((col) => col.getColId())).toEqual(['c', 'a', 'b', 'sum']);
@@ -3095,7 +3179,7 @@ describe('ag-grid calculated columns', () => {
expect(monthVirtualBefore).not.toBeNull();
// Add a calc col — full updateGridOptions round-trip.
- api.addCalculatedColumn({ colId: 'doubled', calculatedExpression: '[amount] * 2' });
+ addCalculatedColumnDef(api, { colId: 'doubled', calculatedExpression: '[amount] * 2' });
await asyncSetTimeout(0);
// Virtuals still present and alive after the round-trip.
@@ -3178,7 +3262,7 @@ describe('ag-grid calculated columns', () => {
api.moveColumns(['c'], 0);
await asyncSetTimeout(0);
- api.addCalculatedColumn({ colId: 'sum', calculatedExpression: '[a] + [b] + [c]' });
+ addCalculatedColumnDef(api, { colId: 'sum', calculatedExpression: '[a] + [b] + [c]' });
await asyncSetTimeout(0);
await new GridColumns(api, 'maintainColumnOrder=true: move + addCalcCol').checkColumns(`
@@ -3206,7 +3290,7 @@ describe('ag-grid calculated columns', () => {
api.moveColumns(['c'], 0);
await asyncSetTimeout(0);
- api.addCalculatedColumn({ colId: 'sum', calculatedExpression: '[a] + [b] + [c]' });
+ addCalculatedColumnDef(api, { colId: 'sum', calculatedExpression: '[a] + [b] + [c]' });
await asyncSetTimeout(0);
// Order preservation now comes from the incremental snapshot, not maintainColumnOrder.
@@ -3238,7 +3322,7 @@ describe('ag-grid calculated columns', () => {
api.updateGridOptions({ columnDefs: [{ field: 'b' }, { field: 'a' }, { field: 'c' }] });
await asyncSetTimeout(0);
- api.addCalculatedColumn({ colId: 'sum', calculatedExpression: '[a] + [b] + [c]' });
+ addCalculatedColumnDef(api, { colId: 'sum', calculatedExpression: '[a] + [b] + [c]' });
await asyncSetTimeout(0);
await new GridColumns(api, 'maintainColumnOrder=true: updateColDefs + addCalcCol').checkColumns(`
@@ -3289,7 +3373,7 @@ describe('ag-grid calculated columns', () => {
});
await asyncSetTimeout(0);
- api.addCalculatedColumn({ colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
+ addCalculatedColumnDef(api, { colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
await asyncSetTimeout(0);
await new GridColumns(api, 'rowGroup + calc col').checkColumns(`
@@ -3337,7 +3421,7 @@ describe('ag-grid calculated columns', () => {
// A calc col is a primary (non-value) column, so the pivot cross-tab has no cell for it:
// adding one while pivot is active does NOT add it to the pivot display, and the pivot result
// is unaffected. It stays a resolvable primary column (and reappears when pivot is off).
- api.addCalculatedColumn({ colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
+ addCalculatedColumnDef(api, { colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
await asyncSetTimeout(0);
expect(api.getColumn('profit')).toBeTruthy();
@@ -3364,7 +3448,7 @@ describe('ag-grid calculated columns', () => {
});
await asyncSetTimeout(0);
- api.addCalculatedColumn({ colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
+ addCalculatedColumnDef(api, { colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
await asyncSetTimeout(0);
await new GridColumns(api, 'rowSelection + calc col').checkColumns(`
@@ -3392,7 +3476,7 @@ describe('ag-grid calculated columns', () => {
});
await asyncSetTimeout(0);
- api.addCalculatedColumn({ colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
+ addCalculatedColumnDef(api, { colId: 'profit', calculatedExpression: '[revenue] - [cost]' });
await asyncSetTimeout(0);
await new GridColumns(api, 'rowNumbers + calc col').checkColumns(`
@@ -3418,7 +3502,7 @@ describe('ag-grid calculated columns', () => {
});
await asyncSetTimeout(0);
- api.addCalculatedColumn({ colId: 'sum', calculatedExpression: '[a] + [b] + [c]' });
+ addCalculatedColumnDef(api, { colId: 'sum', calculatedExpression: '[a] + [b] + [c]' });
await asyncSetTimeout(0);
// Move sum to position 0 after creation.
@@ -3426,7 +3510,7 @@ describe('ag-grid calculated columns', () => {
await asyncSetTimeout(0);
// Add another calc col; sum's runtime position should still be 0.
- api.addCalculatedColumn({ colId: 'avg', calculatedExpression: '([a] + [b] + [c]) / 3' });
+ addCalculatedColumnDef(api, { colId: 'avg', calculatedExpression: '([a] + [b] + [c]) / 3' });
await asyncSetTimeout(0);
await new GridColumns(api, 'moveColumns on calc col + subsequent add').checkColumns(`