Hi,
Can someone help me in retaining the selected rows in UI and display the checkboxes as checked when I switch pages for those selected rows.
I would need to see some of your code, or an example if you would be so kind to provide one.. The behavior you describe is the default behavior of material-table.
Please see this live demo, which shows how this is the default behavior. Make a selection, change the page, then go back - it is still selected.
// Code from demo
import React, { useState } from "react";
import { render } from "react-dom";
import MaterialTable, { MTableBodyRow } from "material-table";
import tableIcons from "./TableIcons.js";
const rando = max => Math.floor(Math.random() * max);
const words = ["Paper", "Rock", "Scissors"];
const rawData = [];
for (let i = 0; i < 100; i++) rawData.push({ id: rando(300), name: words[i % words.length] });
const columns = [
{ title: "Id", field: "id" },
{ title: "Name", field: "name" }
];
const App = () => {
const [data, setData] = useState(rawData);
return (
<MaterialTable
data={data}
columns={columns}
options={{
selection: true,
}}
icons={tableIcons}
/>
);
};
render(<App />, document.querySelector("#root"));
Hi @oze4 , I should have been more detailed.
I was trying to implement my own paging into the material table by getting the page number and page size from the table and call the API then send that data to the material table ( I am not using the Refresh Data feature in Material table ) and this was successful however this caused above issue as the data changed every page so material table ( I think ) is treating it as a new table and re-rendering the whole table. Right now I am using 'selectionProps' to send checked rows based on the selected rows data from 'onSelectionChange' function.
This works but it's not elegant or optimised from a developer perspective as I need to change the key, here Id, to find the selected rows and I was wondering if there was another way
<MaterialTable
columns={columns}
data={data}
selection={true}
selectionProps={(rowData: any) => {
if (selectedRows.find(row => row.Id === rowData.Id)){
return {
checked: true,
}
} else ;
}}
/>
Are you using the remote data feature?
No I am not using remote data feature. I am using my own handlers to get the pagination data and using to get data from database.
In that case I think we need to take another step back... What is your goal with custom pagination? What do you want to accomplish?
Material table already handles pagination for you.
I need custom pagination as I have to apply certain filters to the API call which I have already handled using hooks and want to reuse that code.
Would you mind providing some code or a demo of what you're wanting to accomplish? I'm not sure I understand what you mean.. Are you getting your data similar to the demo below?
import React, { useState, useEffect } from "react";
import { render } from "react-dom";
import MaterialTable from "material-table";
import tableIcons from "./TableIcons.js";
const columns = [
{ title: "To-Do Id", field: "id", type: "numeric" },
{ title: "User Id", field: "userId", type: "numeric" },
{ title: "Title", field: "title" },
{ title: "Completed", field: "completed", type: "boolean" }
];
const options = {
pageSize: 100,
maxBodyHeight: 400,
}
const App = () => {
const [data, setData] = useState([]);
useEffect(() => {
// Grabbing ALL of the data that is in my database when component is mounted
fetch("https://jsonplaceholder.typicode.com/todos")
.then(resp => resp.json())
.then(result => setData(result))
.catch(error => console.trace(error))
}, []);
return (
<MaterialTable
data={data}
isLoading={!data.length}
columns={columns}
icons={tableIcons}
options={{ selection: true }}
/>
);
}
render(<App />, document.querySelector("#root"));
I think you need to keep track of tableData of each row which is generated by mat table.
@saikatstutorial we would ultimately need to see how @soumyaDivami is handling his custom pagination. It's impossible for us to help with something when we don't even know how it's being implemented.
Like if he just wanted to display custom text within the pagination, that is already built into material table so he wouldn't need to rewrite such functionality.
Without knowing what the goal of his custom pagination is, we can't offer a valid solution.
@oze4 I think it's a valid requirement that @soumyaDivami has raised. If I correctly understand him, he is grabbing limited data (based on page size) via API and binding it to material table. Where as in your example, you are grabbing all the data up-front and applying pagination on client-side.
I am exactly doing same that @soumyaDivami has mentioned. In that case, we have to maintain selected row data somehow when you traverse between pages.
Do we have any clean approach to persist selection between pages? Thanks for your help!
Are you using the remote data feature?. I asked @soumyaDivami if they were using async/paginated data (aka the remote data feature) and they said no. From what I can tell, they're grabbing all data up front and trying to implement their own pagination.
If you are using remote data, then it auto paginated for you - getting selections to stay page by page, if you are using remote data, is a different story.
At the end of the day we need more info from @soumyaDivami to offer a specific resolution.
If you are using remote data, then it auto paginated for you - getting selections to stay page by page, if you are using remote data, is a different story.
What do you mean by this? To clarify, I am using remote data feature as well but selection does not persist. Here is the sample code:
const fetchProjects = (query) => {
let payload = {
page: query.page + 1,
size: query.pageSize,
search: query.search,
};
if (query.orderDirection !== "" && query.orderBy !== undefined) {
payload.sortBy = query.orderBy.field;
payload.sortDirection = query.orderDirection;
}
return projectService.getAll(payload);
};
<MaterialTable
tableRef={tableRef}
icons={tableIcons}
columns={columns}
data={(query) =>
fetchProjects(query).then((result) => {
return {
data: result.data,
page: query.page,
totalCount: result.metadata ? result.metadata.totalRecords : 0,
};
})
}
options={{
...tableDefaultOptions,
selection: true,
showTextRowsSelected: false,
selectionProps: (rowData) => ({
disabled: rowData.isActive === false,
}),
}} />
@vireshshah
That is my point - you are using the "remote data" feature, but @soumyaDivami said he was NOT using "remote data"..
With that being said.. when using "remote data", each time you change the page, whether forward, or backward, a request is sent to your server for the data to display in the table. Your backend database is the single source of truth for the data that gets rendered
When using "remote data", nothing is cached client side automatically.
As an example, lets say you are on page 1 and you select a row. You then click 'next' to go to page 2 - when you click 'next' a request is sent to your server to get 'page 2' data. Material-table disposes of page 1 data because we are not caching anything client side - we are using the backend API/database as the single source of truth.
The long way around to fixing this would be to change the schema of the objects you are working with to include an 'isSelected' property. From there you would handle the 'onSelectionChange' event, so that you could send a PUT request to your backend so it can mark that object as selected in your database.. this way when you browse 'back' to page 1, your server returns data that says 'hey this row is already checked'..
For this, I wrote a little 'Cookies' class, which you can find below..
Cookies Class
export default class Cookies {
static get(name) {
if (document.cookie.length === 0) return null;
var c_start = document.cookie.indexOf(`${name}=`);
if (c_start === -1) return null;
c_start = c_start + name.length + 1;
var c_end = document.cookie.indexOf(";", c_start);
if (c_end == -1) c_end = document.cookie.length;
return decodeURIComponent(document.cookie.substring(c_start, c_end));
}
static set(name, value, hours) {
if (hours > 0) {
let now = new Date();
now.setTime(now.getTime() + hours * 3600 * 1000);
let date = now.toUTCString();
document.cookie =
name + `=${encodeURIComponent(value)}; expires=${date}; path=/`;
} else {
document.cookie = name + `=${encodeURIComponent(value)}; path=/`;
}
}
static remove(name) {
if (name)
document.cookie = name + `=''; expires=${new Date(1).toUTCString()}`;
}
static getAll() {
if (document.cookie.length === 0) return null;
var cookies = {};
document.cookie.split(";").forEach(pairs => {
let pair = pairs.split("=");
cookies[(pair[0] + "").trim()] = decodeURIComponent(pair[1]);
});
return cookies;
}
static check(name) {
name = this.get(name);
return name && name !== "" ? true : false;
}
}
This is the code used in the live demo.. let me know if you have any questions..
import React, { useState, useEffect } from "react";
import { render } from "react-dom";
import MaterialTable, { MTableToolbar } from "material-table";
import tableIcons from "./TableIcons.js";
import Cookies from "./cookies";
const columns = [
{ title: "Id", field: "id", type: "numeric" },
{ title: "Email", field: "email" },
{ title: "First Name", field: "first_name" }
];
const App = () => {
const [data, setData] = useState([]);
const [selectedRows, setSelectedRows] = useState([]);
const distinct = (value, index, self) => self.indexOf(value) === index;
useEffect(() => {
Cookies.set("selected_rows", JSON.stringify(selectedRows), 1);
}, [selectedRows]);
const findSelectedRows = (cookieRows, data) => {
const dataCopy = [...data];
if (cookieRows && cookieRows.length) {
cookieRows.forEach(row => {
const found = dataCopy.find(obj => Number(obj.id) === Number(row));
dataCopy[dataCopy.indexOf(found)] = { ...found, tableData: { checked: true } };
});
}
return dataCopy;
}
const getData = query => {
return new Promise((resolve, reject) => {
fetch(`https://reqres.in/api/users?per_page=${query.pageSize}&page=${query.page + 1}`)
.then(response => response.json())
.then(result => {
const rowsFromCookie = JSON.parse(Cookies.get("selected_rows"));
const _data = findSelectedRows(rowsFromCookie, result.data);
setSelectedRows(rowsFromCookie);
setData(_data);
return resolve({
data: _data,
page: result.page - 1,
totalCount: result.total
});
}).catch(err => {
console.trace(err);
reject(err);
});
});
};
const handleSelectionChange = (currentlySelectedRows, clickedRow) => {
const rowsFromCookie = JSON.parse(Cookies.get("selected_rows"));
const isChecking = clickedRow && clickedRow.tableData ? true : false;
if (!isChecking || isChecking && clickedRow.tableData.checked) {
let newrows = [...currentlySelectedRows.map(r => r.id)];
if (rowsFromCookie.length) {
newrows = [...newrows, ...rowsFromCookie]
}
setSelectedRows(newrows.filter(distinct));
} else {
const finalRow = rowsFromCookie.filter(rfc => rfc !== clickedRow.id) || [];
setSelectedRows(finalRow.filter(distinct));
}
};
const components = {
Toolbar: props => <MTableToolbar {...{ ...props, selectedRows }} />,
}
return (
<MaterialTable
data={getData}
isLoading={!data.length}
columns={columns}
icons={tableIcons}
options={{ selection: true }}
onSelectionChange={handleSelectionChange}
components={components}
/>
);
};
render(<App />, document.querySelector("#root"));
Thanks @oze4 for your help with great example! One thing I noticed, is when you click "Select All" it adds the selected rows to cookie but doesn't remove those data when you uncheck "Select All". Because "clickedRow" argument of handleSelectionChange is undefined. I think I have to maintain local variable as well to maintain array of currently selected rows for specific page and use that when user uncheck "Select All". What do you suggest?
Also I see slight performance degradation after introducing handleSelectionChange event as I can see some delay when you check/uncheck item. Any workaround for that?
I am also getting below warning Warning: Failed prop type: Invalid prop `data` supplied to `MTableActions`. I am using Toolbar component exactly in same way like yours:
<MTableToolbar {...{ ...props, selectedRows }} />
Thanks again for your help!
Hey @vireshshah that was just a quick demo I whipped up to show how your overall goal can be achieved. I tried to work out the glaring or 'obvious' bugs, but it can definitely be elaborated on.
As far as the issues you're having, I am not sure - I would have to dig into them. Although, you should be able to tweak the code so that you get what you're looking for.
Sure thing. I would take care of that. Thanks for your help!
@oze4 I am little bit struggling to fix couple of issues. Please see, if you can give some pointers. Thanks!
As we're updating state in getData, I am getting https://github.com/mbrn/material-table/issues/748 issue now where search focus gets lost.
When we're overriding toolbar, somehow it re-renders my child components even I am using React.memo. Please check below code. Even though CustomFilters component doesn't have any props, it still gets re-rendered.
const CustomFilters = React.memo(() => {
return (
<div className="filters-row">
<Grid container spacing={3}>
<Grid item className="deliverableFilter">
<Formik initialValues={filters}>
<DeliverableDropdown
id="deliverableFilter"
placeholder="Filter by deliverable"
onChange={onDeliverableFilterChange}
/>
</Formik>
</Grid>
<Grid item>
<Select
className="projectsStatusFilter"
value={filters.projectsStatus}
onChange={onProjectsStatusFilterChange}
displayEmpty
>
<MenuItem value="">Active and inactive projects</MenuItem>
<MenuItem value={projectStatus.ACTIVE}>Active projects</MenuItem>
<MenuItem value={projectStatus.INACTIVE}>Inactive projects</MenuItem>
</Select>
</Grid>
</Grid>
</div>
);
});
const tableComponents = {
Toolbar: (props) => (
<div className="toolbar">
<MTableToolbar {...{ ...props, selectedRows }} />
<CustomFilters />
</div>
),
};
<MaterialTable
title=""
tableRef={tableRef}
icons={tableIcons}
columns={columns}
data={fetchProjects}
actions={tableActions}
components={tableComponents}
options={tableOptions}
...
....
@vireshshah I think it would be easiest if you could provide a minimal reproducible example using CodeSandbox or StackBlitz.. The search losing focus is happening because of how you are setting state - something is setting state when you type, which causes the rerender and ultimately the loss of focus.
Does the demo I provided exhibit the same behavior? Meaning, if you type in the search does it lose focus? I'm not by a computer or else I would check....
At the end if the day, in order to help, I would need to see exactly how you're using Material Table..otherwise I'm just blindly guessing.
@oze4 Here we go. I forked from your shared code and updated it to show how I am using it. Please check here. So here are the issues, where I would really appreciate your help. Thanks!
CustomFilters component. Even though it doesn't have any props, it still gets re-rendered whenever you change value from filter dropdown or search anything (which updates the state values).MTableActions accepts prop data as well but not sure why it's complaining. Warning: Failed prop type: Invalid prop
datasupplied toMTableActions.
in MTableActions (created by MTableToolbar)
@vireshshah It doesn't look like the search feature and remote data feature play nicely together.
Click me to view troubleshooting code
// Now when you search it should work, but pagination breaks...
import React, { useRef, useState, useEffect } from "react";
import { render } from "react-dom";
import MaterialTable, { MTableToolbar } from "material-table";
import tableIcons from "./TableIcons.js";
import {
Button,
Link,
Grid,
Tooltip,
Typography,
MenuItem,
Select
} from "@material-ui/core";
import Cookies from "./cookies";
const columns = [
{ title: "Id", field: "id", type: "numeric" },
{ title: "Email", field: "email" },
{ title: "First Name", field: "first_name" }
];
const App = () => {
const [data, setData] = useState([]);
const [selectedRows, setSelectedRows] = useState([]);
const tableRef = useRef(null);
const [filters, setFilters] = useState({
projectsStatus: 1,
deliverable: null
});
const distinct = (value, index, self) => self.indexOf(value) === index;
// Load data at 'mount' (I have heard using an empty dependency array is not best practices)
useEffect(() => {
getData({
// Could prob clean this up to pull these values from Material Table
pageSize: 5,
page: 1,
});
}, []);
useEffect(() => {
Cookies.set("selected_rows", JSON.stringify(selectedRows), 1);
}, [selectedRows]);
const findSelectedRows = (cookieRows, data) => {
const dataCopy = [...data];
if (cookieRows && cookieRows.length) {
cookieRows.forEach(row => {
const found = dataCopy.find(obj => Number(obj.id) === Number(row));
dataCopy[dataCopy.indexOf(found)] = {
...found,
tableData: { checked: true }
};
});
}
return dataCopy;
};
const getData = query => {
console.log(`[API_QUERY] ${data.length ? "Reloading" : "Loading"} data...`);
return new Promise((resolve, reject) => {
fetch(
`https://reqres.in/api/users?per_page=${
query.pageSize
}&page=${query.page + 1}`
)
.then(response => response.json())
.then(result => {
const rowsFromCookie = JSON.parse(Cookies.get("selected_rows"));
const _data = findSelectedRows(rowsFromCookie, result.data);
setSelectedRows(rowsFromCookie);
setData(_data);
return resolve({
data: _data,
page: result.page - 1,
totalCount: result.total
});
})
.catch(err => {
console.trace(err);
reject(err);
});
});
};
const handleSelectionChange = (currentlySelectedRows, clickedRow) => {
const rowsFromCookie = JSON.parse(Cookies.get("selected_rows"));
const isChecking = clickedRow && clickedRow.tableData ? true : false;
if (!isChecking || (isChecking && clickedRow.tableData.checked)) {
let newrows = [...currentlySelectedRows.map(r => r.id)];
if (rowsFromCookie.length) {
newrows = [...newrows, ...rowsFromCookie];
}
setSelectedRows(newrows.filter(distinct));
} else {
const finalRow =
rowsFromCookie.filter(rfc => rfc !== clickedRow.id) || [];
setSelectedRows(finalRow.filter(distinct));
}
};
const onProjectsStatusFilterChange = event => {
setFilters({
...filters,
projectsStatus: event.target.value
});
tableRef.current.onQueryChange();
};
const CustomFilters = React.memo(() => {
console.log("CustomFilters called");
return (
<div className="filters-row">
<Grid container spacing={3}>
<Grid item>
<Select
className="projectsStatusFilter"
value={filters.projectsStatus}
onChange={onProjectsStatusFilterChange}
displayEmpty
>
<MenuItem value="">Active and inactive projects</MenuItem>
<MenuItem value={1}>Active projects</MenuItem>
<MenuItem value={2}>Inactive projects</MenuItem>
</Select>
</Grid>
</Grid>
</div>
);
});
const components = {
Toolbar: props => (
<div className="toolbar">
<MTableToolbar {...{ ...props, selectedRows }} />
{<CustomFilters />}
</div>
)
};
return (
<MaterialTable
//data={getData}
data={data}
tableRef={tableRef}
isLoading={!data.length}
columns={columns}
icons={tableIcons}
options={{ selection: true }}
onSelectionChange={handleSelectionChange}
components={components}
/>
);
};
render(<App />, document.querySelector("#root"));
I have not had much time to mess around with it, but if you use useEffect to grab data versus the built in "remote data" feature then the search works fine.. Obviously, this messes up pagination so it's not a final solution.
At a really high level, a final solution is most likely going to be overriding the pagination component and using your own plumbing for tracking pagination. Esentially, it's going to have to be custom. I agree this is something that should be made easier for people to use in Material Table.
Thanks @oze4 for looking into this.
It doesn't look like the search feature and remote data feature play nicely together.
I think search and remote data works fine however issue seems when we override custom toolbar. Please see updated demo:
If you comment out components property on material table, it works fine. And I think issue no "2" which I mentioned in my previous comment is also related to that. Somehow focus is lost when it re-renders the custom toolbar.
Am I missing something here? Do you have any other recommendation to override toolbar? As I would like to put some filter controls as well in the toolbar. Please see issue "2" which I am facing where it re-renders the CustomFilters component even if it's wrapped under React.memo and no property is changed.
Thanks again for all your help so far!
Thanks @oze4. I would put the question on stackoverflow. @mbrn Just wanted to check with you whether you have any insights/workarounds on this as it's an interesting bug.
@vireshshah very interesting bug for sure. Even when commenting out the components the search does not work as expected. Each time you type in the search, a request is sent to the server (open your devtools, go to the network tab).. So, even when commenting out components, the table is in fact re-rendering (which you can also see using React devtools extension).
@oze4 I think it is expected behavior to send request to server when you search because we are not fetching data up-front. Correct?
I asked question in stackoverflow: https://stackoverflow.com/questions/62162992/material-table-toolbar-losing-focus as well but have not heard back. Do you think, we would be able to prioritize and fix this bug soon?
Thanks!
@vireshshah I believe you are correct here.
My train of thought was just searching data for the current page, since material-table has that data in state, but just searching one page doesn't give you much (brain fart lol 馃槩 )
It would appear that the search functionality behaves differently when using remote data. I am not too familiar with what is under the hood as far as searching when using remote data, but I can begin to take a look. I will also chat with @mbrn about this.
I would expect the search query to be sent back to the server when a user is typing, but I don't see any query params or body data that specifies the search term in the request(s) that are sent when typing in the search bar. So, we most likely need to refine how search works when using remote data.
PS: we are always accepting pull requests 馃槣
Thanks @oze4. I wish I could contribute but somehow not able to take time out now a days but will see.
BTW, on side note, how can I make sure that action having toolbarOnSelect position is always visible? If I add freeAction property to true, toolbar button appears but as soon as you select any row, it disappears. My requirement is to show that button all the time along with selection capability. Is it possible?
I would need to see some of your code, or an example if you would be so kind to provide one.. The behavior you describe is the default behavior of material-table.
Please see this live demo, which shows how this is the default behavior. Make a selection, change the page, then go back - it is still selected.
// Code from demo import React, { useState } from "react"; import { render } from "react-dom"; import MaterialTable, { MTableBodyRow } from "material-table"; import tableIcons from "./TableIcons.js"; const rando = max => Math.floor(Math.random() * max); const words = ["Paper", "Rock", "Scissors"]; const rawData = []; for (let i = 0; i < 100; i++) rawData.push({ id: rando(300), name: words[i % words.length] }); const columns = [ { title: "Id", field: "id" }, { title: "Name", field: "name" } ]; const App = () => { const [data, setData] = useState(rawData); return ( <MaterialTable data={data} columns={columns} options={{ selection: true, }} icons={tableIcons} /> ); }; render(<App />, document.querySelector("#root"));
Hi @oze4, I would like to do the constrained default behavior. Check the box, change page and return to previous page, the check box will be unchecked as default. How can I do that?
This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. You can reopen it if it required.
Most helpful comment
@vireshshah
That is my point - you are using the "remote data" feature, but @soumyaDivami said he was NOT using "remote data"..
If you don't feel like reading all of this, scroll down to 'Fix 2' to see solution
With that being said.. when using "remote data", each time you change the page, whether forward, or backward, a request is sent to your server for the data to display in the table. Your backend database is the single source of truth for the data that gets rendered
When using "remote data", nothing is cached client side automatically.
As an example, lets say you are on page 1 and you select a row. You then click 'next' to go to page 2 - when you click 'next' a request is sent to your server to get 'page 2' data. Material-table disposes of page 1 data because we are not caching anything client side - we are using the backend API/database as the single source of truth.
Fix 1 Long Way Around
The long way around to fixing this would be to change the schema of the objects you are working with to include an 'isSelected' property. From there you would handle the 'onSelectionChange' event, so that you could send a PUT request to your backend so it can mark that object as selected in your database.. this way when you browse 'back' to page 1, your server returns data that says 'hey this row is already checked'..
Fix 2 Cache selected rows client side with cookies
You can view a live demo of this here
For this, I wrote a little 'Cookies' class, which you can find below..
Cookies Class
This is the code used in the live demo.. let me know if you have any questions..