Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
2 tasks done
TiagoPortfolio opened this issue Jun 29, 2022 · 15 comments
Open
2 tasks done
Labels
component: data grid This is the name of the generic UI component, not the React module! dx Related to developers' experience feature: Selection Related to the data grid Selection feature support: commercial Support request from paid users support: question Community support but can be turned into an improvement

Comments

@TiagoPortfolio
Copy link
Contributor

Order ID 💳

33240

Duplicates

  • I have searched the existing issues

Latest version

  • I have tested the 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
@TiagoPortfolio TiagoPortfolio added status: waiting for maintainer These issues haven't been looked at yet by a maintainer support: commercial Support request from paid users labels Jun 29, 2022
@alexfauquette
Copy link
Member

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

@alexfauquette alexfauquette added support: question Community support but can be turned into an improvement component: data grid This is the name of the generic UI component, not the React module! feature: Selection Related to the data grid Selection feature and removed status: waiting for maintainer These issues haven't been looked at yet by a maintainer labels Jun 29, 2022
@flaviendelangle flaviendelangle changed the title [question][datagrid] - Selection model - Is it possible to know which row was (un)selected [data grid] Selection model - Is it possible to know which row was (un)selected Jul 4, 2022
@TiagoPortfolio
Copy link
Contributor Author

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...

  • When a user row is selected, all sub-users of that user should be selected;
  • When a user row is unselected, all sub-users of that user should be unselected.

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
Copy link
Member

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

@alexfauquette alexfauquette added the dx Related to developers' experience label Jul 4, 2022
@flaviendelangle
Copy link
Member

flaviendelangle commented Jul 4, 2022

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
Copy link
Contributor Author

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
Copy link

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
Copy link
Member

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 #1141 (comment). We could pass the following reasons to onSelectionModelChange:

  • selectAll
  • unselectAll
  • selectRow
  • unselectRow

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
Copy link

ks0430 commented Jan 31, 2023

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

@johnsonav1992
Copy link

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
Copy link

@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
Copy link
Contributor Author

Hi @niralivasoya !

I used this approach I mentioned in my previous comment: #5343 (comment)

@kvenkatasivareddy
Copy link

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
Copy link

vishal-kadmos commented Aug 7, 2024

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
Copy link

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

@flaviendelangle
Copy link
Member

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
component: data grid This is the name of the generic UI component, not the React module! dx Related to developers' experience feature: Selection Related to the data grid Selection feature support: commercial Support request from paid users support: question Community support but can be turned into an improvement
Projects
None yet
Development

No branches or pull requests

10 participants