diff --git a/docs/pages/api-docs/data-grid/data-grid.md b/docs/pages/api-docs/data-grid/data-grid.md
index 4dcb3d089dd5..302382efbf16 100644
--- a/docs/pages/api-docs/data-grid/data-grid.md
+++ b/docs/pages/api-docs/data-grid/data-grid.md
@@ -82,7 +82,6 @@ import { DataGrid } from '@material-ui/data-grid';
| onRowOut | (param: GridRowParams, event: React.MouseEvent) => void | | Callback fired when a mouse out comes from a row container element. |
| onRowEnter | (param: GridRowParams, event: React.MouseEvent) => void | | Callback fired when a mouse enter comes from a row container element. |
| onRowLeave | (param: GridRowParams, event: React.MouseEvent) => void | | Callback fired when a mouse leave event comes from a row container element. |
-| onRowSelected | (param: GridRowSelectedParams) => void | | Callback fired when one row is selected. |
| onSelectionModelChange | (param: GridSelectionModelChangeParams) => void | | Callback fired when the selection state of one or multiple rows changes. |
| onSortModelChange | (param: GridSortModelParams) => void | | Callback fired when the sort model changes before a column is sorted. |
| page | number | 1 | Set the current page. |
diff --git a/docs/pages/api-docs/data-grid/x-grid.md b/docs/pages/api-docs/data-grid/x-grid.md
index 6c76ccad289f..d701450049cb 100644
--- a/docs/pages/api-docs/data-grid/x-grid.md
+++ b/docs/pages/api-docs/data-grid/x-grid.md
@@ -87,7 +87,6 @@ import { XGrid } from '@material-ui/x-grid';
| onRowOut | (param: GridRowParams, event: React.MouseEvent) => void | | Callback fired when a mouse out comes from a row container element. |
| onRowEnter | (param: GridRowParams, event: React.MouseEvent) => void | | Callback fired when a mouse enter comes from a row container element. |
| onRowLeave | (param: GridRowParams, event: React.MouseEvent) => void | | Callback fired when a mouse leave event comes from a row container element. |
-| onRowSelected | (param: GridRowSelectedParams) => void | | Callback fired when one row is selected. |
| onRowsScrollEnd | (param: GridRowScrollEndParams) => void | | Callback fired when scrolling to the bottom of the grid viewport. |
| onSelectionModelChange | (param: GridSelectionModelChangeParams) => void | | Callback fired when the selection state of one or multiple rows changes. |
| onSortModelChange | (param: GridSortModelParams) => void | | Callback fired when the sort model changes before a column is sorted. |
diff --git a/docs/src/pages/components/data-grid/events/events.json b/docs/src/pages/components/data-grid/events/events.json
index 88f428ffaa6c..95ede2d26296 100644
--- a/docs/src/pages/components/data-grid/events/events.json
+++ b/docs/src/pages/components/data-grid/events/events.json
@@ -109,10 +109,6 @@
"name": "editRowModelChange",
"description": "Fired when the row editing model changes. Called with a GridEditRowModelParams object."
},
- {
- "name": "rowSelected",
- "description": "Fired when a row is selected or unselected. Called with a GridRowSelectedParams object."
- },
{
"name": "columnHeaderKeyDown",
"description": "Fired when a key is pressed in a column header. It's mapped do the keydown
DOM event.\nCalled with a GridColumnHeaderParams object."
diff --git a/docs/src/pages/components/data-grid/selection/ControlledSelectionGrid.js b/docs/src/pages/components/data-grid/selection/ControlledSelectionGrid.js
index ec9f126d0efc..b00cf2f8357f 100644
--- a/docs/src/pages/components/data-grid/selection/ControlledSelectionGrid.js
+++ b/docs/src/pages/components/data-grid/selection/ControlledSelectionGrid.js
@@ -15,8 +15,8 @@ export default function ControlledSelectionGrid() {
{
- setSelectionModel(newSelection.selectionModel);
+ onSelectionModelChange={(newSelectionModel) => {
+ setSelectionModel(newSelectionModel);
}}
selectionModel={selectionModel}
{...data}
diff --git a/docs/src/pages/components/data-grid/selection/ControlledSelectionGrid.tsx b/docs/src/pages/components/data-grid/selection/ControlledSelectionGrid.tsx
index 4594a7004a68..a2be7dc406fd 100644
--- a/docs/src/pages/components/data-grid/selection/ControlledSelectionGrid.tsx
+++ b/docs/src/pages/components/data-grid/selection/ControlledSelectionGrid.tsx
@@ -15,8 +15,8 @@ export default function ControlledSelectionGrid() {
{
- setSelectionModel(newSelection.selectionModel);
+ onSelectionModelChange={(newSelectionModel) => {
+ setSelectionModel(newSelectionModel);
}}
selectionModel={selectionModel}
{...data}
diff --git a/packages/grid/_modules_/grid/components/GridViewport.tsx b/packages/grid/_modules_/grid/components/GridViewport.tsx
index 0ed7f2e839a6..e8ddabf3d845 100644
--- a/packages/grid/_modules_/grid/components/GridViewport.tsx
+++ b/packages/grid/_modules_/grid/components/GridViewport.tsx
@@ -8,7 +8,7 @@ import {
gridTabIndexCellSelector,
} from '../hooks/features/focus/gridFocusStateSelector';
import { gridEditRowsStateSelector } from '../hooks/features/rows/gridEditRowsSelector';
-import { gridSelectionStateSelector } from '../hooks/features/selection/gridSelectionSelector';
+import { selectedIdsLookupSelector } from '../hooks/features/selection/gridSelectionSelector';
import { renderStateSelector } from '../hooks/features/virtualization/renderingStateSelector';
import { optionsSelector } from '../hooks/utils/optionsSelector';
import { useGridApiContext } from '../hooks/root/useGridApiContext';
@@ -38,7 +38,7 @@ export const GridViewport: ViewportType = React.forwardRef(
const renderState = useGridSelector(apiRef, renderStateSelector);
const cellFocus = useGridSelector(apiRef, gridFocusCellSelector);
const cellTabIndex = useGridSelector(apiRef, gridTabIndexCellSelector);
- const selectionState = useGridSelector(apiRef, gridSelectionStateSelector);
+ const selectionLookup = useGridSelector(apiRef, selectedIdsLookupSelector);
const rows = useGridSelector(apiRef, visibleSortedGridRowsAsArraySelector);
const rowHeight = useGridSelector(apiRef, gridDensityRowHeightSelector);
const editRowsState = useGridSelector(apiRef, gridEditRowsStateSelector);
@@ -59,7 +59,7 @@ export const GridViewport: ViewportType = React.forwardRef(
}
key={id}
id={id}
- selected={selectionState[id] !== undefined}
+ selected={selectionLookup[id] !== undefined}
rowIndex={renderState.renderContext!.firstRowIdx! + idx}
>
@@ -77,7 +77,7 @@ export const GridViewport: ViewportType = React.forwardRef(
rowIndex={renderState.renderContext!.firstRowIdx! + idx}
cellFocus={cellFocus}
cellTabIndex={cellTabIndex}
- isSelected={selectionState[id] !== undefined}
+ isSelected={selectionLookup[id] !== undefined}
editRowState={editRowsState[id]}
cellClassName={options.classes?.cell}
getCellClassName={options.getCellClassName}
diff --git a/packages/grid/_modules_/grid/constants/eventsConstants.ts b/packages/grid/_modules_/grid/constants/eventsConstants.ts
index 9d7bedc4f5d0..675bab2623c3 100644
--- a/packages/grid/_modules_/grid/constants/eventsConstants.ts
+++ b/packages/grid/_modules_/grid/constants/eventsConstants.ts
@@ -225,12 +225,6 @@ export const GRID_ROW_LEAVE = 'rowLeave';
*/
export const GRID_ROW_EDIT_MODEL_CHANGE = 'editRowModelChange';
-/**
- * Fired when a row is selected or unselected. Called with a [[GridRowSelectedParams]] object.
- * @event
- */
-export const GRID_ROW_SELECTED = 'rowSelected';
-
/**
* Fired when a column header loses focus. Called with a [[GridColumnHeaderParams]] object.
* @ignore - do not document.
diff --git a/packages/grid/_modules_/grid/hooks/features/clipboard/useGridClipboard.ts b/packages/grid/_modules_/grid/hooks/features/clipboard/useGridClipboard.ts
index dc49737ab5df..3f7afe034033 100644
--- a/packages/grid/_modules_/grid/hooks/features/clipboard/useGridClipboard.ts
+++ b/packages/grid/_modules_/grid/hooks/features/clipboard/useGridClipboard.ts
@@ -48,6 +48,7 @@ export const useGridClipboard = (apiRef: GridApiRef): void => {
const data = buildCSV({
columns: visibleColumns,
rows: selectedRows,
+ selectedRowIds: [],
includeHeaders,
getCellParams: apiRef.current.getCellParams,
delimiterCharacter: '\t',
diff --git a/packages/grid/_modules_/grid/hooks/features/core/gridState.ts b/packages/grid/_modules_/grid/hooks/features/core/gridState.ts
index 4bb5d512c797..1474d1683502 100644
--- a/packages/grid/_modules_/grid/hooks/features/core/gridState.ts
+++ b/packages/grid/_modules_/grid/hooks/features/core/gridState.ts
@@ -79,7 +79,7 @@ export const getInitialGridState: () => GridState = () => ({
sorting: getInitialGridSortingState(),
focus: { cell: null, columnHeader: null },
tabIndex: { cell: null, columnHeader: null },
- selection: {},
+ selection: [],
filter: getInitialGridFilterState(),
columnMenu: { open: false },
preferencePanel: { open: false },
diff --git a/packages/grid/_modules_/grid/hooks/features/core/index.ts b/packages/grid/_modules_/grid/hooks/features/core/index.ts
index 6a14d3632bbc..367a02e51bbe 100644
--- a/packages/grid/_modules_/grid/hooks/features/core/index.ts
+++ b/packages/grid/_modules_/grid/hooks/features/core/index.ts
@@ -1,5 +1,6 @@
export * from './gridState';
export * from './useGridApi';
+export * from './useGridControlState';
export * from './useGridReducer';
export * from './useGridSelector';
export * from './useGridState';
diff --git a/packages/grid/_modules_/grid/hooks/features/core/useGridControlState.ts b/packages/grid/_modules_/grid/hooks/features/core/useGridControlState.ts
new file mode 100644
index 000000000000..6fe8396dcd0b
--- /dev/null
+++ b/packages/grid/_modules_/grid/hooks/features/core/useGridControlState.ts
@@ -0,0 +1,78 @@
+import React from 'react';
+import { GridApiRef } from '../../../models/api/gridApiRef';
+import { GridControlStateApi } from '../../../models/api/gridControlStateApi';
+import { ControlStateItem } from '../../../models/controlStateItem';
+import { useGridApiMethod } from '../../root/useGridApiMethod';
+
+export function useGridControlState(apiRef: GridApiRef) {
+ const controlStateMapRef = React.useRef>>({});
+
+ const updateControlState = React.useCallback((controlStateItem: ControlStateItem) => {
+ const { stateId, stateSelector, ...others } = controlStateItem;
+
+ controlStateMapRef.current[stateId] = {
+ ...others,
+ stateId,
+ stateSelector: !stateSelector ? (state) => state[stateId] : stateSelector,
+ };
+ }, []);
+
+ const applyControlStateConstraint = React.useCallback(
+ (newState) => {
+ let shouldUpdate = true;
+ const updatedStateIds: string[] = [];
+ const controlStateMap = controlStateMapRef.current!;
+
+ Object.keys(controlStateMap).forEach((stateId) => {
+ const controlState = controlStateMap[stateId];
+ const oldState = controlState.stateSelector(apiRef.current.state);
+ const newSubState = controlState.stateSelector(newState);
+ const hasSubStateChanged = oldState !== newSubState;
+
+ if (updatedStateIds.length >= 1 && hasSubStateChanged) {
+ // Each hook modify its own state and it should not leak
+ // Events are here to forward to other hooks and apply changes.
+ // You are trying to update several states in a no isolated way.
+ throw new Error(
+ `You're not allowed to update several sub-state in one transaction. You already updated ${updatedStateIds[0]}, therefore, you're not allowed to update ${controlState.stateId} in the same transaction.`,
+ );
+ }
+
+ if (hasSubStateChanged) {
+ if (controlState.propOnChange) {
+ const newModel = newSubState;
+ if (controlState.propModel !== newModel) {
+ controlState.propOnChange(newModel);
+ }
+ shouldUpdate =
+ controlState.propModel === undefined || controlState.propModel === newModel;
+ } else if (controlState.propModel !== undefined) {
+ shouldUpdate = oldState !== controlState.propModel;
+ }
+ if (shouldUpdate) {
+ updatedStateIds.push(controlState.stateId);
+ }
+ }
+ });
+
+ return {
+ shouldUpdate,
+ postUpdate: () => {
+ updatedStateIds.forEach((stateId) => {
+ if (controlStateMap[stateId].onChangeCallback) {
+ const model = controlStateMap[stateId].stateSelector(newState);
+ controlStateMap[stateId].onChangeCallback!(model);
+ }
+ });
+ },
+ };
+ },
+ [apiRef],
+ );
+
+ const controlStateApi: GridControlStateApi = {
+ updateControlState,
+ applyControlStateConstraint,
+ };
+ useGridApiMethod(apiRef, controlStateApi, 'controlStateApi');
+}
diff --git a/packages/grid/_modules_/grid/hooks/features/core/useGridState.ts b/packages/grid/_modules_/grid/hooks/features/core/useGridState.ts
index aca1d7efcb79..57aecabcce99 100644
--- a/packages/grid/_modules_/grid/hooks/features/core/useGridState.ts
+++ b/packages/grid/_modules_/grid/hooks/features/core/useGridState.ts
@@ -9,23 +9,37 @@ export const useGridState = (
apiRef: GridApiRef,
): [GridState, (stateUpdaterFn: (oldState: GridState) => GridState) => boolean, () => void] => {
useGridApi(apiRef);
+
const forceUpdate = React.useCallback(
() => apiRef.current.forceUpdate(() => apiRef.current.state),
[apiRef],
);
+
const setGridState = React.useCallback(
(stateUpdaterFn: (oldState: GridState) => GridState) => {
const newState = stateUpdaterFn(apiRef.current.state);
const hasChanged = apiRef.current.state !== newState;
+ if (!hasChanged) {
+ // We always assign it as we mutate rows for perf reason.
+ apiRef.current.state = newState;
+ return false;
+ }
+
+ const { shouldUpdate, postUpdate } = apiRef.current.applyControlStateConstraint(newState);
+ if (!shouldUpdate) {
+ return false;
+ }
// We always assign it as we mutate rows for perf reason.
apiRef.current.state = newState;
- // TODO deepFreeze(newState);
if (hasChanged && apiRef.current.publishEvent) {
const params: GridStateChangeParams = { api: apiRef.current, state: newState };
apiRef.current.publishEvent(GRID_STATE_CHANGE, params);
+
+ postUpdate();
}
+
return hasChanged;
},
[apiRef],
diff --git a/packages/grid/_modules_/grid/hooks/features/export/serializers/csvSerializer.ts b/packages/grid/_modules_/grid/hooks/features/export/serializers/csvSerializer.ts
index 7cc108784475..01a9daec97f1 100644
--- a/packages/grid/_modules_/grid/hooks/features/export/serializers/csvSerializer.ts
+++ b/packages/grid/_modules_/grid/hooks/features/export/serializers/csvSerializer.ts
@@ -36,7 +36,7 @@ export function serialiseRow(
interface BuildCSVOptions {
columns: GridColumns;
rows: Map;
- selectedRows?: Record;
+ selectedRowIds: GridRowId[];
getCellParams: (id: GridRowId, field: string) => GridCellParams;
delimiterCharacter: GridExportCsvDelimiter;
includeHeaders?: boolean;
@@ -46,18 +46,15 @@ export function buildCSV(options: BuildCSVOptions): string {
const {
columns,
rows,
- selectedRows,
+ selectedRowIds,
getCellParams,
delimiterCharacter,
includeHeaders = true,
} = options;
let rowIds = [...rows.keys()];
- if (selectedRows) {
- const selectedRowIds = Object.keys(selectedRows);
- if (selectedRowIds.length) {
- rowIds = rowIds.filter((id) => selectedRowIds.includes(`${id}`));
- }
+ if (selectedRowIds.length) {
+ rowIds = rowIds.filter((id) => selectedRowIds.includes(`${id}`));
}
const CSVBody = rowIds
diff --git a/packages/grid/_modules_/grid/hooks/features/export/useGridCsvExport.tsx b/packages/grid/_modules_/grid/hooks/features/export/useGridCsvExport.tsx
index 8e69ef4ad191..c4f3c619d88e 100644
--- a/packages/grid/_modules_/grid/hooks/features/export/useGridCsvExport.tsx
+++ b/packages/grid/_modules_/grid/hooks/features/export/useGridCsvExport.tsx
@@ -24,7 +24,7 @@ export const useGridCsvExport = (apiRef: GridApiRef): void => {
return buildCSV({
columns: visibleColumns,
rows: visibleSortedRows,
- selectedRows: selection,
+ selectedRowIds: selection,
getCellParams: apiRef.current.getCellParams,
delimiterCharacter: options?.delimiter || ',',
});
diff --git a/packages/grid/_modules_/grid/hooks/features/selection/gridSelectionSelector.ts b/packages/grid/_modules_/grid/hooks/features/selection/gridSelectionSelector.ts
index bea005bc383f..a75ec4de19d7 100644
--- a/packages/grid/_modules_/grid/hooks/features/selection/gridSelectionSelector.ts
+++ b/packages/grid/_modules_/grid/hooks/features/selection/gridSelectionSelector.ts
@@ -11,7 +11,7 @@ export const selectedGridRowsCountSelector: OutputSelector<
(res: GridSelectionState) => number
> = createSelector(
gridSelectionStateSelector,
- (selection) => Object.keys(selection).length,
+ (selection) => selection.length,
);
export const selectedGridRowsSelector = createSelector<
@@ -22,6 +22,18 @@ export const selectedGridRowsSelector = createSelector<
>(
gridSelectionStateSelector,
gridRowsLookupSelector,
- (selectedRows, rowsLookup) =>
- new Map(Object.values(selectedRows).map((id) => [id, rowsLookup[id]])),
+ (selectedRows, rowsLookup) => new Map(selectedRows.map((id) => [id, rowsLookup[id]])),
+);
+
+export const selectedIdsLookupSelector: OutputSelector<
+ GridState,
+ Record,
+ (res: GridSelectionState) => Record
+> = createSelector>(
+ gridSelectionStateSelector,
+ (selection) =>
+ selection.reduce((lookup, rowId) => {
+ lookup[rowId] = rowId;
+ return lookup;
+ }, {}),
);
diff --git a/packages/grid/_modules_/grid/hooks/features/selection/gridSelectionState.ts b/packages/grid/_modules_/grid/hooks/features/selection/gridSelectionState.ts
index 02e7d98206e0..28fd25e8c7bb 100644
--- a/packages/grid/_modules_/grid/hooks/features/selection/gridSelectionState.ts
+++ b/packages/grid/_modules_/grid/hooks/features/selection/gridSelectionState.ts
@@ -1,3 +1,3 @@
import { GridRowId } from '../../../models/gridRows';
-export type GridSelectionState = Record;
+export type GridSelectionState = GridRowId[];
diff --git a/packages/grid/_modules_/grid/hooks/features/selection/useGridSelection.ts b/packages/grid/_modules_/grid/hooks/features/selection/useGridSelection.ts
index 437d0ece8f6c..62cecbb4b681 100644
--- a/packages/grid/_modules_/grid/hooks/features/selection/useGridSelection.ts
+++ b/packages/grid/_modules_/grid/hooks/features/selection/useGridSelection.ts
@@ -1,42 +1,32 @@
import * as React from 'react';
-import {
- GRID_ROW_CLICK,
- GRID_ROW_SELECTED,
- GRID_SELECTION_CHANGE,
-} from '../../../constants/eventsConstants';
+import { GRID_ROW_CLICK, GRID_SELECTION_CHANGE } from '../../../constants/eventsConstants';
+import { GridComponentProps } from '../../../GridComponentProps';
import { GridApiRef } from '../../../models/api/gridApiRef';
import { GridSelectionApi } from '../../../models/api/gridSelectionApi';
import { GridRowParams } from '../../../models/params/gridRowParams';
-import { GridRowSelectedParams } from '../../../models/params/gridRowSelectedParams';
-import { GridSelectionModelChangeParams } from '../../../models/params/gridSelectionModelChangeParams';
import { GridRowId, GridRowModel } from '../../../models/gridRows';
import { GridSelectionModel } from '../../../models/gridSelectionModel';
-import { isDeepEqual } from '../../../utils/utils';
-import { useGridApiEventHandler, useGridApiOptionHandler } from '../../root/useGridApiEventHandler';
+import { useGridApiEventHandler } from '../../root/useGridApiEventHandler';
import { useGridApiMethod } from '../../root/useGridApiMethod';
import { optionsSelector } from '../../utils/optionsSelector';
import { useLogger } from '../../utils/useLogger';
import { useGridSelector } from '../core/useGridSelector';
import { useGridState } from '../core/useGridState';
import { gridRowsLookupSelector } from '../rows/gridRowsSelector';
-import { GridSelectionState } from './gridSelectionState';
-import { selectedGridRowsSelector } from './gridSelectionSelector';
+import {
+ gridSelectionStateSelector,
+ selectedGridRowsSelector,
+ selectedIdsLookupSelector,
+} from './gridSelectionSelector';
-export const useGridSelection = (apiRef: GridApiRef): void => {
+export const useGridSelection = (apiRef: GridApiRef, props: GridComponentProps): void => {
const logger = useLogger('useGridSelection');
const [, setGridState, forceUpdate] = useGridState(apiRef);
const options = useGridSelector(apiRef, optionsSelector);
const rowsLookup = useGridSelector(apiRef, gridRowsLookupSelector);
- const {
- checkboxSelection,
- disableMultipleSelection,
- disableSelectionOnClick,
- selectionModel,
- isRowSelectable,
- onRowSelected,
- onSelectionModelChange,
- } = options;
+ const { checkboxSelection, disableMultipleSelection, disableSelectionOnClick, isRowSelectable } =
+ options;
const getSelectedRows = React.useCallback(
() => selectedGridRowsSelector(apiRef.current.getState()),
@@ -53,7 +43,7 @@ export const useGridSelection = (apiRef: GridApiRef): void => {
const selectRowModel = React.useCallback(
(rowModelParams: RowModelParams) => {
- const { id, row, allowMultipleOverride, isSelected, isMultipleKey } = rowModelParams;
+ const { id, allowMultipleOverride, isSelected, isMultipleKey } = rowModelParams;
if (isRowSelectable && !isRowSelectable(apiRef.current.getRowParams(id))) {
return;
@@ -62,43 +52,30 @@ export const useGridSelection = (apiRef: GridApiRef): void => {
logger.debug(`Selecting row ${id}`);
setGridState((state) => {
- let selectionState: GridSelectionState = { ...state.selection };
+ let selectionLookup = selectedIdsLookupSelector(state);
const allowMultiSelect =
allowMultipleOverride ||
(!disableMultipleSelection && isMultipleKey) ||
checkboxSelection;
if (allowMultiSelect) {
- const isRowSelected = isSelected == null ? selectionState[id] === undefined : isSelected;
+ const isRowSelected = isSelected == null ? selectionLookup[id] === undefined : isSelected;
if (isRowSelected) {
- selectionState[id] = id;
+ selectionLookup[id] = id;
} else {
- delete selectionState[id];
+ delete selectionLookup[id];
}
} else {
const isRowSelected =
- isSelected == null ? !isMultipleKey || selectionState[id] === undefined : isSelected;
- selectionState = {};
+ isSelected == null ? !isMultipleKey || selectionLookup[id] === undefined : isSelected;
+ selectionLookup = {};
if (isRowSelected) {
- selectionState[id] = id;
+ selectionLookup[id] = id;
}
}
- return { ...state, selection: selectionState };
+ return { ...state, selection: Object.values(selectionLookup) };
});
forceUpdate();
-
- const selectionState = apiRef!.current!.getState('selection');
-
- const rowSelectedParam: GridRowSelectedParams = {
- api: apiRef,
- data: row,
- isSelected: selectionState[id] !== undefined,
- };
- const selectionChangeParam: GridSelectionModelChangeParams = {
- selectionModel: Object.values(selectionState),
- };
- apiRef.current.publishEvent(GRID_ROW_SELECTED, rowSelectedParam);
- apiRef.current.publishEvent(GRID_SELECTION_CHANGE, selectionChangeParam);
},
[
isRowSelectable,
@@ -134,24 +111,18 @@ export const useGridSelection = (apiRef: GridApiRef): void => {
}
setGridState((state) => {
- const selectionState: GridSelectionState = deSelectOthers ? {} : { ...state.selection };
+ const selectionLookup = deSelectOthers ? {} : selectedIdsLookupSelector(state);
selectableIds.forEach((id) => {
if (isSelected) {
- selectionState[id] = id;
- } else if (selectionState[id] !== undefined) {
- delete selectionState[id];
+ selectionLookup[id] = id;
+ } else if (selectionLookup[id] !== undefined) {
+ delete selectionLookup[id];
}
});
- return { ...state, selection: selectionState };
+ return { ...state, selection: Object.values(selectionLookup) };
});
forceUpdate();
-
- const params: GridSelectionModelChangeParams = {
- selectionModel: Object.values(apiRef!.current!.getState('selection')),
- };
- // We don't emit GRID_ROW_SELECTED on each row as it would be too consuming for large set of data.
- apiRef.current.publishEvent(GRID_SELECTION_CHANGE, params);
},
[
isRowSelectable,
@@ -184,8 +155,6 @@ export const useGridSelection = (apiRef: GridApiRef): void => {
);
useGridApiEventHandler(apiRef, GRID_ROW_CLICK, handleRowClick);
- useGridApiOptionHandler(apiRef, GRID_ROW_SELECTED, onRowSelected);
- useGridApiOptionHandler(apiRef, GRID_SELECTION_CHANGE, onSelectionModelChange);
// TODO handle Cell Click/range selection?
const selectionApi: GridSelectionApi = {
@@ -197,17 +166,32 @@ export const useGridSelection = (apiRef: GridApiRef): void => {
useGridApiMethod(apiRef, selectionApi, 'GridSelectionApi');
React.useEffect(() => {
+ apiRef.current.updateControlState({
+ stateId: 'selection',
+ propModel: props.selectionModel,
+ propOnChange: props.onSelectionModelChange,
+ stateSelector: gridSelectionStateSelector,
+ onChangeCallback: (model: GridSelectionModel) => {
+ apiRef.current.publishEvent(GRID_SELECTION_CHANGE, model);
+ },
+ });
+ }, [apiRef, props.onSelectionModelChange, props.selectionModel]);
+
+ React.useEffect(() => {
+ // Rows changed
setGridState((state) => {
- const newSelectionState = { ...state.selection };
+ const newSelectionState = [...state.selection];
+ const selectionLookup = selectedIdsLookupSelector(state);
+
let hasChanged = false;
- Object.keys(newSelectionState).forEach((id: GridRowId) => {
+ newSelectionState.forEach((id: GridRowId) => {
if (!rowsLookup[id]) {
- delete newSelectionState[id];
+ delete selectionLookup[id];
hasChanged = true;
}
});
if (hasChanged) {
- return { ...state, selection: newSelectionState };
+ return { ...state, selection: Object.values(selectionLookup) };
}
return state;
});
@@ -215,25 +199,31 @@ export const useGridSelection = (apiRef: GridApiRef): void => {
}, [rowsLookup, apiRef, setGridState, forceUpdate]);
React.useEffect(() => {
- const currentModel = Object.values(apiRef.current.getState().selection);
- if (!isDeepEqual(currentModel, selectionModel)) {
- apiRef.current.setSelectionModel(selectionModel || []);
+ // prop selectionModel changed
+ if (props.selectionModel === undefined) {
+ return;
+ }
+ const currentModel = apiRef.current.getState().selection;
+ if (currentModel !== props.selectionModel) {
+ setGridState((state) => ({ ...state, selection: props.selectionModel || [] }));
}
- }, [apiRef, selectionModel]);
+ }, [apiRef, props.selectionModel, setGridState]);
React.useEffect(() => {
+ // isRowSelectable changed
setGridState((state) => {
- const newSelectionState = { ...state.selection };
+ const newSelectionState = [...state.selection];
+ const selectionLookup = selectedIdsLookupSelector(state);
let hasChanged = false;
- Object.keys(newSelectionState).forEach((id: GridRowId) => {
+ newSelectionState.forEach((id: GridRowId) => {
const isSelectable = !isRowSelectable || isRowSelectable(apiRef.current.getRowParams(id));
if (!isSelectable) {
- delete newSelectionState[id];
+ delete selectionLookup[id];
hasChanged = true;
}
});
if (hasChanged) {
- return { ...state, selection: newSelectionState };
+ return { ...state, selection: Object.values(selectionLookup) };
}
return state;
});
diff --git a/packages/grid/_modules_/grid/hooks/root/useGridApiMethod.ts b/packages/grid/_modules_/grid/hooks/root/useGridApiMethod.ts
index 946667a04612..cca797fc9a0f 100644
--- a/packages/grid/_modules_/grid/hooks/root/useGridApiMethod.ts
+++ b/packages/grid/_modules_/grid/hooks/root/useGridApiMethod.ts
@@ -12,11 +12,10 @@ export function useGridApiMethod>(
const apiMethodsRef = React.useRef(apiMethods);
const [apiMethodsNames] = React.useState(Object.keys(apiMethods));
- React.useEffect(() => {
- apiMethodsRef.current = apiMethods;
- }, [apiMethods]);
-
- React.useEffect(() => {
+ const installMethods = React.useCallback(() => {
+ if (!apiRef.current) {
+ return;
+ }
apiMethodsNames.forEach((methodName) => {
if (!apiRef.current.hasOwnProperty(methodName)) {
logger.debug(`Adding ${apiName}.${methodName} to apiRef`);
@@ -24,4 +23,14 @@ export function useGridApiMethod>(
}
});
}, [apiMethodsNames, apiName, apiRef, logger]);
+
+ React.useEffect(() => {
+ apiMethodsRef.current = apiMethods;
+ }, [apiMethods]);
+
+ React.useEffect(() => {
+ installMethods();
+ }, [installMethods]);
+
+ installMethods();
}
diff --git a/packages/grid/_modules_/grid/models/api/gridApi.ts b/packages/grid/_modules_/grid/models/api/gridApi.ts
index 48fc530901d0..5b549803495c 100644
--- a/packages/grid/_modules_/grid/models/api/gridApi.ts
+++ b/packages/grid/_modules_/grid/models/api/gridApi.ts
@@ -1,23 +1,24 @@
+import { GridColumnApi } from './gridColumnApi';
import { GridColumnMenuApi } from './gridColumnMenuApi';
-import { GridFocusApi } from './gridFocusApi';
-import { GridParamsApi } from './gridParamsApi';
import { GridComponentsApi } from './gridComponentsApi';
-import { GridFilterApi } from './gridFilterApi';
+import { GridControlStateApi } from './gridControlStateApi';
+import { GridCoreApi } from './gridCoreApi';
+import { GridClipboardApi } from './gridClipboardApi';
+import { GridCsvExportApi } from './gridCsvExportApi';
+import { GridDensityApi } from './gridDensityApi';
import { GridEditRowApi } from './gridEditRowApi';
+import { GridEventsApi } from './gridEventsApi';
+import { GridFilterApi } from './gridFilterApi';
+import { GridFocusApi } from './gridFocusApi';
+import { GridLocaleTextApi } from './gridLocaleTextApi';
+import { GridPaginationApi } from './gridPaginationApi';
+import { GridParamsApi } from './gridParamsApi';
import { GridPreferencesPanelApi } from './gridPreferencesPanelApi';
import { GridRowApi } from './gridRowApi';
-import { GridColumnApi } from './gridColumnApi';
import { GridSelectionApi } from './gridSelectionApi';
import { GridSortApi } from './gridSortApi';
-import { GridPaginationApi } from './gridPaginationApi';
import { GridStateApi } from './gridStateApi';
import { GridVirtualizationApi } from './gridVirtualizationApi';
-import { GridCoreApi } from './gridCoreApi';
-import { GridEventsApi } from './gridEventsApi';
-import { GridDensityApi } from './gridDensityApi';
-import { GridLocaleTextApi } from './gridLocaleTextApi';
-import { GridCsvExportApi } from './gridCsvExportApi';
-import { GridClipboardApi } from './gridClipboardApi';
/**
* The full grid API.
@@ -42,4 +43,5 @@ export interface GridApi
GridColumnMenuApi,
GridPreferencesPanelApi,
GridLocaleTextApi,
+ GridControlStateApi,
GridClipboardApi {}
diff --git a/packages/grid/_modules_/grid/models/api/gridControlStateApi.ts b/packages/grid/_modules_/grid/models/api/gridControlStateApi.ts
new file mode 100644
index 000000000000..9eb2a12aa70b
--- /dev/null
+++ b/packages/grid/_modules_/grid/models/api/gridControlStateApi.ts
@@ -0,0 +1,24 @@
+import { GridState } from '../../hooks/features/core/gridState';
+import { ControlStateItem } from '../controlStateItem';
+
+/**
+ * The control state API interface that is available in the grid `apiRef`.
+ */
+export interface GridControlStateApi {
+ /**
+ * Updates a control state that binds the model, the onChange prop, and the grid state together.
+ * @param {ControlStateItem} controlState The [[ControlStateItem]] to be registered.
+ * @ignore - do not document.
+ */
+ updateControlState: (controlState: ControlStateItem) => void;
+ /**
+ * Allows the internal grid state to apply the registered control state constraint.
+ * @param {GridState} state The new modified state that would be the next if the state is not controlled.
+ * @returns {shouldUpdate: boolean, postUpdate: () => void}, shouldUpdate let the state know if it should update, and postUpdate is a callback function triggered if the state has updated.
+ * @ignore - do not document.
+ */
+ applyControlStateConstraint: (state: GridState) => {
+ shouldUpdate: boolean;
+ postUpdate: () => void;
+ };
+}
diff --git a/packages/grid/_modules_/grid/models/api/index.ts b/packages/grid/_modules_/grid/models/api/index.ts
index 7599251c833d..3b51def9aa47 100644
--- a/packages/grid/_modules_/grid/models/api/index.ts
+++ b/packages/grid/_modules_/grid/models/api/index.ts
@@ -19,4 +19,5 @@ export * from './gridFocusApi';
export * from './gridFilterApi';
export * from './gridColumnMenuApi';
export * from './gridPreferencesPanelApi';
+export * from './gridControlStateApi';
export * from './gridClipboardApi';
diff --git a/packages/grid/_modules_/grid/models/colDef/gridCheckboxSelection.tsx b/packages/grid/_modules_/grid/models/colDef/gridCheckboxSelection.tsx
index 8c6efbbfebcc..0e7005d0de0d 100644
--- a/packages/grid/_modules_/grid/models/colDef/gridCheckboxSelection.tsx
+++ b/packages/grid/_modules_/grid/models/colDef/gridCheckboxSelection.tsx
@@ -1,6 +1,7 @@
import * as React from 'react';
import { GridCellCheckboxRenderer } from '../../components/columnSelection/GridCellCheckboxRenderer';
import { GridHeaderCheckbox } from '../../components/columnSelection/GridHeaderCheckbox';
+import { selectedIdsLookupSelector } from '../../hooks/features/selection/gridSelectionSelector';
import { GridColDef } from './gridColDef';
import { GRID_BOOLEAN_COL_DEF } from './gridBooleanColDef';
@@ -13,7 +14,10 @@ export const gridCheckboxSelectionColDef: GridColDef = {
sortable: false,
filterable: false,
disableColumnMenu: true,
- valueGetter: (params) => params.api.getState().selection[params.id] !== undefined,
+ valueGetter: (params) => {
+ const selectionLookup = selectedIdsLookupSelector(params.api.getState());
+ return selectionLookup[params.id] !== undefined;
+ },
renderHeader: (params) => ,
renderCell: (params) => ,
cellClassName: 'MuiDataGrid-cellCheckbox',
diff --git a/packages/grid/_modules_/grid/models/controlStateItem.ts b/packages/grid/_modules_/grid/models/controlStateItem.ts
new file mode 100644
index 000000000000..a79f00d618ac
--- /dev/null
+++ b/packages/grid/_modules_/grid/models/controlStateItem.ts
@@ -0,0 +1,9 @@
+import { GridState } from '../hooks/features/core/gridState';
+
+export interface ControlStateItem {
+ stateId: string;
+ propModel?: any;
+ stateSelector: (state: GridState) => TModel;
+ propOnChange?: (model: TModel) => void;
+ onChangeCallback?: (model: TModel) => void;
+}
diff --git a/packages/grid/_modules_/grid/models/gridOptions.tsx b/packages/grid/_modules_/grid/models/gridOptions.tsx
index aa118a90b099..c199f43f45d0 100644
--- a/packages/grid/_modules_/grid/models/gridOptions.tsx
+++ b/packages/grid/_modules_/grid/models/gridOptions.tsx
@@ -7,14 +7,13 @@ import { getGridDefaultColumnTypes } from './colDef/gridDefaultColumnTypes';
import { GridDensity, GridDensityTypes } from './gridDensity';
import { GridEditRowsModel } from './gridEditRowModel';
import { GridFeatureMode, GridFeatureModeConstant } from './gridFeatureMode';
+import { GridRowId } from './gridRows';
import { Logger } from './logger';
import { GridCellParams } from './params/gridCellParams';
import { GridColumnHeaderParams } from './params/gridColumnHeaderParams';
import { GridFilterModelParams } from './params/gridFilterModelParams';
import { GridPageChangeParams } from './params/gridPageChangeParams';
import { GridRowParams } from './params/gridRowParams';
-import { GridRowSelectedParams } from './params/gridRowSelectedParams';
-import { GridSelectionModelChangeParams } from './params/gridSelectionModelChangeParams';
import { GridSortModelParams } from './params/gridSortModelParams';
import { GridSelectionModel } from './gridSelectionModel';
import { GridSortDirection, GridSortModel } from './gridSortModel';
@@ -407,11 +406,6 @@ export interface GridOptions {
* @param event [[React.MouseEvent]].
*/
onRowLeave?: (param: GridRowParams, event: React.MouseEvent) => void;
- /**
- * Callback fired when one row is selected.
- * @param param With all properties from [[GridRowSelectedParams]].
- */
- onRowSelected?: (param: GridRowSelectedParams) => void;
/**
* Callback fired when the grid is resized.
* @param param With all properties from [[GridResizeParams]].
@@ -419,9 +413,9 @@ export interface GridOptions {
onResize?: (param: GridResizeParams) => void;
/**
* Callback fired when the selection state of one or multiple rows changes.
- * @param param With all properties from [[SelectionChangeParams]].
+ * @param selectionModel With all the row ids [[GridRowId]][].
*/
- onSelectionModelChange?: (param: GridSelectionModelChangeParams) => void;
+ onSelectionModelChange?: (selectionModel: GridRowId[]) => void;
/**
* Callback fired when the sort model changes before a column is sorted.
* @param param With all properties from [[GridSortModelParams]].
diff --git a/packages/grid/_modules_/grid/models/index.ts b/packages/grid/_modules_/grid/models/index.ts
index 29988490a06f..640b081ce39f 100644
--- a/packages/grid/_modules_/grid/models/index.ts
+++ b/packages/grid/_modules_/grid/models/index.ts
@@ -11,6 +11,7 @@ export * from './gridRootContainerRef';
export * from './gridRenderContextProps';
export * from './gridRows';
export * from './gridSortModel';
+export * from './gridSelectionModel';
export * from './params';
export * from './gridCellClass';
export * from './gridCell';
diff --git a/packages/grid/_modules_/grid/models/params/gridRowSelectedParams.ts b/packages/grid/_modules_/grid/models/params/gridRowSelectedParams.ts
deleted file mode 100644
index 265faf565005..000000000000
--- a/packages/grid/_modules_/grid/models/params/gridRowSelectedParams.ts
+++ /dev/null
@@ -1,19 +0,0 @@
-import { GridRowModel } from '../gridRows';
-
-/**
- * Object passed as parameter as the row selected event handler.
- */
-export interface GridRowSelectedParams {
- /**
- * The row data of the row that triggers the event.
- */
- data: GridRowModel;
- /**
- * The selected state of the row that triggers the event.
- */
- isSelected: boolean;
- /**
- * GridApiRef that let you manipulate the grid.
- */
- api: any;
-}
diff --git a/packages/grid/_modules_/grid/models/params/index.ts b/packages/grid/_modules_/grid/models/params/index.ts
index 1a46417fabdd..f22e87144c99 100644
--- a/packages/grid/_modules_/grid/models/params/index.ts
+++ b/packages/grid/_modules_/grid/models/params/index.ts
@@ -7,7 +7,6 @@ export * from './gridSlotComponentProps';
export * from './gridFilterModelParams';
export * from './gridPageChangeParams';
export * from './gridRowParams';
-export * from './gridRowSelectedParams';
export * from './gridScrollParams';
export * from './gridSelectionModelChangeParams';
export * from './gridSortModelParams';
diff --git a/packages/grid/_modules_/grid/useGridComponent.tsx b/packages/grid/_modules_/grid/useGridComponent.tsx
index e6bf70b7de11..efdebb59a26f 100644
--- a/packages/grid/_modules_/grid/useGridComponent.tsx
+++ b/packages/grid/_modules_/grid/useGridComponent.tsx
@@ -3,6 +3,7 @@ import { useGridColumnMenu } from './hooks/features/columnMenu/useGridColumnMenu
import { useGridColumnReorder } from './hooks/features/columnReorder/useGridColumnReorder';
import { useGridColumnResize } from './hooks/features/columnResize/useGridColumnResize';
import { useGridColumns } from './hooks/features/columns/useGridColumns';
+import { useGridControlState } from './hooks/features/core/useGridControlState';
import { useGridDensity } from './hooks/features/density/useGridDensity';
import { useGridCsvExport } from './hooks/features/export/useGridCsvExport';
import { useGridFilter } from './hooks/features/filter/useGridFilter';
@@ -38,6 +39,7 @@ export const useGridComponent = (apiRef: GridApiRef, props: GridComponentProps)
useLoggerFactory(apiRef, props);
useApi(apiRef);
useErrorHandler(apiRef, props);
+ useGridControlState(apiRef);
useGridScrollbarSizeDetector(apiRef, props);
useOptionsProp(apiRef, props);
useEvents(apiRef);
@@ -51,7 +53,7 @@ export const useGridComponent = (apiRef: GridApiRef, props: GridComponentProps)
useGridFocus(apiRef, props);
useGridKeyboard(apiRef);
useGridKeyboardNavigation(apiRef);
- useGridSelection(apiRef);
+ useGridSelection(apiRef, props);
useGridSorting(apiRef, props);
useGridColumnMenu(apiRef);
useGridPreferencesPanel(apiRef);
diff --git a/packages/grid/data-grid/src/DataGrid.tsx b/packages/grid/data-grid/src/DataGrid.tsx
index 682d6a7ff85a..98a64d79f75d 100644
--- a/packages/grid/data-grid/src/DataGrid.tsx
+++ b/packages/grid/data-grid/src/DataGrid.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import PropTypes from 'prop-types';
import { chainPropTypes } from '@material-ui/utils';
-import { GridComponent, GridComponentProps, useThemeProps } from '../../_modules_/grid';
+import { GridComponent, GridComponentProps, GridRowId, useThemeProps } from '../../_modules_/grid';
const FORCED_PROPS: Partial = {
disableColumnResize: true,
@@ -29,6 +29,7 @@ export type DataGridProps = Omit<
| 'onRowsScrollEnd'
| 'pagination'
| 'scrollEndThreshold'
+ | 'selectionModel'
> & {
apiRef?: undefined;
checkboxSelectionVisibleOnly?: false;
@@ -38,6 +39,7 @@ export type DataGridProps = Omit<
disableMultipleColumnsSorting?: true;
disableMultipleSelection?: true;
onRowsScrollEnd?: undefined;
+ selectionModel?: GridRowId | GridRowId[];
pagination?: true;
};
@@ -48,19 +50,25 @@ const DataGridRaw = React.forwardRef(function Dat
ref,
) {
const props = useThemeProps({ props: inProps, name: 'MuiDataGrid' });
- const { pageSize: pageSizeProp, ...other } = props;
+ const { pageSize: pageSizeProp, selectionModel: dataGridSelectionModel, ...other } = props;
let pageSize = pageSizeProp;
if (pageSize && pageSize > MAX_PAGE_SIZE) {
pageSize = MAX_PAGE_SIZE;
}
+ const selectionModel =
+ dataGridSelectionModel !== undefined && !Array.isArray(dataGridSelectionModel)
+ ? [dataGridSelectionModel]
+ : dataGridSelectionModel;
+
return (
);
@@ -227,4 +235,22 @@ DataGrid.propTypes = {
}
return null;
}),
+ selectionModel: chainPropTypes(
+ PropTypes.oneOfType([PropTypes.number, PropTypes.string, PropTypes.array]),
+ (props: any) => {
+ if (Array.isArray(props.selectionModel) && props.selectionModel.length > 1) {
+ return new Error(
+ [
+ `Material-UI: \`\` is not a valid prop.`,
+ 'selectionModel can only be of 1 item in DataGrid.',
+ '',
+ 'You need to upgrade to the XGrid component to unlock multiple selection.',
+ ].join('\n'),
+ );
+ }
+ return null;
+ },
+ ),
} as any;
diff --git a/packages/grid/data-grid/src/tests/selection.DataGrid.test.tsx b/packages/grid/data-grid/src/tests/selection.DataGrid.test.tsx
index 850f4c79572e..4860fd22e06a 100644
--- a/packages/grid/data-grid/src/tests/selection.DataGrid.test.tsx
+++ b/packages/grid/data-grid/src/tests/selection.DataGrid.test.tsx
@@ -195,7 +195,7 @@ describe(' - Selection', () => {
},
]}
columns={[{ field: 'brand', width: 100 }]}
- selectionModel={[1]}
+ selectionModel={1}
/>
,
);
@@ -234,7 +234,7 @@ describe(' - Selection', () => {
const row0 = getRow(0);
expect(row0).to.have.class('Mui-selected');
- setProps({ selectionModel: [1] });
+ setProps({ selectionModel: 1 });
// TODO fix this assertion. The model is forced from the outside, hence shouldn't change.
// https://github.com/mui-org/material-ui-x/issues/190
expect(row0).not.to.have.class('Mui-selected');
@@ -265,15 +265,13 @@ describe(' - Selection', () => {
);
}
- const { setProps } = render();
- expect(onSelectionModelChange.callCount).to.equal(1);
- expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equals([0]);
- setProps({ selectionModel: [0, 1] });
- expect(onSelectionModelChange.callCount).to.equal(2);
- expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equals([0, 1]);
- setProps({ selectionModel: [0, 1] });
- expect(onSelectionModelChange.callCount).to.equal(2);
- expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equals([0, 1]);
+ const { setProps } = render();
+ expect(onSelectionModelChange.callCount).to.equal(0);
+ const firstRow = getRow(0);
+ expect(firstRow).to.have.class('Mui-selected');
+ setProps({ selectionModel: 0 });
+ expect(onSelectionModelChange.callCount).to.equal(0);
+ expect(getRow(0)).to.have.class('Mui-selected');
});
it('should filter out unselectable rows when the selectionModel prop changes', () => {
@@ -290,7 +288,7 @@ describe(' - Selection', () => {
},
],
columns: [{ field: 'brand', width: 100 }],
- selectionModel: [1],
+ selectionModel: 1,
isRowSelectable: (params) => params.id > 0,
};
@@ -306,8 +304,8 @@ describe(' - Selection', () => {
expect(getRow(0)).not.to.have.class('Mui-selected');
expect(getRow(1)).to.have.class('Mui-selected');
- setProps({ selectionModel: [0] });
- expect(getRow(0)).not.to.have.class('Mui-selected');
+ setProps({ selectionModel: 0 });
+ expect(getRow(0)).to.have.class('Mui-selected');
expect(getRow(1)).not.to.have.class('Mui-selected');
});
});
@@ -327,7 +325,6 @@ describe(' - Selection', () => {
},
],
columns: [{ field: 'brand', width: 100 }],
- selectionModel: [0],
isRowSelectable: () => true,
};
@@ -340,6 +337,7 @@ describe(' - Selection', () => {
}
const { setProps } = render();
+ fireEvent.click(getRow(0));
expect(getRow(0)).to.have.class('Mui-selected');
expect(getRow(1)).not.to.have.class('Mui-selected');
diff --git a/packages/grid/x-grid/src/tests/events.XGrid.test.tsx b/packages/grid/x-grid/src/tests/events.XGrid.test.tsx
index 0cb26c161477..0d25316d7f21 100644
--- a/packages/grid/x-grid/src/tests/events.XGrid.test.tsx
+++ b/packages/grid/x-grid/src/tests/events.XGrid.test.tsx
@@ -211,7 +211,7 @@ describe(' - Events Params', () => {
const stopClick = (params, event) => {
event.stopPropagation();
};
- render();
+ render();
const cell11 = getCell(1, 1);
fireEvent.click(cell11);
@@ -219,18 +219,21 @@ describe(' - Events Params', () => {
});
it('should select a row by default', () => {
- render();
+ const handleSelection = spy();
+ render();
const cell11 = getCell(1, 1);
fireEvent.click(cell11);
- expect(eventStack).to.deep.equal(['rowSelected']);
+ expect(handleSelection.callCount).to.equal(1);
+ expect(handleSelection.lastCall.firstArg).to.deep.equal([2]);
});
it('should not select a row if options.disableSelectionOnClick', () => {
- render();
+ const handleSelection = spy();
+ render();
const cell11 = getCell(1, 1);
fireEvent.click(cell11);
- expect(eventStack).to.deep.equal([]);
+ expect(handleSelection.callCount).to.equal(0);
});
});
it('publishing GRID_ROWS_SCROLL should call onRowsScrollEnd callback', () => {
diff --git a/packages/grid/x-grid/src/tests/filtering.XGrid.test.tsx b/packages/grid/x-grid/src/tests/filtering.XGrid.test.tsx
index e8aed2bdf58f..d527e7d5e9d5 100644
--- a/packages/grid/x-grid/src/tests/filtering.XGrid.test.tsx
+++ b/packages/grid/x-grid/src/tests/filtering.XGrid.test.tsx
@@ -248,7 +248,7 @@ describe(' - Filter', () => {
render();
const checkAllCell = getColumnHeaderCell(0).querySelector('input');
fireEvent.click(checkAllCell);
- expect(apiRef.current.getState().selection).to.deep.equal({ 1: 1 });
+ expect(apiRef.current.getState().selection).to.deep.equal([1]);
});
it('should allow to clear filters by passing an empty filter model', () => {
diff --git a/packages/grid/x-grid/src/tests/selection.XGrid.test.tsx b/packages/grid/x-grid/src/tests/selection.XGrid.test.tsx
index 92492923eeb3..77e56801e973 100644
--- a/packages/grid/x-grid/src/tests/selection.XGrid.test.tsx
+++ b/packages/grid/x-grid/src/tests/selection.XGrid.test.tsx
@@ -1,7 +1,7 @@
import * as React from 'react';
import { expect } from 'chai';
import { spy } from 'sinon';
-import { getColumnValues, getRow } from 'test/utils/helperFn';
+import { getCell, getColumnValues, getRow } from 'test/utils/helperFn';
import {
// @ts-expect-error need to migrate helpers to TypeScript
screen,
@@ -9,7 +9,13 @@ import {
// @ts-expect-error need to migrate helpers to TypeScript
fireEvent,
} from 'test/utils';
-import { GridApiRef, GridComponentProps, useGridApiRef, XGrid } from '@material-ui/x-grid';
+import {
+ GridApiRef,
+ GridComponentProps,
+ GridSelectionModel,
+ useGridApiRef,
+ XGrid,
+} from '@material-ui/x-grid';
const isJSDOM = /jsdom/.test(window.navigator.userAgent);
@@ -52,17 +58,18 @@ describe(' - Selection', () => {
};
describe('getSelectedRows', () => {
- it('should return the latest values when called inside onSelectionModelChange', () => {
+ it('should not change before onSelectionModelChange', () => {
render(
{
- expect(apiRef!.current.getSelectedRows().size).to.equal(1);
- expect(apiRef!.current.getSelectedRows().get(1)).to.equal(baselineProps.rows[1]);
+ onSelectionModelChange={(model) => {
+ expect(apiRef!.current.getSelectedRows().size).to.equal(0);
+ expect(model).to.deep.equal([1]);
}}
/>,
);
expect(apiRef!.current.getSelectedRows().size).to.equal(0);
apiRef!.current.selectRow(1);
+ expect(apiRef!.current.getSelectedRows().get(1)).to.equal(baselineProps.rows[1]);
});
});
@@ -71,14 +78,14 @@ describe(' - Selection', () => {
const onSelectionModelChange = spy();
render();
apiRef!.current.selectRow(1);
- expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([1]);
+ expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([1]);
apiRef!.current.selectRow(2);
- expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([2]);
+ expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([2]);
// Keep old selection
apiRef!.current.selectRow(3, true, true);
- expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([2, 3]);
+ expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([2, 3]);
apiRef!.current.selectRow(3, false, true);
- expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([2]);
+ expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([2]);
});
it('should not call onSelectionModelChange if the row is unselectable', () => {
@@ -101,14 +108,14 @@ describe(' - Selection', () => {
const onSelectionModelChange = spy();
render();
apiRef!.current.selectRows([1, 2]);
- expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([1, 2]);
+ expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([1, 2]);
apiRef!.current.selectRows([3]);
- expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([1, 2, 3]);
+ expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([1, 2, 3]);
apiRef!.current.selectRows([1, 2], false);
- expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([3]);
+ expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([3]);
// Deselect others
apiRef!.current.selectRows([4, 5], true, true);
- expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([4, 5]);
+ expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([4, 5]);
});
it('should filter out unselectable rows before calling onSelectionModelChange', () => {
@@ -120,12 +127,29 @@ describe(' - Selection', () => {
/>,
);
apiRef!.current.selectRows([0, 1, 2]);
- expect(onSelectionModelChange.lastCall.args[0].selectionModel).to.deep.equal([1, 2]);
+ expect(onSelectionModelChange.lastCall.args[0]).to.deep.equal([1, 2]);
});
});
it('should clean the selected ids when the rows prop changes', () => {
- const { setProps } = render();
+ const DemoTest = (props: Partial) => {
+ apiRef = useGridApiRef();
+ const [selectionModelState, setSelectionModelState] = React.useState(props.selectionModel);
+ const handleSelectionChange = (model) => setSelectionModelState(model);
+ return (
+
+
+
+ );
+ };
+
+ const { setProps } = render();
expect(getSelectedRows(apiRef)).to.deep.equal([0, 1, 2]);
setProps({
rows: [
@@ -191,4 +215,67 @@ describe(' - Selection', () => {
fireEvent.click(screen.getByRole('button', { name: /next page/i }));
expect(getRow(1)).not.to.have.class('Mui-selected');
});
+
+ describe('control Selection', () => {
+ it('should update the selection state when neither the model nor the onChange are set', () => {
+ render();
+ fireEvent.click(getCell(0, 0));
+ expect(getRow(0)).to.have.class('Mui-selected');
+ });
+
+ it('should not update the selection model when the selectionModelProp is set', () => {
+ const selectionModel: GridSelectionModel = [1];
+ render();
+
+ expect(getRow(0)).not.to.have.class('Mui-selected');
+ expect(getRow(1)).to.have.class('Mui-selected');
+ fireEvent.click(getCell(0, 0));
+ expect(getRow(0)).not.to.have.class('Mui-selected');
+ });
+
+ it('should update the selection state when the model is not set, but the onChange is set', () => {
+ const onModelChange = spy();
+ render();
+
+ fireEvent.click(getCell(0, 0));
+ expect(getRow(0)).to.have.class('Mui-selected');
+ expect(onModelChange.callCount).to.equal(1);
+ expect(onModelChange.firstCall.firstArg).to.deep.equal([0]);
+ });
+
+ it('should control selection state when the model and the onChange are set', () => {
+ const ControlCase = (props: Partial) => {
+ const { rows, columns, ...others } = props;
+ const [selectionModel, setSelectionModel] = React.useState([0]);
+ const handleSelectionChange = (newModel) => {
+ if (newModel.length) {
+ setSelectionModel([...newModel, 2]);
+ return;
+ }
+ setSelectionModel(newModel);
+ };
+
+ return (
+
+
+
+ );
+ };
+
+ render();
+
+ expect(getRow(0)).to.have.class('Mui-selected');
+ fireEvent.click(getCell(1, 0));
+ expect(getRow(0)).not.to.have.class('Mui-selected');
+ expect(getRow(1)).to.have.class('Mui-selected');
+ expect(getRow(2)).to.have.class('Mui-selected');
+ });
+ });
});
diff --git a/packages/storybook/src/stories/grid-events.stories.tsx b/packages/storybook/src/stories/grid-events.stories.tsx
index bba60ff57be9..04c115ae4772 100644
--- a/packages/storybook/src/stories/grid-events.stories.tsx
+++ b/packages/storybook/src/stories/grid-events.stories.tsx
@@ -23,7 +23,6 @@ export function AllEvents() {
onCellOver: (params) => action('onCellOver')(params),
onRowOver: (params) => action('onRowOver')(params),
onColumnHeaderClick: (params) => action('onColumnHeaderClick')(params),
- onRowSelected: (params) => action('onRowSelected')(params),
onSelectionModelChange: (params) => action('onSelectionChange', { depth: 1 })(params),
onPageChange: (params) => action('onPageChange')(params),
onPageSizeChange: (params) => action('onPageSizeChange')(params),
diff --git a/packages/storybook/src/stories/grid-selection.stories.tsx b/packages/storybook/src/stories/grid-selection.stories.tsx
index 6d401e5f214a..df00c45bb680 100644
--- a/packages/storybook/src/stories/grid-selection.stories.tsx
+++ b/packages/storybook/src/stories/grid-selection.stories.tsx
@@ -1,13 +1,7 @@
import { useDemoData } from '@material-ui/x-grid-data-generator';
import * as React from 'react';
import { action } from '@storybook/addon-actions';
-import {
- XGrid,
- GridOptionsProp,
- useGridApiRef,
- GridSelectionModelChangeParams,
- GridRowId,
-} from '@material-ui/x-grid';
+import { XGrid, GridOptionsProp, useGridApiRef, GridRowId } from '@material-ui/x-grid';
import { getData, GridData } from '../data/data-service';
import { useData } from '../hooks/useData';
@@ -45,7 +39,6 @@ export const EventsMapped = () => {
const options: GridOptionsProp = {
onSelectionModelChange: (params) => action('onSelectionChange', { depth: 1 })(params),
- onRowSelected: (params) => action('onRowSelected')(params),
};
return ;
@@ -79,8 +72,8 @@ export function HandleSelection() {
const [selectionModel, setSelectionModel] = React.useState([]);
const handleSelection = React.useCallback(
- (params: GridSelectionModelChangeParams) => {
- setSelectionModel(params.selectionModel);
+ (model) => {
+ setSelectionModel(model);
},
[setSelectionModel],
);
@@ -112,3 +105,87 @@ export const UnselectableRows = () => {
/>
);
};
+export function ControlSelection() {
+ const [storyState] = React.useState({
+ rows: [
+ {
+ id: 0,
+ brand: 'Nike',
+ isPublished: false,
+ },
+ {
+ id: 1,
+ brand: 'Adidas',
+ isPublished: true,
+ },
+ {
+ id: 2,
+ brand: 'Puma',
+ isPublished: true,
+ },
+ ],
+ columns: [{ field: 'brand' }, { field: 'isPublished', type: 'boolean' }],
+ });
+
+ const [selectionModel, setSelectionModel] = React.useState([0]);
+ const handleSelectionChange = React.useCallback((newModel) => {
+ setSelectionModel(newModel);
+ }, []);
+
+ return (
+
+
+
+ );
+}
+export function NoControlSelection() {
+ const [storyState] = React.useState({
+ rows: [
+ {
+ id: 0,
+ brand: 'Nike',
+ isPublished: false,
+ },
+ {
+ id: 1,
+ brand: 'Adidas',
+ isPublished: true,
+ },
+ {
+ id: 2,
+ brand: 'Puma',
+ isPublished: true,
+ },
+ ],
+ columns: [{ field: 'brand' }, { field: 'isPublished', type: 'boolean' }],
+ });
+
+ return (
+
+
+
+ );
+}
+export function LargeControlSelection() {
+ const { data } = useDemoData({ rowLength: 500000, dataSet: 'Commodity', maxColumns: 100 });
+
+ const [selectionModel, setSelectionModel] = React.useState([]);
+ const handleSelectionChange = React.useCallback((newModel) => {
+ setSelectionModel(newModel);
+ }, []);
+
+ return (
+
+ );
+}
diff --git a/packages/storybook/src/stories/grid-streaming.stories.tsx b/packages/storybook/src/stories/grid-streaming.stories.tsx
index 07e2cb7ea78c..d25156b4fd22 100644
--- a/packages/storybook/src/stories/grid-streaming.stories.tsx
+++ b/packages/storybook/src/stories/grid-streaming.stories.tsx
@@ -20,7 +20,6 @@ export default {
export const SlowUpdateGrid = () => {
const options: GridOptionsProp = {
onSelectionModelChange: (params) => action('onSelectionChange', { depth: 1 })(params),
- onRowSelected: (params) => action('onRowSelected')(params),
};
const rate = { min: 1000, max: 5000 };
return (
@@ -35,7 +34,6 @@ export const SlowUpdateGrid = () => {
export const FastUpdateGrid = () => {
const options: GridOptionsProp = {
onSelectionModelChange: (params) => action('onSelectionChange', { depth: 1 })(params),
- onRowSelected: (params) => action('onRowSelected')(params),
};
const rate = { min: 100, max: 500 };
return (
@@ -50,7 +48,6 @@ export const FastUpdateGrid = () => {
export const SingleSubscriptionFast = () => {
const options: GridOptionsProp = {
onSelectionModelChange: (params) => action('onSelectionChange', { depth: 1 })(params),
- onRowSelected: (params) => action('onRowSelected')(params),
};
const rate = { min: 100, max: 500 };
return (