mui / mui-x

MUI X: Build complex and data-rich applications using a growing list of advanced React components, like the Data Grid, Date and Time Pickers, Charts, and more!
https://mui.com/x/
4.05k stars 1.25k forks source link

[data grid] Selection model - Is it possible to know which row was (un)selected #5343

Open TiagoPortfolio opened 2 years ago

TiagoPortfolio commented 2 years ago

Order ID 💳

33240

Duplicates

Latest version

The problem in depth 🔍

I am using the checkbox selection in the data grid component and I would like to know if it is possible to know which row was selected/unselected. If the selection model changes from [1] to [1, 2], I want to know that the row with id 2 was selected.

I was expecting to get this info from some kind of event or in the details from the onSelectionModelChange callback but the details is always an object with an undefined reason:

{
  details: {
    reason: undefined
  }
}

CodeSandbox demo with console.log: https://codesandbox.io/s/checkboxselectiongrid-demo-mui-x-forked-iy8e17?file=/demo.tsx

Is it possible already to infer which row was (un)selected or do you think it makes sense to add this to the data grid API?

Cheers!

Your environment 🌎

`npx @mui/envinfo` ``` Browser Used: Chrome System: OS: macOS 12.4 Binaries: Node: 16.13.1 - /usr/local/bin/node Yarn: 1.22.17 - /usr/local/bin/yarn npm: 8.5.0 - /usr/local/bin/npm Browsers: Chrome: 103.0.5060.53 Edge: Not Found Firefox: Not Found Safari: 15.5 npmPackages: @emotion/react: 11.9.0 => 11.9.0 @emotion/styled: 11.8.1 => 11.8.1 @mui/icons-material: 5.8.2 => 5.8.2 @mui/lab: 5.0.0-alpha.84 => 5.0.0-alpha.84 @mui/material: 5.8.2 => 5.8.2 @mui/system: 5.8.2 => 5.8.2 @mui/x-data-grid: 5.12.2 => 5.12.2 @mui/x-data-grid-generator: 5.12.2 => 5.12.2 @mui/x-data-grid-pro: 5.12.2 => 5.12.2 @mui/x-date-pickers: 5.0.0-alpha.3 => 5.0.0-alpha.3 @mui/x-date-pickers-pro: 5.0.0-alpha.3 => 5.0.0-alpha.3 @types/react: 18.0.12 => 18.0.12 react: 18.1.0 => 18.1.0 react-dom: 18.1.0 => 18.1.0 styled-components: 5.3.5 => 5.3.5 typescript: 4.7.4 => 4.7.4 ```
alexfauquette commented 2 years ago

The reason is added to every controlled state, but not all of them use it.

The easiest way to get which rows are selected/unselected is to control the selection model. If you have a state containing the current selectionModel and you get the new one from onSelectionModelChange you can find which row get added and which has been removed.

Could you provide more context about how you want to use this information to customize the grid? I do not manage to imagine a use case using newly selected/unselected rows

TiagoPortfolio commented 2 years ago

Hi @alexfauquette !

Could you provide more context about how you want to use this information to customize the grid? I do not manage to imagine a use case using newly selected/unselected rows

Sure, this would be particularly useful when we have a data grid with selection and Master detail features and when the detail panel content depends on the value of the checkbox.

Let's say we have a data grid where each row has user info and the detail panel is a list of sub-users...

If we could know exactly which row was selected or unselected, it would be easier and more performant to update the selection model we are controlling. It would be more performant because we wouldn't have to loop through the whole selection model whenever a single row was selected/unselected to figure out which sub-users should be selected/unselected. I hope this example was clear and it makes sense, if you have any questions I'm happy to clarify :)

The easiest way to get which rows are selected/unselected is to control the selection model. If you have a state containing the current selectionModel and you get the new one from onSelectionModelChange you can find which row get added and which has been removed.

That's what I am doing at the moment. I am using difference from lodash to get which id was selected/unselected but to be honest I am not happy with the code I have written to handle this 😅 :

  const selectedIds = difference(
    newSelectionModel,
    currentSelectionModel
  )

  if (selectedIds.length === 1) {
    // New id selected
  } else {
    const unselectedIds = difference(
      currentSelectionModel,
      newSelectionModel
    )

    if (unselectedIds.length === 1) {
      // New id unselected
    } else {
      // Several ids selected/unselected
    }
  }

There's probably a better way to do this but it would be nice if the onSelectionModelChange handler could have the new selected/unselected id to avoid all the boilerplate we have to write to handle this.

Cheers!

alexfauquette commented 2 years ago

Effectively, you can use difference, and I'm not sure the internal code would be much different from the following:

const selectedIds = difference(newSelectionModel, currentSelectionModel)
const unselectedIds = difference(currentSelectionModel, newSelectionModel)

Maybe we could add the previouseState to the onSelectionModelChange such that you do not have to store it yourself

