Skip to content

Commit

Permalink
[DataGrid] Implement useControlState hook, and add control state on s…
Browse files Browse the repository at this point in the history
…electionModel (#1823)
  • Loading branch information
dtassone authored Jul 6, 2021
1 parent 5de2cc0 commit 548bb01
Show file tree
Hide file tree
Showing 36 changed files with 496 additions and 202 deletions.
1 change: 0 additions & 1 deletion docs/pages/api-docs/data-grid/data-grid.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,6 @@ import { DataGrid } from '@material-ui/data-grid';
| <span class="prop-name">onRowOut</span> | <span class="prop-type">(param: GridRowParams, event: React.MouseEvent) => void</span> | | Callback fired when a mouse out comes from a row container element. |
| <span class="prop-name">onRowEnter</span> | <span class="prop-type">(param: GridRowParams, event: React.MouseEvent) => void</span> | | Callback fired when a mouse enter comes from a row container element. |
| <span class="prop-name">onRowLeave</span> | <span class="prop-type">(param: GridRowParams, event: React.MouseEvent) => void</span> | | Callback fired when a mouse leave event comes from a row container element. |
| <span class="prop-name">onRowSelected</span> | <span class="prop-type">(param: GridRowSelectedParams) => void</span> | | Callback fired when one row is selected. |
| <span class="prop-name">onSelectionModelChange</span> | <span class="prop-type">(param: GridSelectionModelChangeParams) => void</span> | | Callback fired when the selection state of one or multiple rows changes. |
| <span class="prop-name">onSortModelChange</span> | <span class="prop-type">(param: GridSortModelParams) => void</span> | | Callback fired when the sort model changes before a column is sorted. |
| <span class="prop-name">page</span> | <span class="prop-type">number</span> | 1 | Set the current page. |
Expand Down
1 change: 0 additions & 1 deletion docs/pages/api-docs/data-grid/x-grid.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,6 @@ import { XGrid } from '@material-ui/x-grid';
| <span class="prop-name">onRowOut</span> | <span class="prop-type">(param: GridRowParams, event: React.MouseEvent) => void</span> | | Callback fired when a mouse out comes from a row container element. |
| <span class="prop-name">onRowEnter</span> | <span class="prop-type">(param: GridRowParams, event: React.MouseEvent) => void</span> | | Callback fired when a mouse enter comes from a row container element. |
| <span class="prop-name">onRowLeave</span> | <span class="prop-type">(param: GridRowParams, event: React.MouseEvent) => void</span> | | Callback fired when a mouse leave event comes from a row container element. |
| <span class="prop-name">onRowSelected</span> | <span class="prop-type">(param: GridRowSelectedParams) => void</span> | | Callback fired when one row is selected. |
| <span class="prop-name">onRowsScrollEnd</span> | <span class="prop-type">(param: GridRowScrollEndParams) => void</span> | | Callback fired when scrolling to the bottom of the grid viewport. |
| <span class="prop-name">onSelectionModelChange</span> | <span class="prop-type">(param: GridSelectionModelChangeParams) => void</span> | | Callback fired when the selection state of one or multiple rows changes. |
| <span class="prop-name">onSortModelChange</span> | <span class="prop-type">(param: GridSortModelParams) => void</span> | | Callback fired when the sort model changes before a column is sorted. |
Expand Down
4 changes: 0 additions & 4 deletions docs/src/pages/components/data-grid/events/events.json
Original file line number Diff line number Diff line change
Expand Up @@ -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&#39;s mapped do the <code>keydown</code> DOM event.\nCalled with a GridColumnHeaderParams object."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export default function ControlledSelectionGrid() {
<div style={{ height: 400, width: '100%' }}>
<DataGrid
checkboxSelection
onSelectionModelChange={(newSelection) => {
setSelectionModel(newSelection.selectionModel);
onSelectionModelChange={(newSelectionModel) => {
setSelectionModel(newSelectionModel);
}}
selectionModel={selectionModel}
{...data}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ export default function ControlledSelectionGrid() {
<div style={{ height: 400, width: '100%' }}>
<DataGrid
checkboxSelection
onSelectionModelChange={(newSelection) => {
setSelectionModel(newSelection.selectionModel);
onSelectionModelChange={(newSelectionModel) => {
setSelectionModel(newSelectionModel);
}}
selectionModel={selectionModel}
{...data}
Expand Down
8 changes: 4 additions & 4 deletions packages/grid/_modules_/grid/components/GridViewport.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -38,7 +38,7 @@ export const GridViewport: ViewportType = React.forwardRef<HTMLDivElement, {}>(
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);
Expand All @@ -59,7 +59,7 @@ export const GridViewport: ViewportType = React.forwardRef<HTMLDivElement, {}>(
}
key={id}
id={id}
selected={selectionState[id] !== undefined}
selected={selectionLookup[id] !== undefined}
rowIndex={renderState.renderContext!.firstRowIdx! + idx}
>
<GridEmptyCell width={renderState.renderContext!.leftEmptyWidth} height={rowHeight} />
Expand All @@ -77,7 +77,7 @@ export const GridViewport: ViewportType = React.forwardRef<HTMLDivElement, {}>(
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}
Expand Down
6 changes: 0 additions & 6 deletions packages/grid/_modules_/grid/constants/eventsConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
Expand Down
1 change: 1 addition & 0 deletions packages/grid/_modules_/grid/hooks/features/core/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './gridState';
export * from './useGridApi';
export * from './useGridControlState';
export * from './useGridReducer';
export * from './useGridSelector';
export * from './useGridState';
Original file line number Diff line number Diff line change
@@ -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<Record<string, ControlStateItem<any>>>({});

const updateControlState = React.useCallback((controlStateItem: ControlStateItem<any>) => {
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');
}
16 changes: 15 additions & 1 deletion packages/grid/_modules_/grid/hooks/features/core/useGridState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function serialiseRow(
interface BuildCSVOptions {
columns: GridColumns;
rows: Map<GridRowId, GridRowModel>;
selectedRows?: Record<string, GridRowId>;
selectedRowIds: GridRowId[];
getCellParams: (id: GridRowId, field: string) => GridCellParams;
delimiterCharacter: GridExportCsvDelimiter;
includeHeaders?: boolean;
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 || ',',
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const selectedGridRowsCountSelector: OutputSelector<
(res: GridSelectionState) => number
> = createSelector<GridState, GridSelectionState, number>(
gridSelectionStateSelector,
(selection) => Object.keys(selection).length,
(selection) => selection.length,
);

export const selectedGridRowsSelector = createSelector<
Expand All @@ -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<string, GridRowId>,
(res: GridSelectionState) => Record<string, GridRowId>
> = createSelector<GridState, GridSelectionState, Record<string, GridRowId>>(
gridSelectionStateSelector,
(selection) =>
selection.reduce((lookup, rowId) => {
lookup[rowId] = rowId;
return lookup;
}, {}),
);
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
import { GridRowId } from '../../../models/gridRows';

export type GridSelectionState = Record<string, GridRowId>;
export type GridSelectionState = GridRowId[];
Loading

0 comments on commit 548bb01

Please sign in to comment.