diff --git a/documentation/ag-grid-docs/src/content/docs-nav/nav.json b/documentation/ag-grid-docs/src/content/docs-nav/nav.json index 7c7b59ddb34..5993472b97f 100644 --- a/documentation/ag-grid-docs/src/content/docs-nav/nav.json +++ b/documentation/ag-grid-docs/src/content/docs-nav/nav.json @@ -713,7 +713,15 @@ "type": "item", "title": "Formulas", "path": "formulas", - "isEnterprise": true + "isEnterprise": true, + "children": [ + { + "type": "item", + "title": "Formula Editor", + "path": "formula-editor-component", + "isEnterprise": true + } + ] }, { "type": "item", diff --git a/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component-disabled/example.spec.ts b/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component-disabled/example.spec.ts new file mode 100644 index 00000000000..70f89aa9d01 --- /dev/null +++ b/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component-disabled/example.spec.ts @@ -0,0 +1,10 @@ +import { ensureGridReady, test, waitForGridContent } from '@utils/grid/test-utils'; + +test.agExample(import.meta, () => { + test.eachFramework('Example', async ({ page }) => { + // PLACEHOLDER - MINIMAL TEST TO ENSURE GRID LOADS WITHOUT ERRORS + await ensureGridReady(page); + await waitForGridContent(page); + // END PLACEHOLDER + }); +}); diff --git a/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component-disabled/index.html b/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component-disabled/index.html new file mode 100644 index 00000000000..378fad58398 --- /dev/null +++ b/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component-disabled/index.html @@ -0,0 +1 @@ +
diff --git a/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component-disabled/main.ts b/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component-disabled/main.ts new file mode 100644 index 00000000000..37bdaeb8e24 --- /dev/null +++ b/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component-disabled/main.ts @@ -0,0 +1,88 @@ +import type { ColDef, GetRowIdParams, GridApi, GridOptions, ValueFormatterParams } from 'ag-grid-community'; +import { + ClientSideRowModelModule, + ModuleRegistry, + NumberEditorModule, + TextEditorModule, + ValidationModule, + createGrid, +} from 'ag-grid-community'; +import { FormulaModule } from 'ag-grid-enterprise'; + +ModuleRegistry.registerModules([ + ClientSideRowModelModule, + FormulaModule, + NumberEditorModule, + TextEditorModule, + ValidationModule, +]); + +let gridApi: GridApi; + +const valueFormatter = ({ value }: ValueFormatterParams) => `$ ${Number(value).toFixed(2)}`; +const getRowId = (params: GetRowIdParams) => String(params.data.id); + +const columnDefs: ColDef[] = [ + { field: 'item' }, + { field: 'price', valueFormatter }, + { field: 'qty' }, + { field: 'total', allowFormula: true, cellEditor: 'agTextCellEditor', valueFormatter }, +]; + +const gridOptions: GridOptions = { + columnDefs, + getRowId, + defaultColDef: { + editable: true, + flex: 1, + }, + rowData: [ + { + id: 1, + item: 'Apples', + price: 1.2, + qty: 4, + total: '=REF(COLUMN("price"),ROW(1))*REF(COLUMN("qty"),ROW(1))', + }, + { + id: 2, + item: 'Bananas', + price: 0.5, + qty: 6, + total: '=REF(COLUMN("price"),ROW(2))*REF(COLUMN("qty"),ROW(2))', + }, + { + id: 3, + item: 'Oranges', + price: 0.8, + qty: 3, + total: '=REF(COLUMN("price"),ROW(3))*REF(COLUMN("qty"),ROW(3))', + }, + { + id: 4, + item: 'Pears', + price: 1.4, + qty: 2, + total: '=REF(COLUMN("price"),ROW(4))*REF(COLUMN("qty"),ROW(4))', + }, + { + id: 5, + item: 'Grapes', + price: 2.1, + qty: 3, + total: '=REF(COLUMN("price"),ROW(5))*REF(COLUMN("qty"),ROW(5))', + }, + { + id: 6, + item: 'Strawberries', + price: 1.8, + qty: 4, + total: '=REF(COLUMN("price"),ROW(6))*REF(COLUMN("qty"),ROW(6))', + }, + ], +}; + +document.addEventListener('DOMContentLoaded', () => { + const gridDiv = document.querySelector('#myGrid')!; + gridApi = createGrid(gridDiv, gridOptions); +}); diff --git a/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component/example.spec.ts b/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component/example.spec.ts new file mode 100644 index 00000000000..70f89aa9d01 --- /dev/null +++ b/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component/example.spec.ts @@ -0,0 +1,10 @@ +import { ensureGridReady, test, waitForGridContent } from '@utils/grid/test-utils'; + +test.agExample(import.meta, () => { + test.eachFramework('Example', async ({ page }) => { + // PLACEHOLDER - MINIMAL TEST TO ENSURE GRID LOADS WITHOUT ERRORS + await ensureGridReady(page); + await waitForGridContent(page); + // END PLACEHOLDER + }); +}); diff --git a/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component/index.html b/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component/index.html new file mode 100644 index 00000000000..378fad58398 --- /dev/null +++ b/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component/index.html @@ -0,0 +1 @@ +
diff --git a/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component/main.ts b/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component/main.ts new file mode 100644 index 00000000000..374d32c62b7 --- /dev/null +++ b/documentation/ag-grid-docs/src/content/docs/formula-editor-component/_examples/formula-editor-component/main.ts @@ -0,0 +1,94 @@ +import type { ColDef, GetRowIdParams, GridApi, GridOptions, ValueFormatterParams } from 'ag-grid-community'; +import { + ClientSideRowModelModule, + ModuleRegistry, + NumberEditorModule, + TextEditorModule, + ValidationModule, + createGrid, +} from 'ag-grid-community'; +import { CellSelectionModule, FormulaModule } from 'ag-grid-enterprise'; + +ModuleRegistry.registerModules([ + CellSelectionModule, + ClientSideRowModelModule, + FormulaModule, + NumberEditorModule, + TextEditorModule, + ValidationModule, +]); + +let gridApi: GridApi; + +const valueFormatter = ({ value }: ValueFormatterParams) => `$ ${Number(value).toFixed(2)}`; +const getRowId = (params: GetRowIdParams) => String(params.data.id); + +const columnDefs: ColDef[] = [ + { field: 'item' }, + { field: 'price', valueFormatter }, + { field: 'qty' }, + { field: 'total', allowFormula: true, valueFormatter }, +]; + +const gridOptions: GridOptions = { + columnDefs, + getRowId, + cellSelection: { + handle: { + mode: 'fill', + }, + }, + defaultColDef: { + editable: true, + flex: 1, + }, + rowData: [ + { + id: 1, + item: 'Apples', + price: 1.2, + qty: 4, + total: '=REF(COLUMN("price"),ROW(1))*REF(COLUMN("qty"),ROW(1))', + }, + { + id: 2, + item: 'Bananas', + price: 0.5, + qty: 6, + total: '=REF(COLUMN("price"),ROW(2))*REF(COLUMN("qty"),ROW(2))', + }, + { + id: 3, + item: 'Oranges', + price: 0.8, + qty: 3, + total: '=REF(COLUMN("price"),ROW(3))*REF(COLUMN("qty"),ROW(3))', + }, + { + id: 4, + item: 'Pears', + price: 1.4, + qty: 2, + total: '=REF(COLUMN("price"),ROW(4))*REF(COLUMN("qty"),ROW(4))', + }, + { + id: 5, + item: 'Grapes', + price: 2.1, + qty: 3, + total: '=REF(COLUMN("price"),ROW(5))*REF(COLUMN("qty"),ROW(5))', + }, + { + id: 6, + item: 'Strawberries', + price: 1.8, + qty: 4, + total: '=REF(COLUMN("price"),ROW(6))*REF(COLUMN("qty"),ROW(6))', + }, + ], +}; + +document.addEventListener('DOMContentLoaded', () => { + const gridDiv = document.querySelector('#myGrid')!; + gridApi = createGrid(gridDiv, gridOptions); +}); diff --git a/documentation/ag-grid-docs/src/content/docs/formula-editor-component/index.mdoc b/documentation/ag-grid-docs/src/content/docs/formula-editor-component/index.mdoc new file mode 100644 index 00000000000..3e4e95ba921 --- /dev/null +++ b/documentation/ag-grid-docs/src/content/docs/formula-editor-component/index.mdoc @@ -0,0 +1,21 @@ +--- +title: "Formula Editor Component" +enterprise: true +--- +The Formula Cell Editor is the default editor for columns with `allowFormula: true`. It tokenises cell references, highlights ranges, and provides function autocomplete while you type. + +## Default Formula Editor + +If a column enables formulas and does not specify a `cellEditor`, the grid automatically uses the Formula Cell Editor. + +{% gridExampleRunner title="Formula Editor" name="formula-editor-component" exampleHeight=320 /%} + +{% note %} +Range highlights and range handle editing require `cellSelection` to be enabled. Without it, the editor still works but range highlights and handles are not shown. +{% /note %} + +## Disabling Formulas Cell Editor + +Providing a `cellEditor` opts the column out of the Formula Cell Editor. Formulas still evaluate, but range highlighting, handles, and function autocomplete are disabled because a different editor is in use. + +{% gridExampleRunner title="Formula Editor Disabled" name="formula-editor-component-disabled" exampleHeight=320 /%} diff --git a/documentation/ag-grid-docs/src/content/docs/formulas/_examples/formulas/main.ts b/documentation/ag-grid-docs/src/content/docs/formulas/_examples/formulas/main.ts index 7f673f36cef..c412b58d205 100644 --- a/documentation/ag-grid-docs/src/content/docs/formulas/_examples/formulas/main.ts +++ b/documentation/ag-grid-docs/src/content/docs/formulas/_examples/formulas/main.ts @@ -7,9 +7,10 @@ import { ValidationModule, createGrid, } from 'ag-grid-community'; -import { FormulaModule } from 'ag-grid-enterprise'; +import { CellSelectionModule, FormulaModule } from 'ag-grid-enterprise'; ModuleRegistry.registerModules([ + CellSelectionModule, ClientSideRowModelModule, FormulaModule, NumberEditorModule, @@ -38,6 +39,11 @@ const gridOptions: GridOptions = { editable: true, flex: 1, }, + cellSelection: { + handle: { + mode: 'fill', + }, + }, rowData: [ { id: 1, diff --git a/documentation/ag-grid-docs/src/content/docs/formulas/index.mdoc b/documentation/ag-grid-docs/src/content/docs/formulas/index.mdoc index 10cda1c2e74..7db7c7576f6 100644 --- a/documentation/ag-grid-docs/src/content/docs/formulas/index.mdoc +++ b/documentation/ag-grid-docs/src/content/docs/formulas/index.mdoc @@ -8,8 +8,6 @@ Formula strings can be provided to cells in the grid, allowing for dynamic calcu To enable formulas, set the column property `allowFormula = true` on one or more columns and ensure that your rows have [Row IDs](./row-ids/#row-ids). In the example below, the values in columns Subtotal, Tax and Total are computed using formulas which can be modified by the end-user. -{% gridExampleRunner title="Formulas" name="formulas" exampleHeight=390 /%} - ```{% frameworkTransform=true %} const gridOptions = { columnDefs: [ @@ -20,12 +18,31 @@ const gridOptions = { { field: 'tax', allowFormula: true }, { field: 'total', allowFormula: true }, ], + cellSelection: { + handle: { + mode: 'fill', + }, + }, getRowId: (params) => String(params.data.rid), } ``` -When using formulas, the following features are enabled by default: -- [Row Numbers](./row-numbers/) +{% gridExampleRunner title="Formulas" name="formulas" exampleHeight=390 /%} + + +## Formula Editor Component + +The Formula Cell Editor provides range highlighting, range handles, and function autocomplete while you edit formula-enabled +cells. It appears automatically for columns with `allowFormula: true` unless you supply a `cellEditor`. See +[Formula Editor Component](./formula-editor-component/) for behaviour details and how to disable it. + +Range highlights and range handle editing require `cellSelection` to be enabled. + +## Feature Interactions + +When using formulas, the following feature integrations apply: +- [Fill Handle](./cell-selection-fill-handle/): Dragging to fill from a formula offsets relative references (for example `=B1+C2` becomes `=B2+C3` on the next row). Absolute references (`$A$1`, `A$1`, `$A1`) remain fixed. +- [Row Numbers](./row-numbers/): Enabled by default. Certain features do not work in conjunction with formulas: - [Cell Expressions](./cell-expressions/#cell-expressions) @@ -51,15 +68,13 @@ Constants can be numbers (e.g. `3.14`, `42`, `-7`), strings (e.g. `"Hello"`, `"W ### Cell references -Cell references are used to refer to the value of another cell in the grid and are symbolised by an alphabetical column identifier followed by a numerical row identifier. +Cell references are used to refer to the value of another cell in the grid and are symbolised by an alphabetical column identifier followed by a numerical row identifier (e.g. `A1`). After the 26th column, the columns continue with two letters (e.g. AA, AB, AC, etc.). Rows are numbered starting from 1. {% note %} Column letters are assigned for every column in the grid, including columns that are hidden or not displayed due to column groups being collapsed. {% /note %} -Formulas can reference other cells using their column letter and row number. Columns are labelled alphabetically (A, B, C, ..., Z, AA, AB, etc.), and rows are numbered starting from 1. - Examples of cell references: - `=A1` refers to the cell in column A, row 1. - `=B2` refers to the cell in column B, row 2. @@ -71,13 +86,12 @@ To instead always refer to the same position, use absolute references by prefixi - `=$A1` always refers to column A, but the row can change. #### Long-Form References -When saving cell references, the grid converts these into a long hand format using column and row IDs to ensure that changes in the source data allow the grid to continue to refer to the correct cells when the data changes without -the grid being open. +When saving cell references, the grid converts these into a long-hand format using column and row IDs. This ensures the formula continues to refer to the correct cells even if rows or columns move while the grid is not open. For example, if the column with ID `athlete` is in column A, and the row with ID `a` is in row 1, then the formula `=A1` becomes `=REF(COLUMN('athlete'), ROW('a'))`. -When this is a static reference, for example $A$1, the long hand format uses A and 1 in place of IDs, but adds a true to indicate the value is absolute (e.g `=$A$1` becomes `=REF(COLUMN('a', true), ROW('1', true))`). +When this is a static reference, for example `$A$1`, the long-hand format uses A and 1 in place of IDs but adds `true` to indicate the value is absolute (e.g. `=$A$1` becomes `=REF(COLUMN('a', true), ROW('1', true))`). -This long hand format can be used directly in your data source or application via a valueGetter, and will be converted to the short hand format when the user opens a cell editor. +This long-hand format can be used directly in your data source or application via a `valueGetter`, and will be converted to the shorthand format when the user opens a cell editor. {% note %} As cell formulas are not immediately parsed when the grid opens, we suggest using the long hand format when providing formulas directly in your data source or application as any row positional changes may impact the relative cell when resolved. @@ -267,7 +281,7 @@ Formulas can be stored outside of the grid's `rowData` by providing a `formulaDa The snippet and example below illustrate an example of storing formulae in a `Map` object, external to the grid. -In the example, the "Total" column contains editable formulas which are stored in an external `Map`. The contents of the map and the row data can be visualised with the provided buttons. +In the example, the "Total" column contains editable formulas which are stored in an external `Map`. The contents of the map and the row data can be visualised with the provided buttons. This example intentionally omits `cellSelection` to show that formulas work without range highlights. ```{% frameworkTransform=true %} const gridOptions = { @@ -297,7 +311,7 @@ const gridOptions = { {% gridExampleRunner title="Formula Data Source" name="formulas-formula-data-source" exampleHeight=440 /%} -A user provided datasource should provide the following interface: +A user-provided data source should provide the following interface: {% interfaceDocumentation interfaceName="FormulaDataSource" config={ "description": "" } /%} @@ -367,3 +381,7 @@ When performing an [Excel Export](./excel-export), the grid will export the form | `COUNTA(arg1, arg2, ...)` | Returns the count of non-empty values among the arguments. | | `COUNTBLANK(arg1, arg2, ...)` | Returns the count of empty values among the arguments. | | `COUNTIF(range, criteria)` | Returns the count of values in the range that meet the criteria. | + +## Next Up + +Continue to the next section: [Formula Editor Component](./formula-editor-component/). \ No newline at end of file diff --git a/documentation/ag-grid-docs/src/content/docs/provided-cell-editors/index.mdoc b/documentation/ag-grid-docs/src/content/docs/provided-cell-editors/index.mdoc index 1da3c6344bc..96b2092a3b7 100644 --- a/documentation/ag-grid-docs/src/content/docs/provided-cell-editors/index.mdoc +++ b/documentation/ag-grid-docs/src/content/docs/provided-cell-editors/index.mdoc @@ -8,6 +8,7 @@ The grid comes with some cell editors provided out of the box. These cell editor * [Large Text Cell Editor](./provided-cell-editors-large-text/) * [Select Cell Editor](./provided-cell-editors-select/) * [Rich Select Cell Editor](./provided-cell-editors-rich-select/) {% enterpriseIcon /%} +* [Formula Cell Editor](./formula-editor-component/) {% enterpriseIcon /%} There are also some additional cell editors that are generally used with [Cell Data Types](./cell-data-types/): diff --git a/packages/ag-grid-community/src/edit/strategy/fullRowEditStrategy.ts b/packages/ag-grid-community/src/edit/strategy/fullRowEditStrategy.ts index 826308d0656..6ea2dc0bbd4 100644 --- a/packages/ag-grid-community/src/edit/strategy/fullRowEditStrategy.ts +++ b/packages/ag-grid-community/src/edit/strategy/fullRowEditStrategy.ts @@ -211,20 +211,11 @@ export class FullRowEditStrategy extends BaseEditStrategy { return; // Row not rendered, no editors to destroy. } - const destroyedColumns = new Set(); + // Destroy every editor created for this row, including those without edit model entries. for (const cellCtrl of rowCtrl.getAllCellCtrls()) { - const column = cellCtrl.column; - if (destroyedColumns.has(column)) { - continue; // Column editor already processed. + if (cellCtrl.comp?.getCellEditor()) { + _destroyEditor(this.beans, cellCtrl, undefined, cellCtrl); } - destroyedColumns.add(column); - - if (!cellCtrl.comp?.getCellEditor()) { - continue; // No editor to destroy. - } - - // Destroy every editor created for this row, including those without edit model entries. - _destroyEditor(this.beans, cellCtrl, undefined, cellCtrl); } }