flaviendelangle commented 2 years ago

That would be very easy to accomplish

--- a/packages/grid/x-data-grid/src/hooks/core/useGridStateInitialization.ts
+++ b/packages/grid/x-data-grid/src/hooks/core/useGridStateInitialization.ts
@@ -59,6 +59,7 @@ export const useGridStateInitialization = <Api extends GridApiCommon>(
         updatedControlStateIds.push({
           stateId: controlState.stateId,
           hasPropChanged: newSubState !== controlState.propModel,
+          oldSubState,
         });

         // The state is controlled, the prop should always win
@@ -90,20 +91,23 @@ export const useGridStateInitialization = <Api extends GridApiCommon>(
       }

       if (updatedControlStateIds.length === 1) {
-        const { stateId, hasPropChanged } = updatedControlStateIds[0];
+        const { stateId, hasPropChanged, oldSubState } = updatedControlStateIds[0];
         const controlState = controlStateMapRef.current[stateId];
         const model = controlState.stateSelector(newState, apiRef.current.instanceId);

         if (controlState.propOnChange && hasPropChanged) {
           const details =
             props.signature === GridSignature.DataGridPro
-              ? { api: apiRef.current, reason }
-              : { reason };
+              ? { api: apiRef.current, reason, prevValue: oldSubState }
+              : { reason, prevValue: oldSubState };
           controlState.propOnChange(model, details);
         }

         if (!ignoreSetState) {
-          apiRef.current.publishEvent(controlState.changeEvent, model, { reason });
+          apiRef.current.publishEvent(controlState.changeEvent, model, {
+            reason,
+            prevValue: oldSubState,
+          });
         }
       }
TiagoPortfolio commented 2 years ago

Yes, having the previous selection model would be useful to handle this use case and it will potentially help other use cases down the road.

I'm happy with this :)

Thanks @alexfauquette and @flaviendelangle !

MartinWebDev commented 2 years ago

Adding my own thought to this, I'd like to see the "reason" field be utilised on selectionModel change. Primarily because I'd like to know if the user used the select all checkbox from the header, so I can perform different actions when this is the case.

I am trying to maintain selectAll through page changes with server pagination. The prop for remembering non-existent rows will keep the current page selected, but I want to pre-select new pages. I am currently doing it with a hack by comparing the old and new selection models, if the difference is greater than 1 in length, then assume the user clicked select all. This of course doesn't work when user has manually selected all but one row on the current page, then selects check all, but it's better than nothing for now.

A way to tell if the user specifically clicked the select all checkbox would be great.

m4theushw commented 2 years ago

A way to tell if the user specifically clicked the select all checkbox would be great.

It's the second time someone asks for this. It was first discussed in https://github.com/mui/mui-x/issues/1141#issuecomment-850977514. We could pass the following reasons to onSelectionModelChange:

If we could know exactly which row was selected or unselected, it would be easier and more performant to update the selection model we are controlling.

When the reason for the filtering model was added in #4938, I already had in mind a meta param which we could pass any additional information useful for the user. It seems that it would be valuable here to pass which row was selected or unselected.

diff --git a/packages/grid/x-data-grid/src/hooks/core/useGridStateInitialization.ts b/packages/grid/x-data-grid/src/hooks/core/useGridStateInitialization.ts
index 569f9a54f..728116912 100644
--- a/packages/grid/x-data-grid/src/hooks/core/useGridStateInitialization.ts
+++ b/packages/grid/x-data-grid/src/hooks/core/useGridStateInitialization.ts
@@ -28,7 +28,7 @@ export const useGridStateInitialization = <Api extends GridApiCommon>(
   }, []);

   const setState = React.useCallback<GridStateApi<Api['state']>['setState']>(
-    (state, reason) => {
+    (state, reason, meta) => {
       let newState: Api['state'];
       if (isFunction(state)) {
         newState = state(apiRef.current.state);
@@ -97,8 +97,8 @@ export const useGridStateInitialization = <Api extends GridApiCommon>(
         if (controlState.propOnChange && hasPropChanged) {
           const details =
             props.signature === GridSignature.DataGridPro
-              ? { api: apiRef.current, reason }
-              : { reason };
+              ? { api: apiRef.current, reason, meta }
+              : { reason, meta };
           controlState.propOnChange(model, details);
         }

One example of value for the meta param is

diff --git a/packages/grid/x-data-grid/src/hooks/features/selection/useGridSelection.ts b/packages/grid/x-data-grid/src/hooks/features/selection/useGridSelection.ts
index 89afe9644..774134bd2 100644
--- a/packages/grid/x-data-grid/src/hooks/features/selection/useGridSelection.ts
+++ b/packages/grid/x-data-grid/src/hooks/features/selection/useGridSelection.ts
@@ -179,23 +179,32 @@ export const useGridSelection = (

       lastRowToggled.current = id;

+      const selection = gridSelectionStateSelector(apiRef.current.state);
+
       if (resetSelection) {
         logger.debug(`Setting selection for row ${id}`);

-        apiRef.current.setSelectionModel(isSelected ? [id] : []);
+        apiRef.current.setSelectionModel(
+          isSelected ? [id] : [],
+          isSelected ? 'selectRow' : 'unselectRow',
+          {
+            ids: isSelected ? [id] : selection,
+          },
+        );
       } else {
         logger.debug(`Toggling selection for row ${id}`);

-        const selection = gridSelectionStateSelector(apiRef.current.state);
         const newSelection: GridRowId[] = selection.filter((el) => el !== id);

+        const newlySelectedIds = [];
         if (isSelected) {
           newSelection.push(id);
+          newlySelectedIds.push(id);
         }

         const isSelectionValid = newSelection.length < 2 || canHaveMultipleSelection;
         if (isSelectionValid) {
-          apiRef.current.setSelectionModel(newSelection);
+          apiRef.current.setSelectionModel(newSelection, 'selectRow', { ids: newlySelectedIds });
         }
       }
     },
ks0430 commented 1 year ago

Any updates on this? It would be better to have select all logic inside tree view for parent row as well.

johnsonav1992 commented 1 year ago

Yes, I also have just come across a case where using the details/reason like @m4theushw has described would be crucial/extremely beneficial. What's the update?

niralivasoya commented 10 months ago

@TiagoPortfolio , I am currently facing a similar issue with the selection Model. How did you find the current selected row instead of all the selected rows?

TiagoPortfolio commented 10 months ago

Hi @niralivasoya !

I used this approach I mentioned in my previous comment: https://github.com/mui/mui-x/issues/5343#issuecomment-1173660066

kvenkatasivareddy commented 6 months ago

That would be very easy to accomplish

--- a/packages/grid/x-data-grid/src/hooks/core/useGridStateInitialization.ts
+++ b/packages/grid/x-data-grid/src/hooks/core/useGridStateInitialization.ts
@@ -59,6 +59,7 @@ export const useGridStateInitialization = <Api extends GridApiCommon>(
         updatedControlStateIds.push({
           stateId: controlState.stateId,
           hasPropChanged: newSubState !== controlState.propModel,
+          oldSubState,
         });

         // The state is controlled, the prop should always win
@@ -90,20 +91,23 @@ export const useGridStateInitialization = <Api extends GridApiCommon>(
       }

       if (updatedControlStateIds.length === 1) {
-        const { stateId, hasPropChanged } = updatedControlStateIds[0];
+        const { stateId, hasPropChanged, oldSubState } = updatedControlStateIds[0];
         const controlState = controlStateMapRef.current[stateId];
         const model = controlState.stateSelector(newState, apiRef.current.instanceId);

         if (controlState.propOnChange && hasPropChanged) {
           const details =
             props.signature === GridSignature.DataGridPro
-              ? { api: apiRef.current, reason }
-              : { reason };
+              ? { api: apiRef.current, reason, prevValue: oldSubState }
+              : { reason, prevValue: oldSubState };
           controlState.propOnChange(model, details);
         }

         if (!ignoreSetState) {
-          apiRef.current.publishEvent(controlState.changeEvent, model, { reason });
+          apiRef.current.publishEvent(controlState.changeEvent, model, {
+            reason,
+            prevValue: oldSubState,
+          });
         }
       }

is this is pushed ?

vishal-kadmos commented 1 month ago

Any update on this apart from using controlled state & difference? In similar situation where I would like to know which rows are unselected.

Background:

I am using Mui x data grid-pro checkbox selection. Since its server side paginated grid, keepNonExistentRowsSelected is set to true so that previously selected rows will be maintained to send to API later on along with custom state variable.

  const onRowSelection = (rows: GridRowSelectionModel) => {
    const selectedUsers = filteredPaymentUsers?.filter(
      (paymentUser: { id: string }) => rows.includes(paymentUser.id),
    );
    const mappedUserStr = selectedUsers?.map(
      (user: { userId: string; bankAccountId: string }) => {
        return `${user.userId}/${user.bankAccountId}`;
      },
    );
    // console.log("selectedUsersIds in onRowSelection", selectedUsersIds);
    setSelectedUsersIds(selectedUsersIds.concat(mappedUserStr));
  };

Now issue is, if I select Row1, it correctly concats the value to selectedUsersIds. Problem is when Row1 is checked & unchecked immediately, on checked, it concats values to selectedUsersIds but if its unchecked immediately, rows are [], and can't find a way to know which row/s are unchecked. so that I can remove this unchecked row values from selectedUsersIds. Any suggestion. Thank you 🙏 @flaviendelangle

vishal-kadmos commented 1 month ago

anyways, solved above issue in different way. but still would be great if we have this provision

flaviendelangle commented 1 month ago

I'm not working on the grid anymore Maybe @michelengelen can provide some assistance here :pray: