Inline MaterialTable Edit with DropDown - reactjs

I'm attempting to create a MaterialTable with an inline editable field that has a dropdown. The problem seems to be in the columns object. With the lookup attribute, one can specify key:value pairs as dropdown list items. My dilemma seems to be that I am not able to iterate over a list and add the key-value pairs in the dynamic fashion below. It seems to only work when written like lookup:{ 1: "Test Value", 2: "Test Value 2" }. Please explain if my understanding is incorrect.
<MaterialTable
title="Available Attributes"
icons={this.tableIcons}
data={availableAttributesList}
columns={[
{ title: 'Attribute', field: 'name' },
{
title: 'Data Type',
field: 'dataType',
lookup: { dataTypePayload.map((attribute, name) => ({
attribute.id : attribute.dataType
}))}
}
]}
options={{
actionsColumnIndex: -1
}}
editable={{
onRowAdd: newData =>
new Promise((resolve, reject) => {
setTimeout(() => {
{
const data = editableAvailableAttributesList;
data.push(newData);
this.setState({ data }, () => resolve());
}
resolve();
}, 1000);
}),
onRowUpdate: (newData, oldData) =>
new Promise((resolve, reject) => {
setTimeout(() => {
{
const data = editableAvailableAttributesList;
const index = data.indexOf(oldData);
data[index] = newData;
this.setState({ data }, () => resolve());
}
resolve();
}, 1000);
}),
onRowDelete: oldData =>
new Promise((resolve, reject) => {
setTimeout(() => {
{
let data = availableAttributesList;
const index = data.indexOf(oldData);
data.splice(index, 1);
this.setState({ data }, () => resolve());
}
resolve();
}, 1000);
})
}}
/>

The map creates an array of objects, which you have then placed inside another object.
As you've noticed, this won't work. To get the desired format, try this:
<MaterialTable
// ...other props
columns={[
{ title: 'Attribute', field: 'name' },
{
title: 'Data Type',
field: 'dataType',
lookup: dataTypePayload.reduce((acc: any, attribute: any) => {
acc[attribute.id] = attribute.dataType
return acc
}, {})
}
]}
// ...other props
/>
Hope that helps!

Related

REACT- Displaying and filtering specific data

