diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ca7395..fcaa1af 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,12 @@ All notable changes to `dash-ag-grid` will be documented in this file. This project adheres to [Semantic Versioning](https://semver.org/). Links "DE#nnn" prior to version 2.0 point to the Dash Enterprise closed-source Dash AG Grid repo +## UNRELEASED + +### Changed + +- [#261](https://github.com/plotly/dash-ag-grid/pull/261) The `cellValueChanged` property has changed been changed from a (single) event object to a _list_ of event objects. For multi-cell edits, the list will contain an element per change. In other cases, the list will contain a single element. Fixes [#262](https://github.com/plotly/dash-ag-grid/issues/262) + ## [2.4.0] - 2023-10-17 ### Added diff --git a/package.json b/package.json index 9e6f847..0f5fcc2 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "build:js": "webpack --mode production", "build:backends": "dash-generate-components ./src/lib/components dash_ag_grid -p package-info.json --r-prefix '' --jl-prefix ''", "build": "run-s prepublishOnly build:js build:backends", - "postbuild": "es-check es2015 dash_ag_grid/*.js", + "postbuild": "es-check es2017 dash_ag_grid/*.js", "private::format.eslint": "eslint --quiet --fix src", "private::format.prettier": "prettier --write src --ignore-path=.prettierignore", "format": "run-s private::format.*", diff --git a/src/lib/components/AgGrid.react.js b/src/lib/components/AgGrid.react.js index bfb9d8d..f7b4be1 100644 --- a/src/lib/components/AgGrid.react.js +++ b/src/lib/components/AgGrid.react.js @@ -682,42 +682,44 @@ DashAgGrid.propTypes = { /** * Value has changed after editing. */ - cellValueChanged: PropTypes.shape({ - /** - * rowIndex, typically a row number - */ - rowIndex: PropTypes.number, - - /** - * Row Id from the grid, this could be a number automatically, or set via getRowId - */ - rowId: PropTypes.any, - - /** - * data, data object from the row - */ - data: PropTypes.object, - - /** - * old value of the cell - */ - oldValue: PropTypes.any, - - /** - * new value of the cell - */ - newValue: PropTypes.any, - - /** - * column where the cell was changed - */ - colId: PropTypes.any, - - /** - * Timestamp of when the event was fired - */ - timestamp: PropTypes.any, - }), + cellValueChanged: PropTypes.arrayOf( + PropTypes.shape({ + /** + * rowIndex, typically a row number + */ + rowIndex: PropTypes.number, + + /** + * Row Id from the grid, this could be a number automatically, or set via getRowId + */ + rowId: PropTypes.any, + + /** + * data, data object from the row + */ + data: PropTypes.object, + + /** + * old value of the cell + */ + oldValue: PropTypes.any, + + /** + * new value of the cell + */ + newValue: PropTypes.any, + + /** + * column where the cell was changed + */ + colId: PropTypes.any, + + /** + * Timestamp of when the event was fired + */ + timestamp: PropTypes.any, + }) + ), /** * Other ag-grid options diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js index c4818a6..12a84ff 100644 --- a/src/lib/fragments/AgGrid.react.js +++ b/src/lib/fragments/AgGrid.react.js @@ -65,6 +65,9 @@ const RESIZE_DEBOUNCE_MS = 200; // Rate-limit for updating columnState when interacting with the grid const COL_RESIZE_DEBOUNCE_MS = 500; +// Time between syncing cell value changes with Dash +const CELL_VALUE_CHANGED_DEBOUNCE_MS = 1; + const xssMessage = (context) => { console.error( context, @@ -119,6 +122,7 @@ export default class DashAgGrid extends Component { this.onCellClicked = this.onCellClicked.bind(this); this.onCellDoubleClicked = this.onCellDoubleClicked.bind(this); this.onCellValueChanged = this.onCellValueChanged.bind(this); + this.afterCellValueChanged = this.afterCellValueChanged.bind(this); this.onRowDataUpdated = this.onRowDataUpdated.bind(this); this.onFilterChanged = this.onFilterChanged.bind(this); this.onSortChanged = this.onSortChanged.bind(this); @@ -181,6 +185,7 @@ export default class DashAgGrid extends Component { this.selectionEventFired = false; this.reference = React.createRef(); + this.pendingChanges = null; } onPaginationChanged() { @@ -912,20 +917,38 @@ export default class DashAgGrid extends Component { node, }) { const timestamp = Date.now(); + // Collect new change. + const newChange = { + rowIndex, + rowId: node.id, + data, + oldValue, + value, + colId, + timestamp, + }; + // Append it to current change session. + if (!this.pendingCellValueChanges) { + this.pendingCellValueChanges = [newChange]; + } else { + this.pendingCellValueChanges.push(newChange); + } + } + + afterCellValueChanged() { + // Guard against multiple invocations of the same change session. + if (!this.pendingCellValueChanges) { + return; + } + // Send update(s) for current change session to Dash. const virtualRowData = this.virtualRowData(); this.props.setProps({ - cellValueChanged: { - rowIndex, - rowId: node.id, - data, - oldValue, - value, - colId, - timestamp, - }, + cellValueChanged: this.pendingCellValueChanges, virtualRowData, }); this.syncRowData(); + // Mark current change session as ended. + this.pendingCellValueChanges = null; } onDisplayedColumnsChanged() { @@ -1325,7 +1348,11 @@ export default class DashAgGrid extends Component { onSelectionChanged={this.onSelectionChanged} onCellClicked={this.onCellClicked} onCellDoubleClicked={this.onCellDoubleClicked} - onCellValueChanged={this.onCellValueChanged} + onCellValueChanged={debounce( + this.afterCellValueChanged, + CELL_VALUE_CHANGED_DEBOUNCE_MS, + this.onCellValueChanged + )} onFilterChanged={this.onFilterChanged} onSortChanged={this.onSortChanged} onRowDragEnd={this.onSortChanged} diff --git a/src/lib/utils/debounce.js b/src/lib/utils/debounce.js index 1740baa..33552bc 100644 --- a/src/lib/utils/debounce.js +++ b/src/lib/utils/debounce.js @@ -1,4 +1,4 @@ -export default function debounce(fn, wait) { +export default function debounce(fn, wait, onDebounce) { let lastTimestamp = 0; let handle; @@ -6,6 +6,9 @@ export default function debounce(fn, wait) { const now = Date.now(); const delay = Math.min(now - lastTimestamp, wait); + if (onDebounce) { + onDebounce(...args); + } if (handle) { clearTimeout(handle); } diff --git a/tests/test_cell_value_changed.py b/tests/test_cell_value_changed.py index c1ee108..dabde15 100644 --- a/tests/test_cell_value_changed.py +++ b/tests/test_cell_value_changed.py @@ -29,7 +29,7 @@ def test_cv001_cell_value_changed(dash_duo): dag.AgGrid( id="history", columnDefs=[{"field": "Key", "checkboxSelection": True}] - + [{"field": i} for i in ["Column", "OldValue", "NewValue"]], + + [{"field": i} for i in ["Column", "OldValue", "NewValue"]], rowData=[], ), ], @@ -43,12 +43,16 @@ def test_cv001_cell_value_changed(dash_duo): ) app.clientside_callback( - """function addToHistory(data) { - if (data) { - reloadData = {...data.data} - reloadData[data.colId] = data.oldValue - newData = [{Key: data.rowId, Column: data.colId, OldValue: data.oldValue, NewValue: data.value, - reloadData}] + """function addToHistory(changes) { + if (changes) { + newData = [] + for (let i = 0; i < changes.length; i++) { + data = changes[i]; + reloadData = {...data.data}; + reloadData[data.colId] = data.oldValue; + newData.push({Key: data.rowId, Column: data.colId, OldValue: data.oldValue, + NewValue: data.value, reloadData}); + } return {'add': newData} } return window.dash_clientside.no_update @@ -100,3 +104,47 @@ def test_cv001_cell_value_changed(dash_duo): grid.get_cell(1, 2).click() hist.wait_for_rendered_rows(1) + + +def test_cv001_cell_value_changed_multi(dash_duo): + app = Dash(__name__) + app.layout = html.Div( + [ + dag.AgGrid( + columnDefs=columnDefs, + rowData=df.to_dict("records"), + columnSize="sizeToFit", + defaultColDef={"editable": True}, + id="grid", + getRowId="params.data.nation", + dashGridOptions={'editType':'fullRow'} + ), + html.Div(id="log") + ] + ) + + app.clientside_callback( + """function countEvents(changes) { + console.log("FIRE"); + return changes? changes.length : 0; + }""", + Output("log", "children"), + Input("grid", "cellValueChanged"), + prevent_initial_call=True, + ) + + dash_duo.start_server(app) + + grid = utils.Grid(dash_duo, "grid") + grid.wait_for_cell_text(0, 0, "South Korea") + + # Test single event. + grid.get_cell(0, 1).send_keys("50") + grid.get_cell(1, 2).click() + dash_duo.wait_for_text_to_equal('#log', "1") + + # Test multi event. + grid.get_cell(0, 1).send_keys("20") + grid.get_cell_editing_input(0, 2).send_keys("20") + grid.get_cell(1, 2).click() + dash_duo.wait_for_text_to_equal('#log', "2") diff --git a/tests/utils.py b/tests/utils.py index d6ec10c..e2634ac 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -170,6 +170,11 @@ def get_cell(self, row, col): f'#{self.id} .ag-row[row-index="{row}"] .ag-cell[aria-colindex="{col + 1}"]' ) + def get_cell_editing_input(self, row, col): + return self.dash_duo.find_element( + f'#{self.id} .ag-row[row-index="{row}"] .ag-cell[aria-colindex="{col + 1}"] .ag-cell-editor input' + ) + def get_row(self, row): return self.dash_duo.find_element(f'#{self.id} .ag-row[row-index="{row}"]')