diff --git a/CHANGELOG.md b/CHANGELOG.md
index a4e80578..e1cc912f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,11 @@ 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
+- Component is refactored to be a function component rather than a class
+
## [32.3.0rc0] - 2025-04-15
### Fixed
diff --git a/package-lock.json b/package-lock.json
index 505552ac..5b824820 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -10022,6 +10022,7 @@
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "dev": true,
"dependencies": {
"loose-envify": "^1.1.0"
},
@@ -10063,6 +10064,7 @@
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+ "dev": true,
"dependencies": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
@@ -10827,6 +10829,7 @@
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+ "dev": true,
"dependencies": {
"loose-envify": "^1.1.0"
}
@@ -12824,8 +12827,7 @@
"version": "7.21.0-placeholder-for-preset-env.2",
"resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz",
"integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"@babel/plugin-syntax-async-generators": {
"version": "7.8.4",
@@ -13827,8 +13829,7 @@
"@emotion/use-insertion-effect-with-fallbacks": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz",
- "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==",
- "requires": {}
+ "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw=="
},
"@emotion/utils": {
"version": "1.2.1",
@@ -14316,8 +14317,7 @@
"@mui/types": {
"version": "7.2.13",
"resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.13.tgz",
- "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g==",
- "requires": {}
+ "integrity": "sha512-qP9OgacN62s+l8rdDhSFRe05HWtLLJ5TGclC9I1+tQngbssu0m2dmFZs+Px53AcOs9fD7TbYd4gc9AXzVqO/+g=="
},
"@mui/utils": {
"version": "5.15.7",
@@ -14671,22 +14671,19 @@
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz",
"integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"@webpack-cli/info": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz",
"integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"@webpack-cli/serve": {
"version": "2.0.5",
"resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz",
"integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"@xtuc/ieee754": {
"version": "1.2.0",
@@ -14710,15 +14707,13 @@
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz",
"integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"acorn-jsx": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz",
"integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"ag-charts-community": {
"version": "10.3.4",
@@ -14810,8 +14805,7 @@
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"ansi-regex": {
"version": "5.0.1",
@@ -15908,8 +15902,7 @@
"version": "9.1.0",
"resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.0.tgz",
"integrity": "sha512-NSWl5BFQWEPi1j4TjVNItzYV7dZXZ+wP6I6ZhrBGpChQhZRUaElihE9uRRkcbRnNb76UMKDF3r+WTmNcGPKsqw==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"eslint-import-resolver-node": {
"version": "0.3.9",
@@ -16752,8 +16745,7 @@
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz",
"integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"ignore": {
"version": "5.3.1",
@@ -19088,8 +19080,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz",
"integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"postcss-modules-local-by-default": {
"version": "4.0.4",
@@ -19200,6 +19191,7 @@
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz",
"integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==",
+ "dev": true,
"requires": {
"loose-envify": "^1.1.0"
}
@@ -19234,6 +19226,7 @@
"version": "18.2.0",
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz",
"integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==",
+ "dev": true,
"requires": {
"loose-envify": "^1.1.0",
"scheduler": "^0.23.0"
@@ -19761,6 +19754,7 @@
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz",
"integrity": "sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==",
+ "dev": true,
"requires": {
"loose-envify": "^1.1.0"
}
@@ -20136,8 +20130,7 @@
"version": "3.3.4",
"resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz",
"integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==",
- "dev": true,
- "requires": {}
+ "dev": true
},
"style-to-object": {
"version": "0.4.4",
diff --git a/src/lib/components/AgGrid.react.js b/src/lib/components/AgGrid.react.js
index 05b460e8..5813dd3b 100644
--- a/src/lib/components/AgGrid.react.js
+++ b/src/lib/components/AgGrid.react.js
@@ -1,6 +1,6 @@
import PropTypes from 'prop-types';
import LazyLoader from '../LazyLoader';
-import React, {Component, lazy, Suspense} from 'react';
+import React, {lazy, Suspense, useState, useCallback, useEffect} from 'react';
const RealAgGrid = lazy(LazyLoader.agGrid);
const RealAgGridEnterprise = lazy(LazyLoader.agGridEnterprise);
@@ -12,21 +12,13 @@ function getGrid(enable) {
/**
* Dash interface to AG Grid, a powerful tabular data component.
*/
-class DashAgGrid extends Component {
- constructor(props) {
- super(props);
+function DashAgGrid(props) {
+ const [state, setState] = useState({
+ mounted: false,
+ rowTransaction: null,
+ });
- this.state = {
- mounted: false,
- rowTransaction: null,
- };
-
- this.buildArray = this.buildArray.bind(this);
- }
-
- static dashRenderType = true;
-
- buildArray(arr1, arr2) {
+ const buildArray = useCallback((arr1, arr2) => {
if (arr1) {
if (!arr1.includes(arr2)) {
return [...arr1, arr2];
@@ -34,33 +26,32 @@ class DashAgGrid extends Component {
return arr1;
}
return [JSON.parse(JSON.stringify(arr2))];
- }
-
- UNSAFE_componentWillReceiveProps(nextProps) {
- if (this.props.rowTransaction && !this.state.mounted) {
- if (nextProps.rowTransaction !== this.props.rowTransaction) {
- this.setState({
- rowTransaction: this.buildArray(
- this.state.rowTransaction,
- this.props.rowTransaction
- ),
- });
- }
+ }, []);
+
+ useEffect(() => {
+ if (props.rowTransaction && !state.mounted) {
+ setState((prevState) => ({
+ ...prevState,
+ rowTransaction: buildArray(
+ prevState.rowTransaction,
+ props.rowTransaction
+ ),
+ }));
}
- }
+ }, [props.rowTransaction, state.mounted, buildArray]);
- render() {
- const {enableEnterpriseModules} = this.props;
+ const {enableEnterpriseModules} = props;
+ const RealComponent = getGrid(enableEnterpriseModules);
- const RealComponent = getGrid(enableEnterpriseModules);
- return (
-
-
-
- );
- }
+ return (
+
+
+
+ );
}
+DashAgGrid.dashRenderType = true;
+
DashAgGrid.defaultProps = {
className: 'ag-theme-alpine',
resetColumnState: false,
diff --git a/src/lib/fragments/AgGrid.react.js b/src/lib/fragments/AgGrid.react.js
index 06c273f2..52e6a907 100644
--- a/src/lib/fragments/AgGrid.react.js
+++ b/src/lib/fragments/AgGrid.react.js
@@ -1,4 +1,4 @@
-import React, {Component} from 'react';
+import React, {useCallback, useRef, useState, useMemo, useEffect} from 'react';
import PropTypes from 'prop-types';
import * as evaluate from 'static-eval';
import * as esprima from 'esprima';
@@ -37,7 +37,6 @@ import {
PROPS_NOT_FOR_AG_GRID,
GRID_DANGEROUS_FUNCTIONS,
OMIT_PROP_RENDER,
- OMIT_STATE_RENDER,
OBJ_MAYBE_FUNCTION_OR_MAP_MAYBE_FUNCTIONS,
} from '../utils/propCategories';
import debounce from '../utils/debounce';
@@ -116,100 +115,188 @@ function stringifyId(id) {
return '{' + parts.join(',') + '}';
}
-export default class DashAgGrid extends Component {
- constructor(props) {
- super(props);
-
- this.onGridReady = this.onGridReady.bind(this);
- this.onSelectionChanged = this.onSelectionChanged.bind(this);
- 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);
- this.onRowGroupOpened = this.onRowGroupOpened.bind(this);
- this.onDisplayedColumnsChanged =
- this.onDisplayedColumnsChanged.bind(this);
- this.onColumnResized = this.onColumnResized.bind(this);
- this.onGridSizeChanged = this.onGridSizeChanged.bind(this);
- this.updateColumnWidths = this.updateColumnWidths.bind(this);
- this.handleDynamicStyle = this.handleDynamicStyle.bind(this);
- this.generateRenderer = this.generateRenderer.bind(this);
- this.resetColumnState = this.resetColumnState.bind(this);
- this.exportDataAsCsv = this.exportDataAsCsv.bind(this);
- this.setSelection = this.setSelection.bind(this);
- this.memoizeOne = this.memoizeOne.bind(this);
- this.convertFunction = this.convertFunction.bind(this);
- this.convertMaybeFunction = this.convertMaybeFunction.bind(this);
- this.convertCol = this.convertCol.bind(this);
- this.convertOne = this.convertOne.bind(this);
- this.convertAllProps = this.convertAllProps.bind(this);
- this.buildArray = this.buildArray.bind(this);
- this.onAsyncTransactionsFlushed =
- this.onAsyncTransactionsFlushed.bind(this);
- this.onPaginationChanged = this.onPaginationChanged.bind(this);
- this.scrollTo = this.scrollTo.bind(this);
-
- // Additional Exposure
- this.selectAll = this.selectAll.bind(this);
- this.deselectAll = this.deselectAll.bind(this);
- this.updateColumnState = this.updateColumnState.bind(this);
- this.deleteSelectedRows = this.deleteSelectedRows.bind(this);
- this.rowTransaction = this.rowTransaction.bind(this);
- this.getRowData = this.getRowData.bind(this);
- this.syncRowData = this.syncRowData.bind(this);
- this.isDatasourceLoadedForInfiniteScrolling =
- this.isDatasourceLoadedForInfiniteScrolling.bind(this);
- this.getDatasource = this.getDatasource.bind(this);
- this.applyRowTransaction = this.applyRowTransaction.bind(this);
- this.parseFunction = this.parseFunction.bind(this);
-
- const customComponents = window.dashAgGridComponentFunctions || {};
- const newComponents = map(this.generateRenderer, customComponents);
- this.active = true;
- this.customSetProps = (propsToSet) => {
- if (this.active) {
- this.props.setProps(propsToSet);
+function usePrevious(value) {
+ const ref = useRef();
+
+ useEffect(() => {
+ setTimeout(() => {
+ ref.current = value;
+ }, 1);
+ }, [value]);
+
+ return ref.current;
+}
+
+export function DashAgGrid(props) {
+ const active = useRef(true);
+
+ // const customSetProps = props.setProps;
+ const customSetProps = useCallback(
+ (propsToSet) => {
+ if (active.current) {
+ props.setProps(propsToSet);
}
- };
- this.setEventData = (data) => {
+ },
+ [props.setProps]
+ );
+
+ const setEventData = useCallback(
+ (data) => {
const timestamp = Date.now();
- this.customSetProps({
+ customSetProps({
eventData: {
data,
timestamp,
},
});
- };
+ },
+ [customSetProps]
+ );
- this.convertedPropCache = {};
+ const parseFunction = useMemo(
+ () =>
+ memoizeWith(String, (funcString) => {
+ const parsedCondition =
+ esprima.parse(funcString).body[0].expression;
+ const context = {
+ d3,
+ dash_clientside,
+ ...customFunctions,
+ ...window.dashAgGridFunctions,
+ };
+ return (params) =>
+ evaluate(parsedCondition, {params, ...context});
+ }),
+ []
+ );
- this.state = {
- ...this.props.parentState,
- components: {
- rowMenu: this.generateRenderer(RowMenuRenderer),
- markdown: this.generateRenderer(MarkdownRenderer),
- ...newComponents,
- },
- rerender: 0,
- openGroups: {},
- gridApi: null,
- columnState_push: true,
- };
+ const parseFunctionEvent = useMemo(
+ () =>
+ memoizeWith(String, (funcString) => {
+ const parsedCondition =
+ esprima.parse(funcString).body[0].expression;
+ const context = {
+ d3,
+ dash_clientside,
+ ...customFunctions,
+ ...window.dashAgGridFunctions,
+ setGridProps: customSetProps,
+ setEventData: setEventData,
+ };
+ return (params) =>
+ evaluate(parsedCondition, {params, ...context});
+ }),
+ [customSetProps, setEventData]
+ );
- this.selectionEventFired = false;
- this.pauseSelections = false;
- this.reference = React.createRef();
- this.pendingChanges = null;
- this.dataUpdates = false;
- }
+ const parseFunctionNoParams = useMemo(
+ () =>
+ memoizeWith(String, (funcString) => {
+ const parsedCondition =
+ esprima.parse(funcString).body[0].expression;
+ const context = {
+ d3,
+ ...customFunctions,
+ ...window.dashAgGridFunctions,
+ };
+ return evaluate(parsedCondition, context);
+ }),
+ []
+ );
+
+ /**
+ * @params AG-Grid Styles rules attribute.
+ * Cells: https://www.ag-grid.com/react-grid/cell-styles/#cell-style-cell-class--cell-class-rules-params
+ * Rows: https://www.ag-grid.com/react-grid/row-styles/#row-style-row-class--row-class-rules-params
+ */
+ const handleDynamicStyle = useCallback(
+ (cellStyle) => {
+ const {styleConditions, defaultStyle} = cellStyle;
+ const _defaultStyle = defaultStyle || null;
+
+ if (styleConditions && styleConditions.length) {
+ const tests = styleConditions.map(({condition, style}) => ({
+ test: parseFunction(condition),
+ style,
+ }));
+ return (params) => {
+ for (const {test, style} of tests) {
+ if (params) {
+ if (params.node.id && params.node.id !== null) {
+ if (test(params)) {
+ return style;
+ }
+ }
+ }
+ }
+ return _defaultStyle;
+ };
+ }
- onPaginationChanged() {
- const {gridApi} = this.state;
+ return _defaultStyle;
+ },
+ [parseFunction]
+ );
+
+ const generateRenderer = useCallback(
+ (Renderer) => {
+ const {dangerously_allow_code} = props;
+
+ return (cellProps) => (
+ {
+ customSetProps({
+ cellRendererData: {
+ value,
+ colId: cellProps.column.colId,
+ rowIndex: cellProps.node.sourceRowIndex,
+ rowId: cellProps.node.id,
+ timestamp: Date.now(),
+ },
+ });
+ }}
+ dangerously_allow_code={dangerously_allow_code}
+ {...cellProps}
+ >
+ );
+ },
+ [props.dangerously_allow_code, customSetProps]
+ );
+
+ const customComponents = window.dashAgGridComponentFunctions || {};
+ const newComponents = map(generateRenderer, customComponents);
+
+ const [gridApi, setGridApi] = useState(null);
+ const [, forceRerender] = useState({});
+ const [openGroups, setOpenGroups] = useState({});
+ const [columnState_push, setColumnState_push] = useState(true);
+ const [rowTransactionState, setRowTransactionState] = useState(null);
+
+ const components = useMemo(
+ () => ({
+ rowMenu: generateRenderer(RowMenuRenderer),
+ markdown: generateRenderer(MarkdownRenderer),
+ ...newComponents,
+ }),
+ [generateRenderer, newComponents]
+ );
+
+ const prevProps = usePrevious(props);
+ const prevGridApi = usePrevious(gridApi);
+
+ const convertedPropCache = useRef({});
+
+ const selectionEventFired = useRef(false);
+ const pauseSelections = useRef(false);
+ const reference = useRef();
+ const dataUpdates = useRef(false);
+ const getDetailParams = useRef();
+ const getRowsParams = useRef(null);
+ const pendingCellValueChanges = useRef(null);
+
+ const onPaginationChanged = useCallback(() => {
if (gridApi && !gridApi?.isDestroyed()) {
- this.customSetProps({
+ customSetProps({
paginationInfo: {
isLastPageFound: gridApi.paginationIsLastPageFound(),
pageSize: gridApi.paginationGetPageSize(),
@@ -219,1282 +306,1300 @@ export default class DashAgGrid extends Component {
},
});
}
- }
+ }, [gridApi, customSetProps]);
- setSelection(selection, gridApi = this.state?.gridApi) {
- const {getRowId} = this.props;
- if (gridApi && selection && !gridApi?.isDestroyed()) {
- this.pauseSelections = true;
- const nodeData = [];
- if (has('function', selection)) {
- const test = this.parseFunction(selection.function);
+ const setSelection = useCallback(
+ (selection) => {
+ const {getRowId} = props;
+ if (gridApi && selection && !gridApi?.isDestroyed()) {
+ pauseSelections.current = true;
+ const nodeData = [];
+ if (has('function', selection)) {
+ const test = parseFunction(selection.function);
- gridApi.forEachNode((node) => {
- if (test(node)) {
- nodeData.push(node);
- }
- });
- } else if (has('ids', selection)) {
- const mapId = {};
- selection.ids.forEach((id) => {
- mapId[id] = true;
- });
- gridApi.forEachNode((node) => {
- if (mapId[node.id]) {
- nodeData.push(node);
- }
- });
- } else {
- if (selection.length) {
- if (getRowId) {
- const parsedCondition = esprima.parse(
- getRowId.replaceAll('params.data.', '')
- ).body[0].expression;
- const mapId = {};
- selection.forEach((params) => {
- mapId[evaluate(parsedCondition, params)] = true;
- });
- gridApi.forEachNode((node) => {
- if (mapId[node.id]) {
- nodeData.push(node);
- }
- });
- } else {
- gridApi.forEachNode((node) => {
- if (includes(node.data, selection)) {
- nodeData.push(node);
- }
- });
+ gridApi.forEachNode((node) => {
+ if (test(node)) {
+ nodeData.push(node);
+ }
+ });
+ } else if (has('ids', selection)) {
+ const mapId = {};
+ selection.ids.forEach((id) => {
+ mapId[id] = true;
+ });
+ gridApi.forEachNode((node) => {
+ if (mapId[node.id]) {
+ nodeData.push(node);
+ }
+ });
+ } else {
+ if (selection.length) {
+ if (getRowId) {
+ const parsedCondition = esprima.parse(
+ getRowId.replaceAll('params.data.', '')
+ ).body[0].expression;
+ const mapId = {};
+ selection.forEach((params) => {
+ mapId[evaluate(parsedCondition, params)] = true;
+ });
+ gridApi.forEachNode((node) => {
+ if (mapId[node.id]) {
+ nodeData.push(node);
+ }
+ });
+ } else {
+ gridApi.forEachNode((node) => {
+ if (includes(node.data, selection)) {
+ nodeData.push(node);
+ }
+ });
+ }
}
}
+ gridApi.deselectAll();
+ gridApi.setNodesSelected({
+ nodes: nodeData,
+ newValue: true,
+ });
+ setTimeout(() => {
+ pauseSelections.current = false;
+ }, 1);
}
- gridApi.deselectAll();
- gridApi.setNodesSelected({nodes: nodeData, newValue: true});
- setTimeout(() => {
- this.pauseSelections = false;
- }, 1);
- }
- }
+ },
+ [gridApi, props.getRowId, parseFunction]
+ );
- memoizeOne(converter, obj, target) {
- const cache = this.convertedPropCache[target];
- if (cache && obj === cache[0]) {
- return cache[1];
- }
- const result = converter(obj, target);
- this.convertedPropCache[target] = [obj, result];
- return result;
- }
+ const memoizeOne = useCallback(
+ (converter, obj, target) => {
+ const cache = convertedPropCache.current[target];
+ if (cache && obj === cache[0]) {
+ return cache[1];
+ }
+ const result = converter(obj, target);
+ convertedPropCache.current[target] = [obj, result];
+ return result;
+ },
+ [convertedPropCache]
+ );
- convertFunction(func) {
- // TODO: do we want this? ie allow the form `{function: }` even when
- // we're expecting just a string?
- if (has('function', func)) {
- return this.convertFunction(func.function);
- }
+ const convertFunction = useCallback(
+ (func) => {
+ // TODO: do we want this? ie allow the form `{function: }` even when
+ // we're expecting just a string?
+ if (has('function', func)) {
+ return convertFunction(func.function);
+ }
- try {
- if (typeof func !== 'string') {
- throw new Error('tried to parse non-string as function', func);
+ try {
+ if (typeof func !== 'string') {
+ throw new Error(
+ 'tried to parse non-string as function',
+ func
+ );
+ }
+ return parseFunction(func);
+ } catch (err) {
+ console.log(err);
}
- return this.parseFunction(func);
- } catch (err) {
- console.log(err);
- }
- return '';
- }
+ return '';
+ },
+ [parseFunction]
+ );
- convertFunctionNoParams(func) {
- // TODO: do we want this? ie allow the form `{function: }` even when
- // we're expecting just a string?
- if (has('function', func)) {
- return this.convertFunctionNoParams(func.function);
- }
+ const convertFunctionNoParams = useCallback(
+ (func) => {
+ // TODO: do we want this? ie allow the form `{function: }` even when
+ // we're expecting just a string?
+ if (has('function', func)) {
+ return convertFunctionNoParams(func.function);
+ }
- try {
- if (typeof func !== 'string') {
- throw new Error('tried to parse non-string as function', func);
+ try {
+ if (typeof func !== 'string') {
+ throw new Error(
+ 'tried to parse non-string as function',
+ func
+ );
+ }
+ return parseFunctionNoParams(func);
+ } catch (err) {
+ console.log(err);
}
- return this.parseFunctionNoParams(func);
- } catch (err) {
- console.log(err);
- }
- return '';
- }
+ return '';
+ },
+ [parseFunctionNoParams]
+ );
- convertMaybeFunction(maybeFunc, stringsEvalContext) {
- if (has('function', maybeFunc)) {
- return this.convertFunction(maybeFunc.function);
- }
+ const convertMaybeFunction = useCallback(
+ (maybeFunc, stringsEvalContext) => {
+ if (has('function', maybeFunc)) {
+ return convertFunction(maybeFunc.function);
+ }
- if (
- stringsEvalContext &&
- typeof maybeFunc === 'string' &&
- !this.props.dangerously_allow_code
- ) {
- xssMessage(stringsEvalContext);
- return null;
- }
- return maybeFunc;
- }
+ if (
+ stringsEvalContext &&
+ typeof maybeFunc === 'string' &&
+ !props.dangerously_allow_code
+ ) {
+ xssMessage(stringsEvalContext);
+ return null;
+ }
+ return maybeFunc;
+ },
+ [props.dangerously_allow_code, convertFunction]
+ );
- convertMaybeFunctionNoParams(maybeFunc, stringsEvalContext) {
- if (has('function', maybeFunc)) {
- return this.convertFunctionNoParams(maybeFunc.function);
- }
+ const convertMaybeFunctionNoParams = useCallback(
+ (maybeFunc, stringsEvalContext) => {
+ if (has('function', maybeFunc)) {
+ return convertFunctionNoParams(maybeFunc.function);
+ }
- if (
- stringsEvalContext &&
- typeof maybeFunc === 'string' &&
- !this.props.dangerously_allow_code
- ) {
- xssMessage(stringsEvalContext);
- return null;
- }
- return maybeFunc;
- }
+ if (
+ stringsEvalContext &&
+ typeof maybeFunc === 'string' &&
+ !props.dangerously_allow_code
+ ) {
+ xssMessage(stringsEvalContext);
+ return null;
+ }
+ return maybeFunc;
+ },
+ [props.dangerously_allow_code, convertFunctionNoParams]
+ );
- suppressGetDetail(colName) {
+ const suppressGetDetail = useCallback((colName) => {
return (params) => {
params.successCallback(params.data[colName]);
};
- }
+ }, []);
- callbackGetDetail = (params) => {
+ const callbackGetDetail = useCallback((params) => {
const {data} = params;
- this.getDetailParams = params;
+ getDetailParams.current = params;
// Adding the current time in ms forces Dash to trigger a callback
// when the same row is closed and re-opened.
- this.customSetProps({
+ customSetProps({
getDetailRequest: {data: data, requestTime: Date.now()},
});
- };
+ }, []);
- convertCol(columnDef) {
- if (typeof columnDef === 'function') {
- return columnDef;
- }
-
- return mapObjIndexed((value, target) => {
- if (
- target === 'cellStyle' &&
- (has('styleConditions', value) || has('defaultStyle', value))
- ) {
- return this.handleDynamicStyle(value);
- }
- if (OBJ_OF_FUNCTIONS[target]) {
- return map(this.convertFunction, value);
- }
- if (COLUMN_DANGEROUS_FUNCTIONS[target]) {
- // the second argument tells convertMaybeFunction
- // that a plain string is dangerous,
- // and provides the context for error reporting
- const field = columnDef.field || columnDef.headerName;
- return this.convertMaybeFunction(value, {target, field});
- }
- if (COLUMN_MAYBE_FUNCTIONS[target]) {
- return this.convertMaybeFunction(value);
- }
- if (COLUMN_MAYBE_FUNCTIONS_NO_PARAMS[target]) {
- return this.convertMaybeFunctionNoParams(value);
+ const convertCol = useCallback(
+ (columnDef) => {
+ if (typeof columnDef === 'function') {
+ return columnDef;
}
- if (COLUMN_ARRAY_NESTED_FUNCTIONS[target] && Array.isArray(value)) {
- return value.map((c) => {
- if (typeof c === 'object') {
- return this.convertCol(c);
- }
- return c;
- });
- }
- if (OBJ_MAYBE_FUNCTION_OR_MAP_MAYBE_FUNCTIONS[target]) {
- if ('function' in value) {
- if (typeof value.function === 'string') {
- return this.convertMaybeFunctionNoParams(value);
- }
+
+ return mapObjIndexed((value, target) => {
+ if (
+ target === 'cellStyle' &&
+ (has('styleConditions', value) ||
+ has('defaultStyle', value))
+ ) {
+ return handleDynamicStyle(value);
+ }
+ if (OBJ_OF_FUNCTIONS[target]) {
+ return map(convertFunction, value);
+ }
+ if (COLUMN_DANGEROUS_FUNCTIONS[target]) {
+ // the second argument tells convertMaybeFunction
+ // that a plain string is dangerous,
+ // and provides the context for error reporting
+ const field = columnDef.field || columnDef.headerName;
+ return convertMaybeFunction(value, {target, field});
+ }
+ if (COLUMN_MAYBE_FUNCTIONS[target]) {
+ return convertMaybeFunction(value);
}
- return map((v) => {
- if (typeof v === 'object') {
- if (typeof v.function === 'string') {
- return this.convertMaybeFunctionNoParams(v);
+ if (COLUMN_MAYBE_FUNCTIONS_NO_PARAMS[target]) {
+ return convertMaybeFunctionNoParams(value);
+ }
+ if (
+ COLUMN_ARRAY_NESTED_FUNCTIONS[target] &&
+ Array.isArray(value)
+ ) {
+ return value.map((c) => {
+ if (typeof c === 'object') {
+ return convertCol(c);
+ }
+ return c;
+ });
+ }
+ if (OBJ_MAYBE_FUNCTION_OR_MAP_MAYBE_FUNCTIONS[target]) {
+ if ('function' in value) {
+ if (typeof value.function === 'string') {
+ return convertMaybeFunctionNoParams(value);
}
- return this.convertCol(v);
}
- return v;
- }, value);
- }
- if (COLUMN_NESTED_FUNCTIONS[target] && typeof value === 'object') {
- return this.convertCol(value);
- }
- if (COLUMN_NESTED_OR_OBJ_OF_FUNCTIONS[target]) {
- if (has('function', value)) {
- return this.convertMaybeFunction(value);
+ return map((v) => {
+ if (typeof v === 'object') {
+ if (typeof v.function === 'string') {
+ return convertMaybeFunctionNoParams(v);
+ }
+ return convertCol(v);
+ }
+ return v;
+ }, value);
}
- return this.convertCol(value);
- }
- // not one of those categories - pass it straight through
- return value;
- }, columnDef);
- }
-
- convertOne(value, target) {
- if (value) {
- if (target === 'columnDefs') {
- return value.map(this.convertCol);
- }
- if (GRID_COLUMN_CONTAINERS[target]) {
- return this.convertCol(value);
- }
- if (OBJ_MAYBE_FUNCTION_OR_MAP_MAYBE_FUNCTIONS[target]) {
- if ('function' in value) {
- if (typeof value.function === 'string') {
- return this.convertMaybeFunctionNoParams(value);
+ if (
+ COLUMN_NESTED_FUNCTIONS[target] &&
+ typeof value === 'object'
+ ) {
+ return convertCol(value);
+ }
+ if (COLUMN_NESTED_OR_OBJ_OF_FUNCTIONS[target]) {
+ if (has('function', value)) {
+ return convertMaybeFunction(value);
}
+ return convertCol(value);
}
- return mapObjIndexed((v) => {
- if (typeof v === 'object') {
- if ('function' in v) {
- if (typeof v.function === 'string') {
- return this.convertMaybeFunctionNoParams(v);
+ // not one of those categories - pass it straight through
+ return value;
+ }, columnDef);
+ },
+ [
+ handleDynamicStyle,
+ convertFunction,
+ convertMaybeFunction,
+ convertMaybeFunctionNoParams,
+ ]
+ );
+
+ const convertOneRef = useRef();
+ const convertAllPropsRef = useRef();
+
+ const convertOne = useCallback(
+ (value, target) => {
+ if (value) {
+ if (target === 'columnDefs') {
+ return value.map(convertCol);
+ }
+ if (GRID_COLUMN_CONTAINERS[target]) {
+ return convertCol(value);
+ }
+ if (OBJ_MAYBE_FUNCTION_OR_MAP_MAYBE_FUNCTIONS[target]) {
+ if ('function' in value) {
+ if (typeof value.function === 'string') {
+ return convertMaybeFunctionNoParams(value);
+ }
+ }
+ return mapObjIndexed((v) => {
+ if (typeof v === 'object') {
+ if ('function' in v) {
+ if (typeof v.function === 'string') {
+ return convertMaybeFunctionNoParams(v);
+ }
+ } else {
+ return convertCol(v);
}
- } else {
- return this.convertCol(v);
}
+ return v;
+ }, value);
+ }
+ if (GRID_NESTED_FUNCTIONS[target]) {
+ let adjustedVal = value;
+ if ('suppressCallback' in value) {
+ adjustedVal = {
+ ...adjustedVal,
+ getDetailRowData: value.suppressCallback
+ ? suppressGetDetail(value.detailColName)
+ : callbackGetDetail,
+ };
}
- return v;
- }, value);
- }
- if (GRID_NESTED_FUNCTIONS[target]) {
- let adjustedVal = value;
- if ('suppressCallback' in value) {
- adjustedVal = {
- ...adjustedVal,
- getDetailRowData: value.suppressCallback
- ? this.suppressGetDetail(value.detailColName)
- : this.callbackGetDetail,
- };
+ if ('detailGridOptions' in value) {
+ adjustedVal = assocPath(
+ ['detailGridOptions', 'components'],
+ components,
+ adjustedVal
+ );
+ }
+ return convertAllPropsRef.current(adjustedVal);
}
- if ('detailGridOptions' in value) {
- adjustedVal = assocPath(
- ['detailGridOptions', 'components'],
- this.state.components,
- adjustedVal
- );
+ if (GRID_DANGEROUS_FUNCTIONS[target]) {
+ return convertMaybeFunctionNoParams(value, {prop: target});
+ }
+ if (target === 'getRowId') {
+ return convertFunction(value);
+ }
+ if (
+ target === 'getRowStyle' &&
+ (has('styleConditions', value) ||
+ has('defaultStyle', value))
+ ) {
+ return handleDynamicStyle(value);
+ }
+ if (OBJ_OF_FUNCTIONS[target]) {
+ return map(convertFunction, value);
+ }
+ if (GRID_ONLY_FUNCTIONS[target]) {
+ return convertFunction(value);
+ }
+ if (GRID_MAYBE_FUNCTIONS[target]) {
+ return convertMaybeFunction(value);
+ }
+ if (GRID_MAYBE_FUNCTIONS_NO_PARAMS[target]) {
+ return convertMaybeFunctionNoParams(value);
}
- return this.convertAllProps(adjustedVal);
- }
- if (GRID_DANGEROUS_FUNCTIONS[target]) {
- return this.convertMaybeFunctionNoParams(value, {prop: target});
- }
- if (target === 'getRowId') {
- return this.convertFunction(value);
- }
- if (
- target === 'getRowStyle' &&
- (has('styleConditions', value) || has('defaultStyle', value))
- ) {
- return this.handleDynamicStyle(value);
- }
- if (OBJ_OF_FUNCTIONS[target]) {
- return map(this.convertFunction, value);
- }
- if (GRID_ONLY_FUNCTIONS[target]) {
- return this.convertFunction(value);
- }
- if (GRID_MAYBE_FUNCTIONS[target]) {
- return this.convertMaybeFunction(value);
- }
- if (GRID_MAYBE_FUNCTIONS_NO_PARAMS[target]) {
- return this.convertMaybeFunctionNoParams(value);
- }
+ return value;
+ }
return value;
- }
- return value;
- }
+ },
+ [
+ convertCol,
+ convertMaybeFunctionNoParams,
+ suppressGetDetail,
+ callbackGetDetail,
+ components,
+ convertAllPropsRef.current,
+ convertFunction,
+ handleDynamicStyle,
+ convertMaybeFunction,
+ ]
+ );
- convertAllProps(props) {
- return mapObjIndexed(
- (value, target) => this.memoizeOne(this.convertOne, value, target),
- props
- );
- }
+ const convertAllProps = useCallback(
+ (props) => {
+ return mapObjIndexed(
+ (value, target) =>
+ memoizeOne(convertOneRef.current, value, target),
+ props
+ );
+ },
+ [memoizeOne, convertOneRef.current]
+ );
+
+ convertOneRef.current = convertOne;
+ convertAllPropsRef.current = convertAllProps;
+
+ const virtualRowData = useCallback(() => {
+ const {rowModelType} = props;
+ const virtualRowData = [];
+ if (rowModelType === 'clientSide' && gridApi) {
+ gridApi.forEachNodeAfterFilterAndSort((node) => {
+ if (node.data) {
+ virtualRowData.push(node.data);
+ }
+ });
+ }
+ return virtualRowData;
+ }, [props.rowModelType, gridApi]);
- onFilterChanged() {
- const {rowModelType} = this.props;
- if (!this.state.gridApi) {
+ const onFilterChanged = useCallback(() => {
+ const {rowModelType} = props;
+ if (!gridApi) {
return;
}
- const filterModel = this.state.gridApi.getFilterModel();
+ const filterModel = gridApi.getFilterModel();
const propsToSet = {filterModel};
if (rowModelType === 'clientSide') {
- propsToSet.virtualRowData = this.virtualRowData();
+ propsToSet.virtualRowData = virtualRowData();
}
- this.customSetProps(propsToSet);
- }
+ customSetProps(propsToSet);
+ }, [props.rowModelType, gridApi, virtualRowData, customSetProps]);
- getRowData() {
+ const getRowData = useCallback(() => {
const newRowData = [];
- this.state.gridApi.forEachLeafNode((node) => {
+ gridApi.forEachLeafNode((node) => {
newRowData.push(node.data);
});
return newRowData;
- }
-
- virtualRowData() {
- const {rowModelType} = this.props;
- const {gridApi} = this.state;
- const virtualRowData = [];
- if (rowModelType === 'clientSide' && gridApi) {
- gridApi.forEachNodeAfterFilterAndSort((node) => {
- if (node.data) {
- virtualRowData.push(node.data);
- }
- });
- }
- return virtualRowData;
- }
+ }, [gridApi]);
- syncRowData() {
- const {rowData} = this.props;
+ const syncRowData = useCallback(() => {
+ const {rowData} = props;
if (rowData) {
- const virtualRowData = this.virtualRowData();
- const newRowData = this.getRowData();
+ const virtualRowDataResult = virtualRowData();
+ const newRowData = getRowData();
if (rowData !== newRowData) {
- this.customSetProps({rowData: newRowData, virtualRowData});
+ customSetProps({
+ rowData: newRowData,
+ virtualRowData: virtualRowDataResult,
+ });
} else {
- this.customSetProps({virtualRowData});
+ customSetProps({virtualRowData: virtualRowDataResult});
}
}
- }
+ }, [props.rowData, virtualRowData, getRowData, customSetProps]);
- onSortChanged() {
- const {rowModelType} = this.props;
+ const onSortChanged = useCallback(() => {
+ const {rowModelType} = props;
const propsToSet = {};
if (rowModelType === 'clientSide') {
- propsToSet.virtualRowData = this.virtualRowData();
+ propsToSet.virtualRowData = virtualRowData();
}
- if (!this.state.gridApi.isDestroyed()) {
+ if (!gridApi.isDestroyed()) {
propsToSet.columnState = JSON.parse(
- JSON.stringify(this.state.gridApi.getColumnState())
+ JSON.stringify(gridApi.getColumnState())
);
}
- this.customSetProps(propsToSet);
- }
+ customSetProps(propsToSet);
+ }, [props.rowModelType, virtualRowData, gridApi, customSetProps]);
- componentDidMount() {
- const {id} = this.props;
- if (id) {
- agGridRefs[id] = this.reference.current;
- eventBus.dispatch(id);
- }
- }
+ const onRowDataUpdated = useCallback(() => {
+ // Handles preserving existing selections when rowData is updated in a callback
+ const {selectedRows, rowData, rowModelType, filterModel} = props;
- componentWillUnmount() {
- this.setState({mounted: false, gridApi: null});
- this.active = false;
- if (this.props.id) {
- delete agGridRefs[this.props.id];
- eventBus.remove(this.props.id);
- }
- }
+ if (gridApi && !gridApi?.isDestroyed()) {
+ dataUpdates.current = true;
+ pauseSelections.current = true;
+ setSelection(selectedRows);
- shouldComponentUpdate(nextProps, nextState) {
- const {gridApi} = this.state;
- const {columnState, filterModel, selectedRows} = nextProps;
+ if (rowData && rowModelType === 'clientSide') {
+ const virtualRowDataResult = virtualRowData();
- if (
- !equals(
- {...omit(OMIT_PROP_RENDER, nextProps)},
- {...omit(OMIT_PROP_RENDER, this.props)}
- ) &&
- (nextProps?.dashRenderType !== 'internal' ||
- !equals(nextProps.rowData, this.props.rowData) ||
- !equals(nextProps.selectedRows, this.props.selectedRows))
- ) {
- return true;
- }
- if (
- !equals(
- {...omit(OMIT_STATE_RENDER, nextState)},
- {...omit(OMIT_STATE_RENDER, this.state)}
- )
- ) {
- return true;
- }
- if (gridApi && !gridApi?.isDestroyed()) {
- if (nextProps?.dashRenderType !== 'internal') {
- if (columnState) {
- if (columnState !== this.props.columnState) {
- return true;
- }
- }
- if (filterModel) {
- if (!equals(filterModel, gridApi.getFilterModel())) {
- return true;
+ customSetProps({virtualRowData: virtualRowDataResult});
+ }
+
+ // When the rowData is updated, reopen any row groups if they previously existed in the table
+ // Iterate through all nodes in the grid. Unfortunately there's no way to iterate through only nodes representing groups
+ if (!isEmpty(openGroups)) {
+ gridApi.forEachNode((node) => {
+ // Check if it's a group row based on whether it has the __hasChildren prop
+ if (node.__hasChildren) {
+ // If the key for the node (i.e. the group name) is the same as an
+ if (openGroups[node.key]) {
+ gridApi.setRowNodeExpanded(node, true);
+ }
}
- }
+ });
}
- if (selectedRows) {
- if (!equals(selectedRows, gridApi.getSelectedRows())) {
- return true;
- }
+ if (!isEmpty(filterModel)) {
+ gridApi.setFilterModel(filterModel);
}
- return false;
+ setTimeout(() => {
+ dataUpdates.current = false;
+ }, 1);
}
- return false;
- }
+ }, [
+ props.selectedRows,
+ props.rowData,
+ props.rowModelType,
+ props.filterModel,
+ gridApi,
+ openGroups,
+ setSelection,
+ virtualRowData,
+ customSetProps,
+ ]);
+
+ const onRowGroupOpened = useCallback((e) => {
+ setOpenGroups((prevOpenGroups) =>
+ e.expanded
+ ? assoc(e.node.key, 1, prevOpenGroups)
+ : omit([e.node.key], prevOpenGroups)
+ );
+ }, []);
- componentDidUpdate(prevProps, prevState) {
- const {
- selectedRows,
- getDetailResponse,
- detailCellRendererParams,
- masterDetail,
- id,
- resetColumnState,
- csvExportParams,
- exportDataAsCsv,
- selectAll,
- deselectAll,
- deleteSelectedRows,
- filterModel,
- columnState,
- columnSize,
- paginationGoTo,
- scrollTo,
- rowTransaction,
- updateColumnState,
- loading_state,
- } = this.props;
+ const onSelectionChanged = useCallback(() => {
+ setTimeout(() => {
+ if (!pauseSelections.current) {
+ const selectedRows = gridApi.getSelectedRows();
+ if (!equals(selectedRows, props.selectedRows)) {
+ // Flag that the selection event was fired
+ selectionEventFired.current = true;
+ customSetProps({selectedRows});
+ }
+ }
+ }, 1);
+ }, [gridApi, props.selectedRows, customSetProps]);
- if (
- this.state.gridApi &&
- (!loading_state || prevProps.loading_state?.is_loading)
- ) {
- if (
- this.props.columnState !== prevProps.columnState &&
- !this.state.columnState_push
- ) {
- this.setState({columnState_push: true});
- }
- }
-
- if (id !== prevProps.id) {
- if (id) {
- agGridRefs[id] = this.reference.current;
- eventBus.dispatch(id);
- }
- if (prevProps.id) {
- delete agGridRefs[prevProps.id];
- eventBus.remove(prevProps.id);
- }
- }
+ const isDatasourceLoadedForInfiniteScrolling = useCallback(() => {
+ return (
+ props.rowModelType === 'infinite' &&
+ getRowsParams.current &&
+ props.getRowsResponse
+ );
+ }, [props.rowModelType, getRowsParams.current, props.getRowsResponse]);
- if (this.state.gridApi && this.state.gridApi !== prevState.gridApi) {
- const propsToSet = {};
- this.updateColumnWidths(false);
+ const getDatasource = useCallback(() => {
+ return {
+ getRows(params) {
+ getRowsParams.current = params;
+ customSetProps({getRowsRequest: params});
+ },
- const groups = {};
- this.state.gridApi.forEachNode((node) => {
- if (node.expanded) {
- groups[node.key] = 1;
+ destroy() {
+ getRowsParams.current = null;
+ },
+ };
+ }, [getRowsParams.current, customSetProps]);
+
+ const applyRowTransaction = useCallback(
+ (data, gridApiParam = gridApi) => {
+ const {selectedRows} = props;
+ if (data.async === false) {
+ gridApiParam.applyTransaction(data);
+ if (selectedRows) {
+ setSelection(selectedRows);
}
- });
-
- if (this.state.rowTransaction) {
- this.state.rowTransaction.map((data) =>
- this.applyRowTransaction(data, this.state.gridApi)
- );
- this.setState({rowTransaction: null});
- this.syncRowData();
- }
-
- // Handles applying selections when a selection was persisted by Dash
- this.setSelection(selectedRows);
-
- if (this.reference.current.props.pagination) {
- this.onPaginationChanged();
+ } else {
+ gridApiParam.applyTransactionAsync(data);
}
+ },
+ [gridApi, props.selectedRows, setSelection]
+ );
- if (!isEmpty(filterModel)) {
- this.state.gridApi.setFilterModel(filterModel);
- }
+ const onGridReady = useCallback(
+ (params) => {
+ // Applying Infinite Row Model
+ // see: https://www.ag-grid.com/javascript-grid/infinite-scrolling/
+ const {rowModelType, eventListeners} = props;
- if (columnState) {
- this.setColumnState();
+ if (rowModelType === 'infinite') {
+ params.api.setGridOption('datasource', getDatasource());
}
- if (paginationGoTo || paginationGoTo === 0) {
- this.paginationGoTo(false);
- propsToSet.paginationGoTo = null;
+ if (eventListeners) {
+ Object.entries(eventListeners).map(([key, v]) => {
+ v.map((func) => {
+ params.api.addEventListener(
+ key,
+ parseFunctionEvent(func)
+ );
+ });
+ });
}
+ setGridApi(params.api);
+ },
+ [
+ props.rowModelType,
+ props.eventListeners,
+ getDatasource,
+ parseFunctionEvent,
+ setGridApi,
+ ]
+ );
- if (scrollTo) {
- this.scrollTo(false);
- propsToSet.scrollTo = null;
- }
+ const onCellClicked = useCallback(
+ ({value, column: {colId}, rowIndex, node}) => {
+ const timestamp = Date.now();
+ customSetProps({
+ cellClicked: {
+ value,
+ colId,
+ rowIndex,
+ rowId: node.id,
+ timestamp,
+ },
+ });
+ },
+ [customSetProps]
+ );
- if (resetColumnState) {
- this.resetColumnState(false);
- propsToSet.resetColumnState = false;
- }
+ const onCellDoubleClicked = useCallback(
+ ({value, column: {colId}, rowIndex, node}) => {
+ const timestamp = Date.now();
+ customSetProps({
+ cellDoubleClicked: {
+ value,
+ colId,
+ rowIndex,
+ rowId: node.id,
+ timestamp,
+ },
+ });
+ },
+ [customSetProps]
+ );
- if (exportDataAsCsv) {
- this.exportDataAsCsv(csvExportParams, false);
- propsToSet.exportDataAsCsv = false;
+ const onCellValueChanged = useCallback(
+ ({oldValue, value, column: {colId}, rowIndex, data, 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 (!pendingCellValueChanges.current) {
+ pendingCellValueChanges.current = [newChange];
+ } else {
+ pendingCellValueChanges.current.push(newChange);
}
+ },
+ [pendingCellValueChanges.current]
+ );
- if (selectAll) {
- this.selectAll(selectAll, false);
- propsToSet.selectAll = false;
- }
+ const afterCellValueChanged = useCallback(() => {
+ // Guard against multiple invocations of the same change session.
+ if (!pendingCellValueChanges.current) {
+ return;
+ }
+ // Send update(s) for current change session to Dash.
+ const virtualRowDataResult = virtualRowData();
+ customSetProps({
+ cellValueChanged: pendingCellValueChanges.current,
+ virtualRowData: virtualRowDataResult,
+ });
+ syncRowData();
+ // Mark current change session as ended.
+ pendingCellValueChanges.current = null;
+ }, [
+ pendingCellValueChanges.current,
+ virtualRowData,
+ customSetProps,
+ syncRowData,
+ ]);
+
+ const updateColumnState = useCallback(() => {
+ if (!gridApi) {
+ return;
+ }
+ if (!gridApi.isDestroyed()) {
+ var columnState = JSON.parse(
+ JSON.stringify(gridApi.getColumnState())
+ );
- if (deselectAll) {
- this.deselectAll(false);
- propsToSet.deselectAll = false;
- }
+ customSetProps({
+ columnState,
+ updateColumnState: false,
+ });
+ } else {
+ customSetProps({
+ updateColumnState: false,
+ });
+ }
+ }, [gridApi, customSetProps]);
- if (deleteSelectedRows) {
- this.deleteSelectedRows(false);
- propsToSet.deleteSelectedRows = false;
+ const updateColumnWidths = useCallback(
+ (setColumns = true) => {
+ const {columnSize, columnSizeOptions} = props;
+ if (gridApi && !gridApi?.isDestroyed()) {
+ const {
+ keys,
+ skipHeader,
+ defaultMinWidth,
+ defaultMaxWidth,
+ columnLimits,
+ } = columnSizeOptions || {};
+ if (columnSize === 'autoSize') {
+ if (keys) {
+ gridApi.autoSizeColumns(keys, skipHeader);
+ } else {
+ gridApi.autoSizeAllColumns(skipHeader);
+ }
+ } else if (
+ columnSize === 'sizeToFit' ||
+ columnSize === 'responsiveSizeToFit'
+ ) {
+ gridApi.sizeColumnsToFit({
+ defaultMinWidth,
+ defaultMaxWidth,
+ columnLimits,
+ });
+ }
+ if (columnSize !== 'responsiveSizeToFit') {
+ customSetProps({columnSize: null});
+ }
+ if (setColumns) {
+ updateColumnState();
+ }
}
+ },
+ [
+ props.columnSize,
+ props.columnSizeOptions,
+ gridApi,
+ customSetProps,
+ updateColumnState,
+ ]
+ );
- if (!isEmpty(propsToSet)) {
- this.customSetProps(propsToSet);
- }
- // Hydrate virtualRowData
- this.onFilterChanged(true);
- this.setState({
- mounted: true,
- openGroups: groups,
- columnState_push: false,
- });
- this.updateColumnState();
+ const onDisplayedColumnsChanged = useCallback(() => {
+ if (props.columnSize === 'responsiveSizeToFit') {
+ updateColumnWidths();
}
+ updateColumnState();
+ }, [props.columnSize, updateColumnWidths, updateColumnState]);
- if (this.isDatasourceLoadedForInfiniteScrolling()) {
- const {rowData, rowCount} = this.props.getRowsResponse;
- this.getRowsParams.successCallback(rowData, rowCount);
- this.customSetProps({getRowsResponse: null});
+ const onColumnResized = useCallback(() => {
+ if (props.columnSize !== 'responsiveSizeToFit') {
+ updateColumnState();
}
+ }, [props.columnSize, updateColumnState]);
- if (
- masterDetail &&
- !detailCellRendererParams.suppressCallback &&
- getDetailResponse
- ) {
- this.getDetailParams.successCallback(getDetailResponse);
- this.customSetProps({getDetailResponse: null});
+ const onGridSizeChanged = useCallback(() => {
+ if (props.columnSize === 'responsiveSizeToFit') {
+ updateColumnWidths();
}
- // Call the API to select rows unless the update was triggered by a selection made in the UI
- if (
- !equals(selectedRows, prevProps.selectedRows) &&
- // eslint-disable-next-line no-undefined
- !(typeof loading_state !== 'undefined'
- ? loading_state && this.selectionEventFired
- : this.selectionEventFired)
- ) {
- if (!this.dataUpdates) {
- setTimeout(() => {
- if (!this.dataUpdates) {
- this.setSelection(selectedRows);
- }
- }, 10);
- }
+ }, [props.columnSize, updateColumnWidths]);
+
+ const setColumnState = useCallback(() => {
+ if (!gridApi || props.updateColumnState) {
+ return;
}
- this.dataUpdates = false;
+ if (columnState_push) {
+ gridApi.applyColumnState({
+ state: props.columnState,
+ applyOrder: true,
+ });
+ setColumnState_push(false);
+ }
+ }, [gridApi, props.updateColumnState, columnState_push]);
- if (this.state.gridApi && this.state.gridApi === prevState.gridApi) {
- if (filterModel) {
- if (this.state.gridApi) {
- if (this.state.gridApi.getFilterModel() !== filterModel) {
- this.state.gridApi.setFilterModel(filterModel);
- }
- }
+ const exportDataAsCsv = useCallback(
+ (csvExportParams, reset = true) => {
+ if (!gridApi) {
+ return;
}
-
- if (paginationGoTo || paginationGoTo === 0) {
- this.paginationGoTo();
+ gridApi.exportDataAsCsv(convertAllProps(csvExportParams));
+ if (reset) {
+ customSetProps({
+ exportDataAsCsv: false,
+ });
}
+ },
+ [gridApi, convertAllProps, customSetProps]
+ );
- if (scrollTo) {
- this.scrollTo();
+ const paginationGoTo = useCallback(
+ (reset = true) => {
+ if (!gridApi) {
+ return;
+ }
+ switch (props.paginationGoTo) {
+ case 'next':
+ gridApi.paginationGoToNextPage();
+ break;
+ case 'previous':
+ gridApi.paginationGoToPreviousPage();
+ break;
+ case 'last':
+ gridApi.paginationGoToLastPage();
+ break;
+ case 'first':
+ gridApi.paginationGoToFirstPage();
+ break;
+ default:
+ gridApi.paginationGoToPage(props.paginationGoTo);
+ }
+ if (reset) {
+ customSetProps({
+ paginationGoTo: null,
+ });
}
+ },
+ [gridApi, props.paginationGoTo, customSetProps]
+ );
- if (columnSize) {
- this.updateColumnWidths();
+ const scrollTo = useCallback(
+ (reset = true) => {
+ const {scrollTo, getRowId} = props;
+ if (!gridApi) {
+ return;
+ }
+ const rowPosition = scrollTo.rowPosition
+ ? scrollTo.rowPosition
+ : 'top';
+ if (scrollTo.rowIndex || scrollTo.rowIndex === 0) {
+ gridApi.ensureIndexVisible(scrollTo.rowIndex, rowPosition);
+ } else if (typeof scrollTo.rowId !== 'undefined') {
+ const node = gridApi.getRowNode(scrollTo.rowId);
+ gridApi.ensureNodeVisible(node, rowPosition);
+ } else if (scrollTo.data) {
+ if (getRowId) {
+ const parsedCondition = esprima.parse(
+ getRowId.replaceAll('params.data.', '')
+ ).body[0].expression;
+ const node = gridApi.getRowNode(
+ evaluate(parsedCondition, scrollTo.data)
+ );
+ gridApi.ensureNodeVisible(node, rowPosition);
+ } else {
+ let scrolled = false;
+ gridApi.forEachNodeAfterFilterAndSort((node) => {
+ if (!scrolled && equals(node.data, scrollTo.data)) {
+ gridApi.ensureNodeVisible(node, rowPosition);
+ scrolled = true;
+ }
+ });
+ }
}
-
- if (resetColumnState) {
- this.resetColumnState();
+ if (scrollTo.column) {
+ const columnPosition = scrollTo.columnPosition
+ ? scrollTo.columnPosition
+ : 'auto';
+ gridApi.ensureColumnVisible(scrollTo.column, columnPosition);
}
-
- if (exportDataAsCsv) {
- this.exportDataAsCsv(csvExportParams);
+ if (reset) {
+ customSetProps({
+ scrollTo: null,
+ });
}
+ },
+ [gridApi, props.scrollTo, props.getRowId, customSetProps]
+ );
- if (selectAll) {
- this.selectAll(selectAll);
+ const resetColumnState = useCallback(
+ (reset = true) => {
+ if (!gridApi) {
+ return;
}
-
- if (deselectAll) {
- this.deselectAll();
+ gridApi.resetColumnState();
+ if (reset) {
+ customSetProps({
+ resetColumnState: false,
+ });
+ updateColumnState();
}
+ },
+ [gridApi, customSetProps, updateColumnState]
+ );
- if (deleteSelectedRows) {
- this.deleteSelectedRows();
+ const selectAll = useCallback(
+ (opts, reset = true) => {
+ if (!gridApi) {
+ return;
}
-
- if (rowTransaction) {
- this.rowTransaction(rowTransaction);
+ if (opts?.filtered) {
+ gridApi.selectAllFiltered();
+ } else {
+ gridApi.selectAll();
}
- if (updateColumnState) {
- this.updateColumnState();
- } else if (this.state.columnState_push) {
- this.setColumnState();
+ if (reset) {
+ customSetProps({
+ selectAll: false,
+ });
}
- }
-
- // Reset selection event flag
- this.selectionEventFired = false;
- }
-
- onRowDataUpdated() {
- // Handles preserving existing selections when rowData is updated in a callback
- const {selectedRows, rowData, rowModelType, filterModel} = this.props;
- const {openGroups, gridApi} = this.state;
-
- if (gridApi && !gridApi?.isDestroyed()) {
- this.dataUpdates = true;
- this.pauseSelections = true;
- this.setSelection(selectedRows);
-
- if (rowData && rowModelType === 'clientSide') {
- const virtualRowData = this.virtualRowData();
+ },
+ [gridApi, customSetProps]
+ );
- this.customSetProps({virtualRowData});
+ const deselectAll = useCallback(
+ (reset = true) => {
+ if (!gridApi) {
+ return;
+ }
+ gridApi.deselectAll();
+ if (reset) {
+ customSetProps({
+ deselectAll: false,
+ });
}
+ },
+ [gridApi, customSetProps]
+ );
- // When the rowData is updated, reopen any row groups if they previously existed in the table
- // Iterate through all nodes in the grid. Unfortunately there's no way to iterate through only nodes representing groups
- if (!isEmpty(openGroups)) {
- gridApi.forEachNode((node) => {
- // Check if it's a group row based on whether it has the __hasChildren prop
- if (node.__hasChildren) {
- // If the key for the node (i.e. the group name) is the same as an
- if (openGroups[node.key]) {
- gridApi.setRowNodeExpanded(node, true);
- }
- }
+ const deleteSelectedRows = useCallback(
+ (reset = true) => {
+ if (!gridApi) {
+ return;
+ }
+ const sel = gridApi.getSelectedRows();
+ gridApi.applyTransaction({remove: sel});
+ if (reset) {
+ customSetProps({
+ deleteSelectedRows: false,
});
+ syncRowData();
}
- if (!isEmpty(filterModel)) {
- gridApi.setFilterModel(filterModel);
+ },
+ [gridApi, customSetProps, syncRowData]
+ );
+
+ const buildArray = useCallback((arr1, arr2) => {
+ if (arr1) {
+ if (!arr1.includes(arr2)) {
+ return [...arr1, arr2];
}
- setTimeout(() => {
- this.dataUpdates = false;
- }, 1);
+ return arr1;
}
- }
-
- onRowGroupOpened(e) {
- this.setState(({openGroups}) => ({
- openGroups: e.expanded
- ? assoc(e.node.key, 1, openGroups)
- : omit([e.node.key], openGroups),
- }));
- }
+ return [JSON.parse(JSON.stringify(arr2))];
+ }, []);
- onSelectionChanged() {
- setTimeout(() => {
- if (!this.pauseSelections) {
- const selectedRows = this.state.gridApi.getSelectedRows();
- if (!equals(selectedRows, this.props.selectedRows)) {
- // Flag that the selection event was fired
- this.selectionEventFired = true;
- this.customSetProps({selectedRows});
+ const rowTransaction = useCallback(
+ (data) => {
+ const rowTransaction = rowTransactionState;
+ if (gridApi && !gridApi?.isDestroyed()) {
+ if (rowTransaction) {
+ rowTransaction.forEach(applyRowTransaction);
+ setRowTransactionState(null);
}
+ applyRowTransaction(data);
+ customSetProps({
+ rowTransaction: null,
+ });
+ syncRowData();
+ } else {
+ setRowTransactionState(
+ rowTransaction
+ ? buildArray(rowTransaction, data)
+ : [JSON.parse(JSON.stringify(data))]
+ );
}
- }, 1);
- }
-
- isDatasourceLoadedForInfiniteScrolling() {
- return (
- this.props.rowModelType === 'infinite' &&
- this.getRowsParams &&
- this.props.getRowsResponse
- );
- }
+ },
+ [
+ rowTransactionState,
+ gridApi,
+ applyRowTransaction,
+ setRowTransactionState,
+ customSetProps,
+ syncRowData,
+ buildArray,
+ ]
+ );
- getDatasource() {
- const self = this;
+ const onAsyncTransactionsFlushed = useCallback(() => {
+ const {selectedRows} = props;
+ if (selectedRows) {
+ setSelection(selectedRows);
+ }
+ syncRowData();
+ }, [props.selectedRows, setSelection, syncRowData]);
- return {
- getRows(params) {
- self.getRowsParams = params;
- self.customSetProps({getRowsRequest: params});
- },
+ // Mount and unmount effect
+ useEffect(() => {
+ const {id} = props;
+ if (id) {
+ agGridRefs[id] = reference.current;
+ eventBus.dispatch(id);
+ }
- destroy() {
- self.getRowsParams = null;
- },
+ return () => {
+ setGridApi(null);
+ active.current = false;
+ if (props.id) {
+ delete agGridRefs[props.id];
+ eventBus.remove(props.id);
+ }
};
- }
+ }, []);
- applyRowTransaction(data, gridApi = this.state.gridApi) {
- const {selectedRows} = this.props;
- if (data.async === false) {
- gridApi.applyTransaction(data);
- if (selectedRows) {
- this.setSelection(selectedRows);
+ useEffect(() => {
+ // Apply selections
+ if (gridApi) {
+ const selectedRows = gridApi.getSelectedRows();
+ if (!equals(selectedRows, props.selectedRows)) {
+ setSelection(props.selectedRows);
}
- } else {
- gridApi.applyTransactionAsync(data);
}
- }
+ }, [props.selectedRows, gridApi]);
- onGridReady(params) {
- // Applying Infinite Row Model
- // see: https://www.ag-grid.com/javascript-grid/infinite-scrolling/
- const {rowModelType, eventListeners} = this.props;
+ // Handle gridApi initialization - basic setup
+ useEffect(() => {
+ if (gridApi && gridApi !== prevGridApi) {
+ updateColumnWidths(false);
- if (rowModelType === 'infinite') {
- params.api.setGridOption('datasource', this.getDatasource());
+ // Handle pagination initialization
+ if (reference.current.props.pagination) {
+ onPaginationChanged();
+ }
}
+ }, [gridApi, prevGridApi, updateColumnWidths, onPaginationChanged]);
- if (eventListeners) {
- Object.entries(eventListeners).map(([key, v]) => {
- v.map((func) => {
- params.api.addEventListener(
- key,
- this.parseFunctionEvent(func)
- );
- });
+ // Handle gridApi initialization - expanded groups tracking
+ useEffect(() => {
+ if (gridApi && gridApi !== prevGridApi) {
+ const groups = {};
+ gridApi.forEachNode((node) => {
+ if (node.expanded) {
+ groups[node.key] = 1;
+ }
});
+ setOpenGroups(groups);
}
+ }, [gridApi, prevGridApi, setOpenGroups]);
- this.setState({
- gridApi: params.api,
- });
- }
-
- onCellClicked({value, column: {colId}, rowIndex, node}) {
- const timestamp = Date.now();
- this.customSetProps({
- cellClicked: {value, colId, rowIndex, rowId: node.id, timestamp},
- });
- }
-
- onCellDoubleClicked({value, column: {colId}, rowIndex, node}) {
- const timestamp = Date.now();
- this.customSetProps({
- cellDoubleClicked: {
- value,
- colId,
- rowIndex,
- rowId: node.id,
- timestamp,
- },
- });
- }
-
- onCellValueChanged({
- oldValue,
- value,
- column: {colId},
- rowIndex,
- data,
- 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.customSetProps({
- cellValueChanged: this.pendingCellValueChanges,
- virtualRowData,
- });
- this.syncRowData();
- // Mark current change session as ended.
- this.pendingCellValueChanges = null;
- }
-
- onDisplayedColumnsChanged() {
- if (this.props.columnSize === 'responsiveSizeToFit') {
- this.updateColumnWidths();
- }
- if (this.state.mounted) {
- this.updateColumnState();
- }
- }
-
- onColumnResized() {
+ // Handle gridApi initialization - row transactions
+ useEffect(() => {
+ if (gridApi && gridApi !== prevGridApi && rowTransactionState) {
+ rowTransactionState.map((data) =>
+ applyRowTransaction(data, gridApi)
+ );
+ setRowTransactionState(null);
+ syncRowData();
+ }
+ }, [
+ gridApi,
+ prevGridApi,
+ rowTransactionState,
+ applyRowTransaction,
+ setRowTransactionState,
+ syncRowData,
+ ]);
+
+ // Handle gridApi initialization - filter model application
+ useEffect(() => {
+ if (gridApi && gridApi !== prevGridApi && !isEmpty(props.filterModel)) {
+ gridApi.setFilterModel(props.filterModel);
+ }
+ }, [gridApi, prevGridApi, props.filterModel]);
+
+ // Handle gridApi initialization - column state application
+ useEffect(() => {
+ if (gridApi && gridApi !== prevGridApi && props.columnState) {
+ setColumnState();
+ }
+ }, [gridApi, prevGridApi, props.columnState, setColumnState]);
+
+ // Handle gridApi initialization - finalization
+ useEffect(() => {
+ if (gridApi && gridApi !== prevGridApi) {
+ // Hydrate virtualRowData and finalize setup
+ onFilterChanged(true);
+ updateColumnState();
+ setColumnState_push(false);
+ }
+ }, [
+ gridApi,
+ prevGridApi,
+ onFilterChanged,
+ setColumnState_push,
+ updateColumnState,
+ ]);
+
+ // Handle columnState push changes
+ useEffect(() => {
if (
- this.state.mounted &&
- this.props.columnSize !== 'responsiveSizeToFit'
+ gridApi &&
+ (!props.loading_state || prevProps?.loading_state?.is_loading)
) {
- this.updateColumnState();
- }
- }
+ const existingColumnState = gridApi.getColumnState();
+ const realStateChange =
+ props.columnState &&
+ !equals(props.columnState, existingColumnState);
- onGridSizeChanged() {
- if (this.props.columnSize === 'responsiveSizeToFit') {
- this.updateColumnWidths();
+ if (realStateChange && !columnState_push) {
+ setColumnState_push(true);
+ }
}
- }
+ }, [props.columnState, props.loading_state, columnState_push]);
- updateColumnWidths(setColumns = true) {
- const {columnSize, columnSizeOptions} = this.props;
- const {gridApi} = this.state;
- if (gridApi && !gridApi?.isDestroyed()) {
- const {
- keys,
- skipHeader,
- defaultMinWidth,
- defaultMaxWidth,
- columnLimits,
- } = columnSizeOptions || {};
- if (columnSize === 'autoSize') {
- if (keys) {
- gridApi.autoSizeColumns(keys, skipHeader);
- } else {
- gridApi.autoSizeAllColumns(skipHeader);
- }
- } else if (
- columnSize === 'sizeToFit' ||
- columnSize === 'responsiveSizeToFit'
- ) {
- gridApi.sizeColumnsToFit({
- defaultMinWidth,
- defaultMaxWidth,
- columnLimits,
- });
- }
- if (columnSize !== 'responsiveSizeToFit') {
- this.customSetProps({columnSize: null});
+ // Handle ID changes
+ useEffect(() => {
+ if (props.id !== prevProps?.id) {
+ if (props.id) {
+ agGridRefs[props.id] = reference.current;
+ eventBus.dispatch(props.id);
}
- if (setColumns) {
- this.updateColumnState();
+ if (prevProps?.id) {
+ delete agGridRefs[prevProps.id];
+ eventBus.remove(prevProps.id);
}
}
- }
+ }, [props.id]);
- parseFunction = memoizeWith(String, (funcString) => {
- const parsedCondition = esprima.parse(funcString).body[0].expression;
- const context = {
- d3,
- dash_clientside,
- ...customFunctions,
- ...window.dashAgGridFunctions,
- };
- return (params) => evaluate(parsedCondition, {params, ...context});
- });
-
- parseFunctionEvent = memoizeWith(String, (funcString) => {
- const parsedCondition = esprima.parse(funcString).body[0].expression;
- const context = {
- d3,
- dash_clientside,
- ...customFunctions,
- ...window.dashAgGridFunctions,
- setGridProps: this.customSetProps,
- setEventData: this.setEventData,
- };
- return (params) => evaluate(parsedCondition, {params, ...context});
- });
-
- parseFunctionNoParams = memoizeWith(String, (funcString) => {
- const parsedCondition = esprima.parse(funcString).body[0].expression;
- const context = {
- d3,
- ...customFunctions,
- ...window.dashAgGridFunctions,
- };
- return evaluate(parsedCondition, context);
- });
-
- /**
- * @params AG-Grid Styles rules attribute.
- * Cells: https://www.ag-grid.com/react-grid/cell-styles/#cell-style-cell-class--cell-class-rules-params
- * Rows: https://www.ag-grid.com/react-grid/row-styles/#row-style-row-class--row-class-rules-params
- */
- handleDynamicStyle(cellStyle) {
- const {styleConditions, defaultStyle} = cellStyle;
- const _defaultStyle = defaultStyle || null;
-
- if (styleConditions && styleConditions.length) {
- const tests = styleConditions.map(({condition, style}) => ({
- test: this.parseFunction(condition),
- style,
- }));
- return (params) => {
- for (const {test, style} of tests) {
- if (params) {
- if (params.node.id && params.node.id !== null) {
- if (test(params)) {
- return style;
- }
- }
- }
- }
- return _defaultStyle;
- };
+ // Handle infinite scrolling datasource
+ useEffect(() => {
+ if (isDatasourceLoadedForInfiniteScrolling()) {
+ const {rowData, rowCount} = props.getRowsResponse;
+ getRowsParams.current.successCallback(rowData, rowCount);
+ customSetProps({getRowsResponse: null});
}
+ }, [props.getRowsResponse]);
- return _defaultStyle;
- }
-
- generateRenderer(Renderer) {
- const {dangerously_allow_code} = this.props;
-
- return (props) => (
- {
- this.customSetProps({
- cellRendererData: {
- value,
- colId: props.column.colId,
- rowIndex: props.node.sourceRowIndex,
- rowId: props.node.id,
- timestamp: Date.now(),
- },
- });
- }}
- dangerously_allow_code={dangerously_allow_code}
- {...props}
- >
- );
- }
-
- setColumnState() {
- if (!this.state.gridApi || this.props.updateColumnState) {
- return;
- }
-
- if (this.state.columnState_push) {
- this.state.gridApi.applyColumnState({
- state: this.props.columnState,
- applyOrder: true,
- });
- this.setState({columnState_push: false});
- }
- }
+ // Handle master detail response
+ useEffect(() => {
+ if (
+ props.masterDetail &&
+ !props.detailCellRendererParams.suppressCallback &&
+ props.getDetailResponse
+ ) {
+ getDetailParams.current.successCallback(props.getDetailResponse);
+ customSetProps({getDetailResponse: null});
+ }
+ }, [
+ props.getDetailResponse,
+ props.masterDetail,
+ props.detailCellRendererParams,
+ ]);
+
+ // Handle dataUpdates reset
+ useEffect(() => {
+ dataUpdates.current = false;
+ });
- // Event actions that reset
- exportDataAsCsv(csvExportParams, reset = true) {
- if (!this.state.gridApi) {
- return;
- }
- this.state.gridApi.exportDataAsCsv(
- this.convertAllProps(csvExportParams)
- );
- if (reset) {
- this.customSetProps({
- exportDataAsCsv: false,
- });
+ // Handle filter model updates
+ useEffect(() => {
+ if (
+ gridApi &&
+ gridApi === prevGridApi &&
+ props.filterModel &&
+ gridApi.getFilterModel() !== props.filterModel
+ ) {
+ gridApi.setFilterModel(props.filterModel);
}
- }
+ }, [props.filterModel, gridApi, prevGridApi]);
- paginationGoTo(reset = true) {
- const {gridApi} = this.state;
- if (!gridApi) {
- return;
- }
- switch (this.props.paginationGoTo) {
- case 'next':
- gridApi.paginationGoToNextPage();
- break;
- case 'previous':
- gridApi.paginationGoToPreviousPage();
- break;
- case 'last':
- gridApi.paginationGoToLastPage();
- break;
- case 'first':
- gridApi.paginationGoToFirstPage();
- break;
- default:
- gridApi.paginationGoToPage(this.props.paginationGoTo);
- }
- if (reset) {
- this.customSetProps({
- paginationGoTo: null,
- });
- }
- }
+ // Handle pagination actions
+ useEffect(() => {
+ if (
+ gridApi &&
+ gridApi === prevGridApi &&
+ (props.paginationGoTo || props.paginationGoTo === 0)
+ ) {
+ paginationGoTo();
+ }
+ }, [props.paginationGoTo, gridApi, prevGridApi, paginationGoTo]);
+
+ // Handle scroll actions
+ useEffect(() => {
+ if (gridApi && props.scrollTo) {
+ scrollTo();
+ }
+ }, [props.scrollTo, gridApi, prevGridApi, scrollTo]);
+
+ // Handle column size updates
+ useEffect(() => {
+ if (gridApi && props.columnSize) {
+ updateColumnWidths();
+ }
+ }, [props.columnSize, gridApi, prevGridApi, updateColumnWidths]);
+
+ // Handle column state reset
+ useEffect(() => {
+ if (gridApi && props.resetColumnState) {
+ resetColumnState();
+ }
+ }, [props.resetColumnState, gridApi, prevGridApi, resetColumnState]);
+
+ // Handle CSV export
+ useEffect(() => {
+ if (gridApi && props.exportDataAsCsv) {
+ exportDataAsCsv(props.csvExportParams);
+ }
+ }, [
+ props.exportDataAsCsv,
+ props.csvExportParams,
+ gridApi,
+ prevGridApi,
+ exportDataAsCsv,
+ ]);
+
+ // Handle row selection actions
+ useEffect(() => {
+ if (gridApi) {
+ if (props.selectAll) {
+ selectAll(props.selectAll);
+ }
+ if (props.deselectAll) {
+ deselectAll();
+ }
+ if (props.deleteSelectedRows) {
+ deleteSelectedRows();
+ }
+ }
+ }, [
+ props.selectAll,
+ props.deselectAll,
+ props.deleteSelectedRows,
+ gridApi,
+ prevGridApi,
+ selectAll,
+ deselectAll,
+ deleteSelectedRows,
+ ]);
+
+ // Handle row transactions
+ useEffect(() => {
+ if (gridApi && props.rowTransaction) {
+ rowTransaction(props.rowTransaction);
+ }
+ }, [props.rowTransaction, gridApi, prevGridApi, rowTransaction]);
+
+ // Handle column state updates
+ useEffect(() => {
+ if (gridApi) {
+ if (props.updateColumnState) {
+ updateColumnState();
+ } else if (columnState_push) {
+ setColumnState();
+ }
+ }
+ }, [
+ props.updateColumnState,
+ columnState_push,
+ gridApi,
+ prevGridApi,
+ updateColumnState,
+ setColumnState,
+ ]);
+
+ const {id, style, className, dashGridOptions, ...restProps} = props;
+ const passingProps = pick(PASSTHRU_PROPS, restProps);
+ const convertedProps = convertAllProps(
+ omit(NO_CONVERT_PROPS, {...dashGridOptions, ...restProps})
+ );
- scrollTo(reset = true) {
- const {gridApi} = this.state;
- const {scrollTo, getRowId} = this.props;
- if (!gridApi) {
- return;
- }
- const rowPosition = scrollTo.rowPosition ? scrollTo.rowPosition : 'top';
- if (scrollTo.rowIndex || scrollTo.rowIndex === 0) {
- gridApi.ensureIndexVisible(scrollTo.rowIndex, rowPosition);
- } else if (typeof scrollTo.rowId !== 'undefined') {
- const node = gridApi.getRowNode(scrollTo.rowId);
- gridApi.ensureNodeVisible(node, rowPosition);
- } else if (scrollTo.data) {
- if (getRowId) {
- const parsedCondition = esprima.parse(
- getRowId.replaceAll('params.data.', '')
- ).body[0].expression;
- const node = gridApi.getRowNode(
- evaluate(parsedCondition, scrollTo.data)
- );
- gridApi.ensureNodeVisible(node, rowPosition);
- } else {
- let scrolled = false;
- gridApi.forEachNodeAfterFilterAndSort((node) => {
- if (!scrolled && equals(node.data, scrollTo.data)) {
- gridApi.ensureNodeVisible(node, rowPosition);
- scrolled = true;
- }
+ let alignedGrids;
+ if (dashGridOptions) {
+ if ('alignedGrids' in dashGridOptions) {
+ alignedGrids = [];
+ const addGrid = (id) => {
+ const strId = stringifyId(id);
+ eventBus.on(props.id, strId, () => {
+ forceRerender({});
});
- }
- }
- if (scrollTo.column) {
- const columnPosition = scrollTo.columnPosition
- ? scrollTo.columnPosition
- : 'auto';
- gridApi.ensureColumnVisible(scrollTo.column, columnPosition);
- }
- if (reset) {
- this.customSetProps({
- scrollTo: null,
- });
- }
- }
-
- resetColumnState(reset = true) {
- if (!this.state.gridApi) {
- return;
- }
- this.state.gridApi.resetColumnState();
- if (reset) {
- this.customSetProps({
- resetColumnState: false,
- });
- this.updateColumnState();
- }
- }
-
- selectAll(opts, reset = true) {
- if (!this.state.gridApi) {
- return;
- }
- if (opts?.filtered) {
- this.state.gridApi.selectAllFiltered();
- } else {
- this.state.gridApi.selectAll();
- }
- if (reset) {
- this.customSetProps({
- selectAll: false,
- });
- }
- }
-
- deselectAll(reset = true) {
- if (!this.state.gridApi) {
- return;
- }
- this.state.gridApi.deselectAll();
- if (reset) {
- this.customSetProps({
- deselectAll: false,
- });
- }
- }
-
- deleteSelectedRows(reset = true) {
- if (!this.state.gridApi) {
- return;
- }
- const sel = this.state.gridApi.getSelectedRows();
- this.state.gridApi.applyTransaction({remove: sel});
- if (reset) {
- this.customSetProps({
- deleteSelectedRows: false,
- });
- this.syncRowData();
- }
- }
-
- // end event actions
-
- updateColumnState() {
- if (!this.state.gridApi || !this.state.mounted) {
- return;
- }
- if (!this.state.gridApi.isDestroyed()) {
- var columnState = JSON.parse(
- JSON.stringify(this.state.gridApi.getColumnState())
- );
-
- this.customSetProps({
- columnState,
- updateColumnState: false,
- });
- } else {
- this.customSetProps({
- updateColumnState: false,
- });
- }
- }
-
- buildArray(arr1, arr2) {
- if (arr1) {
- if (!arr1.includes(arr2)) {
- return [...arr1, arr2];
- }
- return arr1;
- }
- return [JSON.parse(JSON.stringify(arr2))];
- }
-
- rowTransaction(data) {
- const {rowTransaction, gridApi, mounted} = this.state;
- if (mounted) {
- if (gridApi && !gridApi?.isDestroyed()) {
- if (rowTransaction) {
- rowTransaction.forEach(this.applyRowTransaction);
- this.setState({rowTransaction: null});
+ if (!agGridRefs[strId]) {
+ agGridRefs[strId] = {api: null};
}
- this.applyRowTransaction(data);
- this.customSetProps({
- rowTransaction: null,
- });
- this.syncRowData();
+ alignedGrids.push(agGridRefs[strId]);
+ };
+ eventBus.remove(props.id);
+ if (Array.isArray(dashGridOptions.alignedGrids)) {
+ dashGridOptions.alignedGrids.map(addGrid);
} else {
- this.setState({
- rowTransaction: rowTransaction
- ? this.buildArray(rowTransaction, data)
- : [JSON.parse(JSON.stringify(data))],
- });
+ addGrid(dashGridOptions.alignedGrids);
}
}
}
- onAsyncTransactionsFlushed() {
- const {selectedRows} = this.props;
- if (selectedRows) {
- this.setSelection(selectedRows);
- }
- this.syncRowData();
- }
-
- render() {
- const {id, style, className, dashGridOptions, ...restProps} =
- this.props;
-
- const passingProps = pick(PASSTHRU_PROPS, restProps);
-
- const convertedProps = this.convertAllProps(
- omit(NO_CONVERT_PROPS, {...dashGridOptions, ...restProps})
- );
-
- let alignedGrids;
- if (dashGridOptions) {
- if ('alignedGrids' in dashGridOptions) {
- alignedGrids = [];
- const addGrid = (id) => {
- const strId = stringifyId(id);
- eventBus.on(this.props.id, strId, () => {
- this.setState(({rerender}) => ({
- rerender: rerender + 1,
- }));
- });
- if (!agGridRefs[strId]) {
- agGridRefs[strId] = {api: null};
- }
- alignedGrids.push(agGridRefs[strId]);
- };
- eventBus.remove(this.props.id);
- if (Array.isArray(dashGridOptions.alignedGrids)) {
- dashGridOptions.alignedGrids.map(addGrid);
- } else {
- addGrid(dashGridOptions.alignedGrids);
- }
- }
- }
-
- return (
-
- );
- }
+ return (
+
+ );
}
DashAgGrid.defaultProps = _defaultProps;
@@ -1505,3 +1610,28 @@ export const defaultProps = DashAgGrid.defaultProps;
var dagfuncs = (window.dash_ag_grid = window.dash_ag_grid || {});
dagfuncs.useGridFilter = useGridFilter;
+
+const MemoizedAgGrid = React.memo(DashAgGrid, (prevProps, nextProps) => {
+ // Check if props are equal (excluding render-specific props)
+ const relevantNextProps = {...omit(OMIT_PROP_RENDER, nextProps)};
+ const relevantPrevProps = {...omit(OMIT_PROP_RENDER, prevProps)};
+
+ const isInternalChange = nextProps?.dashRenderType === 'internal';
+ const propsHaveChanged = !equals(relevantNextProps, relevantPrevProps);
+ const rowDataChanged = !equals(nextProps.rowData, prevProps.rowData);
+ const selectedRowsChanged = !equals(
+ nextProps.selectedRows,
+ prevProps.selectedRows
+ );
+
+ if (
+ propsHaveChanged &&
+ (!isInternalChange || rowDataChanged || selectedRowsChanged)
+ ) {
+ return false; // Props changed, re-render
+ }
+
+ return true;
+});
+
+export default MemoizedAgGrid;
diff --git a/src/lib/fragments/AgGridEnterprise.react.js b/src/lib/fragments/AgGridEnterprise.react.js
index d1ef1a1f..3df1e599 100644
--- a/src/lib/fragments/AgGridEnterprise.react.js
+++ b/src/lib/fragments/AgGridEnterprise.react.js
@@ -1,15 +1,13 @@
-import React, {Component} from 'react';
+import React from 'react';
import {LicenseManager} from 'ag-grid-enterprise';
-import DashAgGrid, {propTypes} from './AgGrid.react';
+import MemoizedAgGrid, {propTypes} from './AgGrid.react';
-export default class DashAgGridEnterprise extends Component {
- render() {
- const {licenseKey} = this.props;
- if (licenseKey) {
- LicenseManager.setLicenseKey(licenseKey);
- }
- return ;
+export default function DashAgGridEnterprise(props) {
+ const {licenseKey} = props;
+ if (licenseKey) {
+ LicenseManager.setLicenseKey(licenseKey);
}
+ return ;
}
DashAgGridEnterprise.propTypes = propTypes;
diff --git a/src/lib/utils/propCategories.js b/src/lib/utils/propCategories.js
index e6995f85..64f6c511 100644
--- a/src/lib/utils/propCategories.js
+++ b/src/lib/utils/propCategories.js
@@ -317,10 +317,12 @@ export const PASSTHRU_PROPS = ['rowData'];
* in the render() method, so they don't need to be listed here
*/
export const PROPS_NOT_FOR_AG_GRID = [
+ 'children',
'setProps',
'loading_state',
'enableEnterpriseModules',
'parentState',
+ 'persistence',
'persisted_props',
'persistence_type',
'virtualRowData',
@@ -335,6 +337,7 @@ export const PROPS_NOT_FOR_AG_GRID = [
'alignedGrids',
'resetColumnState',
'exportDataAsCsv',
+ 'selectedRows',
'selectAll',
'deselectAll',
'deleteSelectedRows',
@@ -357,7 +360,6 @@ export const OMIT_PROP_RENDER = [
'virtualRowData',
'columnState',
'filterModel',
- 'selectedRows',
'getRowRequest',
'getDetailRequest',
'cellValueChanged',
diff --git a/tests/test_column_state.py b/tests/test_column_state.py
index c771d6d8..5b573b0b 100644
--- a/tests/test_column_state.py
+++ b/tests/test_column_state.py
@@ -247,6 +247,9 @@ def loadState(n):
)
dash_duo.find_element("#load-column-defs").click()
+
+ time.sleep(0.5) # pausing to emulate separation because user inputs
+
until(
lambda: json.dumps(alt_colState)
in dash_duo.find_element("#reset-column-state-grid-pre").text,
diff --git a/tests/test_event_listeners.py b/tests/test_event_listeners.py
index 74634fc8..70bac7f8 100644
--- a/tests/test_event_listeners.py
+++ b/tests/test_event_listeners.py
@@ -5,6 +5,7 @@
import json
from selenium.webdriver.common.by import By
from dash.testing.wait import until
+import time
df = px.data.medals_wide()
@@ -52,7 +53,7 @@ def test_el001_event_listener(dash_duo):
# Test left click.
grid.get_cell(1, 2).click()
- until(lambda: json.loads(dash_duo.find_element('#log').text).get('value') == 15, timeout=3)
+ until(lambda: json.loads(dash_duo.find_element('#log').text or "{}").get('value') == 15, timeout=3)
# Test right click
action = utils.ActionChains(dash_duo.driver)
diff --git a/tests/test_pagination.py b/tests/test_pagination.py
index e6f1e9be..fd3d8bb0 100644
--- a/tests/test_pagination.py
+++ b/tests/test_pagination.py
@@ -4,6 +4,7 @@
import pandas as pd
import json
from dash.testing.wait import until
+import time
from . import utils
@@ -74,6 +75,7 @@ def updatePage(_):
grid = utils.Grid(dash_duo, "grid")
+ time.sleep(1) # wait for the grid to load
until(lambda: "Australia" == grid.get_cell(500, 0).text, timeout=3)
oldValue = '{"isLastPageFound": true, "pageSize": 100, "currentPage": 5, "totalPages": 87, "rowCount": 8618}'
until(lambda: oldValue == dash_duo.find_element("#grid-info").text, timeout=3)
diff --git a/tests/test_scroll_to.py b/tests/test_scroll_to.py
index 052207a6..4e8546e5 100644
--- a/tests/test_scroll_to.py
+++ b/tests/test_scroll_to.py
@@ -122,6 +122,7 @@ def reset_columnState(n, s):
def update_scrollTo(n_clicks):
return scroll_to_inputs[n_clicks - 1]
+ dash_duo.driver.set_window_size(800, 600) # Make window small enough to scroll things
dash_duo.start_server(app)
grid = utils.Grid(dash_duo, "grid")
@@ -230,6 +231,7 @@ def reset_columnState(n, s):
state[0]['width'] = s[0]['width'] - n + 1
return state
+ dash_duo.driver.set_window_size(800, 600) # Make window small enough to scroll things
dash_duo.start_server(app)
grid = utils.Grid(dash_duo, "grid")
diff --git a/tests/test_sizing_buttons.py b/tests/test_sizing_buttons.py
index 4737c560..94339025 100644
--- a/tests/test_sizing_buttons.py
+++ b/tests/test_sizing_buttons.py
@@ -448,6 +448,7 @@ def selected(state):
assert oldValue == dash_duo.find_element("#columnState").get_attribute(
"innerText"
)
+ time.sleep(.2) # allow window size to change
dash_duo.find_element(f"#{x}").click()
until(
lambda: oldValue
@@ -457,4 +458,4 @@ def selected(state):
oldValue = dash_duo.find_element("#columnState").text
dash_duo.driver.set_window_size(1000, 1000)
- time.sleep(.2)
+ time.sleep(.2) # allow oldValue to change to the bigger size