I want to display by default only data where the status are Pending and Not started. For now, all data are displayed in my Table with
these status: Good,Pending, Not started (see the picture).
But I also want to have the possibility to see the Good status either by creating next to the Apply button a toggle switch : Show good menus, ( I've made a function Toggle.jsx), which will offer the possibility to see all status included Good.
I really don't know how to do that, here what I have now :
export default function MenuDisplay() {
const { menuId } = useParams();
const [selected, setSelected] = useState({});
const [hidden, setHidden] = useState({});
const [menus, setMenus] = useState([]);
useEffect(() => {
axios.post(url,{menuId:parseInt(menuId)})
.then(res => {
console.log(res)
setMenus(res.data.menus)
})
.catch(err => {
console.log(err)
})
}, [menuId]);
// If any row is selected, the button should be in the Apply state
// else it should be in the Cancel state
const buttonMode = Object.values(selected).some((isSelected) => isSelected)
? "apply"
: "cancel";
const rowSelectHandler = (id) => (checked) => {
setSelected((selected) => ({
...selected,
[id]: checked
}));
};
const handleClick = () => {
if (buttonMode === "apply") {
// Hide currently selected items
const currentlySelected = {};
Object.entries(selected).forEach(([id, isSelected]) => {
if (isSelected) {
currentlySelected[id] = isSelected;
}
});
setHidden({ ...hidden, ...currentlySelected });
// Clear all selection
const newSelected = {};
Object.keys(selected).forEach((id) => {
newSelected[id] = false;
});
setSelected(newSelected);
} else {
// Select all currently hidden items
const currentlyHidden = {};
Object.entries(hidden).forEach(([id, isHidden]) => {
if (isHidden) {
currentlyHidden[id] = isHidden;
}
});
setSelected({ ...selected, ...currentlyHidden });
// Clear all hidden items
const newHidden = {};
Object.keys(hidden).forEach((id) => {
newHidden[id] = false;
});
setHidden(newHidden);
}
};
const matchData = (
menus.filter(({ _id }) => {
return !hidden[_id];
});
const getRowProps = (row) => {
return {
style: {
backgroundColor: selected[row.values.id] ? "lightgrey" : "white"
}
};
};
const data = [
{
Header: "id",
accessor: (row) => row._id
},
{
Header: "Name",
accessor: (row) => (
<Link to={{ pathname: `/menu/${menuId}/${row._id}` }}>{row.name}</Link>
)
},
{
Header: "Description",
//check current row is in hidden rows or not
accessor: (row) => row.description
},
{
Header: "Status",
accessor: (row) => row.status
},
{
Header: "Dishes",
//check current row is in hidden rows or not
accessor: (row) => row.dishes,
id: "dishes",
Cell: ({ value }) => value && Object.values(value[0]).join(", ")
},
{
Header: "Show",
accessor: (row) => (
<Toggle
value={selected[row._id]}
onChange={rowSelectHandler(row._id)}
/>
)
}
];
const initialState = {
sortBy: [
{ desc: false, id: "id" },
{ desc: false, id: "description" }
],
hiddenColumns: ["dishes", "id"]
};
return (
<div>
<button type="button" onClick={handleClick}>
{buttonMode === "cancel" ? "Cancel" : "Apply"}
</button>
<Table
data={matchData}
columns={data}
initialState={initialState}
withCellBorder
withRowBorder
withSorting
withPagination
rowProps={getRowProps}
/>
</div>
);
}
Here my json from my api for menuId:1:
[
{
"menuId": 1,
"_id": "123ml66",
"name": "Pea Soup",
"description": "Creamy pea soup topped with melted cheese and sourdough croutons.",
"dishes": [
{
"meat": "N/A",
"vegetables": "pea"
}
],
"taste": "Good",
"comments": "3/4",
"price": "Low",
"availability": 0,
"trust": 1,
"status": "Pending",
"apply": 1
},
//...other data
]
Here my CodeSandbox
Here a picture to get the idea:
Here's the second solution I proposed in the comment:
// Setting up toggle button state
const [showGood, setShowGood] = useState(false);
const [menus, setMenus] = useState([]);
// Simulate fetch data from API
useEffect(() => {
async function fetchData() {
// After fetching data with axios or fetch api
// We process the data
const goodMenus = dataFromAPI.filter((i) => i.taste === "Good");
const restOfMenus = dataFromAPI.filter((i) => i.taste !== "Good");
// Combine two arrays into one using spread operator
// Put the good ones to the front of the array
setMenus([...goodMenus, ...restOfMenus]);
}
fetchData();
}, []);
return (
<div>
// Create a checkbox (you can change it to a toggle button)
<input type="checkbox" onChange={() => setShowGood(!showGood)} />
// Conditionally pass in menu data based on the value of toggle button "showGood"
<Table
data={showGood ? menus : menus.filter((i) => i.taste !== "Good")}
/>
</div>
);
On ternary operator and filter function:
showGood ? menus : menus.filter((i) => i.taste !== "Good")
If button is checked, then showGood's value is true, and all data is passed down to the table, but the good ones will be displayed first, since we have processed it right after the data is fetched, otherwise, the menus that doesn't have good status is shown to the UI.
See sandbox for the simple demo.

using api to call users and material ui data grid to show users

i am working on an application that I make API calls to get some users for an id. the API gives you the users in an object of 25 length, and i order to get the other users u have to make other API calls.
I have a parent component from which I look for users and I pass down some variables to my child component:
<UserSection
id={id}
code={code}
open={open}
users={users}
setOpen={setOpen}
handleClose={handleClose}
handleUsers={handleUsers}
total={total}
currentPageNr={currentPageNr}
maxPageNr={maxPageNr}
/>
then in my child component I am using the material ui data grid as follows:
const [rowsState, setRowsState] = React.useState({
page: 0,
pageSize: 25,
rows: [],
loading: false,
});
const rows = [];
useEffect(() => {
let active = true;
(async () => {
setRowsState((prev) => ({ ...prev, loading: true }));
await fetchAllUsers(rowsState.page);
for (let i = 0; i < users.length; i++) {
if (users[i].campaign_id == props.id) {
let row = {
id: i + 1,
col1: i + 1,
col2: users[i].id,
col3: users[i].first_name,
col4: users[i].qualified,
col5: users[i].email,
col6: users[i].source,
col7: users[i].referrer_id,
col8: showName(users[i].referrer_id),
// col9: props.users[i].url,
col10: justSHowReached(users[i].id),
col11: users.filter(
(u) => u.referrer_id == users[i].id && u.qualified
).length,
col12: changeDate(users[i].date),
// col13: "",
};
rows[i] = row;
}
}
const newRows = rows;
console.log("new rows:", newRows);
console.log("eowsState.page:", rowsState.page);
// console.log("===**=== rowsState.pageSize:", rowsState.pageSize);
if (!active) {
return;
}
setRowsState((prev) => ({ ...prev, loading: false, rows: newRows }));
})();
return () => {
active = false;
};
}, [rowsState.page, rowsState.pageSize]);
and this is how I try to fetch users based on page number:
const fetchAllUsers = async (pageNumber) => {
console.log("----------------------------------");
console.log("page number: ", pageNumber);
console.log("----------------------------------");
await fetch(
`........./api/v1/users?page=${pageNumber}`,
{
method: "GET",
headers: new Headers({
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
}),
}
)
.then((res) => res.json())
.then(async (data) => {
// console.log("=================rows=================");
setUsers(data.data);
return data.data;
})
.catch((error) => {
console.log(error);
});
};
so I set my users here which I want to use on data model.
and also
const columns = [
{ field: "col1", headerName: "#", width: 50 },
{ field: "col2", headerName: "Id", width: 100, sortable: false },
{ field: "col3", headerName: "Name", width: 100 },
{ field: "col4", headerName: "Qualified", width: 100 },
{ field: "col5", headerName: "Email", width: 200 },
{ field: "col6", headerName: "Source", width: 75 },
{ field: "col7", headerName: "Referrer Id", width: 125 },
{ field: "col8", headerName: "Referrer Name", width: 125 },
// { field: "col9", headerName: "Url", width: 300 },
{
field: "col10",
headerName: "Reached",
width: 150,
},
{ field: "col11", headerName: "Qualified", width: 150 },
{ field: "col12", headerName: "Date Created", width: 150 },
{
field: "col13",
headerName: "Action",
width: 150,
sortable: false,
filterable: false,
hideable: false,
renderCell: (params) => {
const onClick = (e) => {
e.stopPropagation(); // don't select this row after clicking
const api = params.api;
const thisRow = {};
api
.getAllColumns()
.filter((c) => c.field !== "__check__" && !!c)
.forEach(
(c) => (thisRow[c.field] = params.getValue(params.id, c.field))
);
console.log("---->", thisRow.col2, thisRow.col4);
setUserId(thisRow.col2);
updateUser(thisRow.col2, thisRow.col4);
// return alert(JSON.stringify(thisRow, null, 4));
};
return (
<>
<Button variant="contained" onClick={onClick}>
update
</Button>
</>
);
},
},
];
this is how I make my model:
<DataGrid
// rows={rows}
columns={columns}
pagination
rowCount={props.total}
paginationMode="server"
// pageSize={25}
rowsPerPageOptions={[25]}
{...rowsState}
onPageChange={(page) => {
// console.log("and page is ", page);
setRowsState((prev) => ({ ...prev, page }));
}}
onPageSizeChange={(pageSize) =>
setRowsState((prev) => ({ ...prev, pageSize }))
}
/>
the problem is that I load users but I wont be able to show them inside my model
here u can see:
I am loading 25 users but the model doesn't show anything, however it shows me 1–25 of 5101 when i click on > I can load the users on my model like but now I am on 26–50 of 5101 so I am in page 2 but I am showing the data for page 1, when i click on > again I can see that this works but I am always behinds the correct page and sometimes Im in page 6 but I am still seeing data for page 2, and I can see that model is not being updated correctly.
on my dependency on my useEffect I have [rowsState.page, rowsState.pageSize], while the toturial says I need to have 3, and the 3rd one is the rows, if I put users there, the app will keep rendering and eventually firefox will crush. How can i make sure I am getting the correct data for every page and also how to load the data directly to the model?
There's a lot going on here, and I think your component is overall too complex. If you try and simplify things you might make it easier to see the problem.
First, I'd move the fetchAllUsers out to a separate method - it doesn't need to use state, it can just be a simple wrapper around an API call. Also, given that it's fetching a subset of users, it should probably not be called "fetchAllUsers". And, you're mixing async/await with promises - just stick with using async/await. Something like this might work
const fetchUsersForPage = async (pageNumber) => {
try {
const response = await fetch(
// Copied from your code but looks very suspicious...
`........./api/v1/users?page=${pageNumber}`,
{
method: "GET",
headers: new Headers({
"Content-Type": "application/json",
Authorization: `Bearer ${token}`,
}),
});
const { data } = await response.json();
return data;
} catch (error) {
console.log(error);
}
};
I'd also suggest you encapsulate the loading of the paged data into a separate hook. This article does a good job of explaining why you should use custom hooks for encapsulation. Your effect also has a dependency on the props.id which looks like it's a campaign id. Again, something like this might work - there's a few red flags in there which I've commented in the code below:
const usePagedData = (campaignId, page, pageSize) => {
const [loading, setLoading] = useState(false);
const [rows, setRows] = useState([]);
useEffect(() => {
const loadPageData = async () => {
setLoading(true);
const users = await fetchUsersForPage(page);
const userRows = users
// This looks suspicious - you might be losing users because they
// don't match the campaign? Shouldn't you pass the campaignId
// as part of the fetch in that case?
.filter(user => user.campaign_id === campaignId)
.map((user, index) => ({
id: index + 1,
col1: index + 1,
col2: user.id,
col3: user.first_name,
col4: user.qualified,
col5: user.email,
col6: user.source,
col7: user.referrer_id,
// Not sure what these functions are??
col8: showName(user.referrer_id),
// col9: props.users[i].url,
col10: justSHowReached(user.id),
// This number will almost certainly be wrong since 'users' is
// the list of users for this page.
col11: users.filter(u => u.referrer_id == user.id && u.qualified).length,
col12: changeDate(user.date),
// col13: "",
}));
setRows(userRows);
}
loadPageData();
}, [campaignId, page, pageSize]);
return {
rows,
loading
}
}
Now your component that contains the data grid can use your custom hook as follows:
const { rows, loading } = usePagedData(props.id, page, pageSize);

How can I delete an item inside a nested array with Hooks?

I am trying to remove a single item from state inside a nested array, but i am really struggling to understand how.
My data looks as follows, and I'm trying to remove one of the 'variants' objects on click.
const MYDATA = {
id: '0001',
title: 'A good title',
items: [
{
itemid: 0,
title: 'Cheddar',
variants: [
{ id: '062518', grams: 200, price: 3.00},
{ id: '071928', grams: 400, price: 5.50},
]
},
{
itemid: 1,
title: 'Edam',
variants: [
{ id: '183038', grams: 220, price: 2.50},
{ id: '194846', grams: 460, price: 4.99},
]
},
{
itemid: 2,
title: 'Red Leicester',
variants: [
{ id: '293834', grams: 420, price: 4.25},
{ id: '293837', grams: 660, price: 5.99},
]
}
]
}
Against each variant is a button which calls a remove function, which (should) remove the deleted item and update the state. However, this is not happening and I'm not sure what I am doing wrong.
const [myCheeses, setMyCheeses] = useState(MYDATA);
const removeMyCheese = (variantID, itemindx) => {
console.log(variantID);
setMyCheeses((prev) => {
const items = myCheeses.items[itemindx].variants.filter(
(variant) => variant.id !== variantID
);
console.log(items, itemindx);
return {
...myCheeses.items[itemindx].variants,
items
};
});
};
An example of the issue I'm facing can be seen here
https://codesandbox.io/s/funny-dan-c84cr?file=/src/App.js
Any help would be truly appreciated.
The issue is that, setMyCheeses function not returning the previous state including your change(removal)
Try one of these functions;
1st way
const removeMyCheese = (variantID, itemindx) => {
setMyCheeses((prev) => {
const items = myCheeses.items[itemindx].variants.filter(
(variant) => variant.id !== variantID
);
const newState = prev;
newState.items[itemindx].variants = items;
return {...newState};
});
};
https://codesandbox.io/s/bold-worker-b12x1?file=/src/App.js
2nd way
const removeMyCheese = (variantID, itemindx) => {
setMyCheeses((prev) => {
const items = myCheeses.items.map((item, index) => {
if (itemindx === index) {
return {
...item,
variants: item.variants.filter(
(variant) => variant.id !== variantID
)
};
} else {
return item;
}
});
return { ...prev, items: items };
});
};
https://codesandbox.io/s/sharp-forest-qhhwd
try this function, it's work for me :
const removeMyCheese = (variantID, itemindx) => {
//console.log(variantID);
const newMyCheeses = myCheeses;
const newItems = newMyCheeses.items.map((item) => {
return {
...item,
variants: item.variants.filter((variant) => variant.id !== variantID)
};
});
setMyCheeses({ ...newMyCheeses, items: newItems });
};
https://codesandbox.io/s/jolly-greider-fck6p?file=/src/App.js
Or, you can do somthing like this if you don't like to use the map function :
const removeMyCheese = (variantID, itemindx) => {
//console.log(variantID);
const newMyCheeses = myCheeses;
const newVariants = newMyCheeses.items[itemindx].variants.filter(
(variant) => variant.id !== variantID
);
newMyCheeses.items[itemindx].variants = newVariants;
setMyCheeses({ ...newMyCheeses });
};

React Material-Table editing from props using hooks

I am building an application that will request data from an API and display it in an editable table, where the user can edit and update the data base. I am using React with material-ui and material-table.
I will initialize the data in the state of the parent component, and pass it as props to the child component that renders the table. For test purposes, I initialize the data in the state to simulate later implementation of props. The table renders correctly, but when I edit, the values don't change.
export default function Table(props){
const [gridData, setGridData] = useState({
data: [
{ param: "Admin", val: "0.03" },
{ param: "Margin", val: "0.4" },
{ param: "Price", val: "5080" },
],
resolve: () => {}
});
useEffect(() => {
gridData.resolve();
}, [gridData]);
const onRowUpdate = (newData, oldData) =>
new Promise((resolve, reject) => {
const { data } = gridData;
const index = data.indexOf(oldData);
data[index] = newData;
setGridData({ ...gridData, data, resolve });
});
const { data } = gridData;
return (
<div>
<MaterialTable
columns={props.col}
data={data}
editable={{
isEditable: rowData => true,
isDeletable: rowData => true,
onRowUpdate: onRowUpdate
}}
/>
</div>
);
}
Now, I found that the table works properly when I replace the columns={props.col} line with this:
columns={[
{ title: 'Parameters', field: 'param', editable: 'never' },
{ title: 'Value', field: 'val', editable: 'onUpdate' }
]}
So it appears that my problem is with the columns and not the data.
Any help would be greatly appreciated!
NOTE:
the code is based on this response from github: https://github.com/mbrn/material-table/issues/1325
EDIT:
The columns are passed from the parent component like this:
const comonscol = [
{ title: 'Parameters', field: 'param', editable: 'never' },
{ title: 'Value', field: 'val', editable: 'onUpdate' }
];
export default function ParamsSection(props) {
...
return (
<div>
...
<Table col={comonscol} data={dummy2} />
...
</div>
);
}
I'm not quite sure about what causing this issue but it seems that MaterialTable component doesn't trigger a re-render when columns data passed as a porps.
Here is how I fixed it:
First Approach:
Create a new state for columns and trigger re-render by updating the columns via useEffect:
const [gridData, setGridData] = useState(props.data);
const [columns, setcolumns] = useState(props.col);
useEffect(() => {
gridData.resolve();
// update columns from props
setcolumns(props.col);
}, [gridData, props.col]);
...
const onRowUpdate = (newData, oldData) =>
new Promise((resolve, reject) => {
// Reset the columns will trigger re-render as the state has changed
// then it will update by useEffect
setcolumns([]);
const { data } = gridData;
const updatedAt = new Date();
const index = data.indexOf(oldData);
data[index] = newData;
setGridData({ ...gridData, data, resolve, updatedAt });
});
codeSandbox Example.
Second Approach:
Merge data, columns into a state of object and make a copy of props data then use that copy. (I've changed the date structure a bit for testing)
// Parent
const data = [
{ param: "Admin", val: "0.03" },
{ param: "Margin", val: "0.4" },
{ param: "Price", val: "5080" }
];
const comonscol = [
{ title: "Parameters", field: "param" },
{ title: "Value", field: "val" }
];
...
<Table col={comonscol} data={data} />
// Table.js
const [gridData, setGridData] = useState({
data: props.data,
columns: props.col,
resolve: () => {},
updatedAt: new Date()
});
const onRowUpdate = (newData, oldData) =>
new Promise((resolve, reject) => {
// Copy current state data to a new array
const data = [...gridData.data];
// Get edited row index
const index = data.indexOf(oldData);
// replace old row
data[index] = newData;
// update state with the new array
const updatedAt = new Date();
setGridData({ ...gridData, data, updatedAt, resolve });
});
codeSandbox Example.
Note: onRowUpdate here as an example, same goes for onRowAdd, onRowDelete

Error while inline editing in material-table + reactjs

I am using Material-Table along-with ReactJS to create data-table. The table properly loads and displays the data properly but once I edit the data, I get the below error in developer console
Below is my code for Material Table
<MaterialTable
title={tbl_title}
columns={datatable.columns}
data={datatable.data}
options={tableOptions}
editable={{
onRowAdd: newData =>
new Promise((resolve, reject) => {
setTimeout(() => {
{
const data = datatable.data;
data.push(newData);
setTable({ data }, () => resolve());
}
resolve()
}, 1000);
}),
onRowUpdate: (newData, oldData) =>
new Promise((resolve, reject) => {
setTimeout(() => {
{
const data = datatable.data;
const index = data.indexOf(oldData);
data[index] = newData;
setTable({ data }, () => resolve());
}
resolve()
}, 1000)
}),
}}
/>
I am just using the same code sample from the provider with small modification to meet React Functional Component pattern. It would be great if anyone can point me in the right direction.
An older issue for similar error Github Issue: 400 is listed but has no guidance for the issue.
[EDIT]
I am not sure if this helps, but I am seeing a very unusual thing with last modified date for all files after a fresh install of the dependency.
Would this affect somehow the functioning of the dependency package?
Further, error details while editing/adding row
Code changes I used to fix the error in above image
File: node_modules/material-table/dist/components/m-table-body.js:113
return renderData.map(function (data, index) {
if (typeof(data.tableData) != "undefined") {
if (data.tableData.hasOwnProperty("editing")) {
return React.createElement(_this.props.components.EditRow, {
columns: _this.props.columns.filter(function (columnDef) {
return !columnDef.hidden;
}),
components: _this.props.components,
data: data,
icons: _this.props.icons,
localization: (0, _objectSpread2["default"])({}, MTableBody.defaultProps.localization.editRow, _this.props.localization.editRow, {
dateTimePickerLocalization: _this.props.localization.dateTimePickerLocalization
}),
key: index,
mode: data.tableData.editing,
options: _this.props.options,
isTreeData: _this.props.isTreeData,
detailPanel: _this.props.detailPanel,
onEditingCanceled: _this.props.onEditingCanceled,
onEditingApproved: _this.props.onEditingApproved,
getFieldValue: _this.props.getFieldValue
});
} else {
return React.createElement(_this.props.components.Row, {
components: _this.props.components,
icons: _this.props.icons,
data: data,
index: index,
key: "row-" + data.tableData.id,
level: 0,
options: _this.props.options,
localization: (0, _objectSpread2["default"])({}, MTableBody.defaultProps.localization.editRow, _this.props.localization.editRow),
onRowSelected: _this.props.onRowSelected,
actions: _this.props.actions,
columns: _this.props.columns,
getFieldValue: _this.props.getFieldValue,
detailPanel: _this.props.detailPanel,
path: [index + _this.props.pageSize * _this.props.currentPage],
onToggleDetailPanel: _this.props.onToggleDetailPanel,
onRowClick: _this.props.onRowClick,
isTreeData: _this.props.isTreeData,
onTreeExpandChanged: _this.props.onTreeExpandChanged,
onEditingCanceled: _this.props.onEditingCanceled,
onEditingApproved: _this.props.onEditingApproved,
hasAnyEditingRow: _this.props.hasAnyEditingRow,
treeDataMaxLevel: _this.props.treeDataMaxLevel
});
}
}
});

Resources