How to retrieve selectedRows from a material-table in React - reactjs

I have a material table and I use selection and filtering.
Above my material table I have a button that should "send" the selected rows to its parent if you click on it.
How can I retrieve the selected Rows though? I know I can do
const [selectedRows, setSelectedRows] = useState([]);
<MaterialTable
...
onSelectionChange={(rows) => {
setSelectedRows(rows);
}}
... />
But the setSelectedRows results in the Table to be rerendered and then all my filters are gone. I know I could store filters in a state too , but this sounds like way too much overhead for just the simpe question to retrieve the selectedRows at a certain point in time.
Any easy suggestions?
Thanks a lot for your help

Use the components prop and lift the FilterRow component into parent state like below. The filter values will persist.
import MaterialTable, { MTableFilterRow } from 'material-table';
const Parent = () => {
const [components, whatever] = useState({
FilterRow: props => <MTableFilterRow {...props} />,
});
const [columns] = useState([
{ title: "Name", field: "name" },
{ title: "Pet", field: "pet" }
]);
const [data] = useState([
{ name: "Jim", pet: "Dog" },
{ name: "Tom", pet: "Horse" },
{ name: "Susan", pet: "Rat" },
{ name: "Penny", pet: "Cat" }
]);
return (
<MaterialTable
columns={columns}
data={data}
components={components}
/>
);
}
``

Add a ref to the component
<MaterialTable
tableRef={tableRef}
you can get selected rows with
tableRef.current.dataManager.data.filter(o => o.tableData.checked)

Related

Using MUI Autocorrect, with options populated by API

I almost have a working solution, but the label aspect is giving an undefined error, and I also want to make sure my solution is elegant as its a component I will reuse a lot.
I have an API which returns a json response, and for the purposes of this, the important values are; (I will stub out the API and just provide its response).
const countries =
[
{ country_id: 1, name: 'France', iso: 'fr'},
{ country_id: 2, name: 'Germany', iso: 'de'},
{ country_id: 3, name: 'United Kingdom', iso: 'gb'},
{ country_id: 4, name: 'Spain', iso: 'es'}
];
It's the MUI example with some tweaks to almost make it work as desired.
I want the label in the AutoComplete to be the country name, I want the value returned to be the country_id and the text in the AutoComplete to be the name of the Country they selected. It's the label that's not being set.
const Select = ({ country, onUpdate }) => {
//country is the valuable passed in to preselect an option or the option chosen, and the onUpdate is a function passed in (its a setState in the parent component).
const [countries, setCountries] = useState([]);
const [value, setValue] = React.useState('');
useEffect(() => {
api.get(`/countries`).then((response) => {
if (response.data) {
setCountries(response.data);
}
});
}, []);
return (
<>
<Autocomplete
autoHighlight
fullWidth
value={value}
options={countries}
onChange={(event, newValue) => {
setValue(newValue.name);
}}
inputValue={country}
onInputChange={(event, newInputValue) => {
onUpdate(newInputValue);
}}
renderOption={(props, country) => (
<Box component="li" {...props}>
{`{country.name} (${country.iso})`}
</Box>
)}
renderInput={(params) => (
<TextField
{...params}
label="Choose a country"
/>
)}
/>
</>
);
};
Select.propTypes = {
country: PropTypes.string,
onUpdate: PropTypes.func.isRequired,
};
export default Select;
You need to add this line to your AutoComplete attributes:
getOptionLabel={(option) => option.name}
According to the documentation:
Options structure
By default, the component accepts the following options structures:
interface AutocompleteOption {
label: string;
}
// or
type AutocompleteOption = string;
for instance:
const options = [
{ label: 'The Godfather', id: 1 },
{ label: 'Pulp Fiction', id: 2 },
];
// or
const options = ['The Godfather', 'Pulp Fiction'];
However, you can use different structures by providing a getOptionLabel prop.
Another option is to change the name property in your response object into label and you will be set.

Edit custom column component while adding new row of Material Table

With the React Material Table library, is it possible to render a custom component while adding a new row? I'm using a custom component (a Material UI select box, actually), for the Expected Result column. When I add a new row, I only see a field for the Requirement column, not the Expected Result column. Is it possible to add an input for the Expected Result column of the new row as well?
Another option is to not use custom components at all and instead use something like the Cell Editable Example of https://material-table.com/#/docs/features/editable. However, I'm not a fan of the extra clicks that it takes to edit the Expected Result, compared to directly using a Select field.
import MaterialTable from 'material-table'
import { MenuItem, Select } from '#material-ui/core'
import React, { useState } from 'react'
import update from 'immutability-helper'
type PassFailNA = 'Pass' | 'Fail' | 'N/A'
type RowData = {
requirementId: number,
requirementName: string,
expectedResult: PassFailNA,
expectedResultId?: number
}
export function ExpectedResultsTable(props: {
scenarioId: number
}) {
const [tableData, setTableData] = useState<RowData[]>([{ requirementId: 1, requirementName: 'hello', expectedResult: 'Pass' }])
const { enqueueSnackbar } = useSnackbar()
const handleSelect = (id: number) => (event: React.ChangeEvent<{ name?: string; value: any }>) => {
setTableData((tableData: RowData[]) => {
const rowNum = tableData.findIndex(x => x.requirementId === id)
return update<RowData[]>(tableData, {
[rowNum]: { expectedResult: { $set: event.target.value } }
})
})
}
return (
<MaterialTable<RowData>
title=""
columns={[
{
title: 'Requirement',
field: 'requirementName'
},
{
title: 'Expected Result',
field: 'expectedResult',
render: (rowData) => (
<Select value={rowData.expectedResult} onChange={handleSelect(rowData.requirementId)}>
<MenuItem value="Pass">Pass</MenuItem>
<MenuItem value="Fail">Fail</MenuItem>
<MenuItem value="N/A">N/A</MenuItem>
</Select>
)
}
]}
data={tableData}
editable={{
onRowAdd: newRow =>
new Promise((resolve, reject) => {
setTimeout(() => {
setTableData(tableData => update(tableData, { $push: [{ ...newRow, expectedResult: 'N/A'}] }))
resolve()
}, 1000)
})
}}
/>
)
}
To achieve what you are looking for, I think you should specify the editComponent property ( besides render ) when defining the column. That prop takes a function where you can define the component used during the edit or creation phase.
Here is an example I made with a boolean input:
const tableColumns = [
{ title: "Client", field: "id" },
{ title: "Name", field: "name" },
{
title: "booleanValue",
field: "booleanValue",
editComponent: (props) => {
console.log(props);
return (
<input
type="checkbox"
checked={props.value}
onChange={(e) => props.onChange(e.target.checked)}
/>
);
},
render: (rowdata) => (
<input type="checkbox" checked={rowdata.booleanValue} />
)
}
];
Link to working sandbox. I hope that works for you!

Cant turn on switch inside Material-Table

I am trying to create material-table with switches, that onclick changes the state of the component.
Here is component with table, which has state of the parent component as a props. I pass rows variable to the material table data prop. Switch is custom rendered field. Whenever I click on it, it triggers changeRow, which finds index of row in rows variable changes it and saves into new variable. ChangeRows is then called to change the state.
The problem is, the switch is not changing. It looks like nothing is happenning, even though I can clearly see new state in console.
const StuffTable = ({rows, changeRows}) => {
const changeRow = (oldRow, e) => {
const changeData = {[e.target.name]: e.target.checked};
const newRow = {...oldRow, ...changeData};
console.log(oldRow, e);
const index = rows.findIndex(dtaRow => dtaRow.id === oldRow.id);
const newData = rows;
newData[index] = newRow;
console.log(newData);
changeRows(newData);
};
return (
<Container maxWidth="lg">
<Button onClick={() => { changeRow({id: 6}, { target: {name: 'borrowable', checked: true} }) }}>klikni</Button>
<MaterialTable
options={{
actionsColumnIndex: -1,
search: true,
exportButton: true,
exportDelimiter: ";"
}}
actions={[
{
icon: 'edit',
tooltip: 'Edit Study',
onClick: (event, rowData) => alert("Do you want to edit?")
}]}
columns={[
{ title: "Název", field: "title" },
{ title: "Stav", field: "status", render: (data) => <Chip label={data.status} color="primary" avatar={<Avatar src="/static/images/avatar/1.jpg" />} /> },
{ title: "Půjčovat", field: "borrowable", render: (data, id) => (<FormControlLabel control={<Switch checked={data.borrowable} onChange={(e) => changeRow(data, e)} name="borrowable" color="primary"/>} label={data.borrowable ? 'půjčovat' : 'nepůjčovat'} />) },
{ title: "Vidí všichni", field: "active", render: (data, id) => (<FormControlLabel control={<Switch checked={data.borrowable} onChange={(e) => changeRow(data, e)} name="borrowable" color="primary"/>} label={data.borrowable ? 'půjčovat' : 'nepůjčovat'} />) },
{ title: "Uskladněno", field: "location" },
]}
data={rows}
title="Moje věci"
/>
</Container>
);
};
export default StuffTable;
I tried to add button, which on click changes state to empty array, and table did show nothing. But when I triggered changeRow (mockup data) with this button, result was the same - no change on the switch.
import React, {useEffect, useState} from 'react';
import StuffTable from "../components/stuffTable";
let rows = [
{id:5, title: "prošívanice", borrowable: false, surname: "Baran", status: "zapůjčeno", location: "Praha" },
{id:6, title: "prošívanice 2", borrowable: false, surname: "Baran", status: "zapůjčeno", location: "Praha" },
{id:7, title: "prošívanice 3", borrowable: false, surname: "Baran", status: "zapůjčeno" , location: "Brno"}
];
Here is Parent component
const MyStuffPage = () => {
const [data, setData] = useState(rows);
return (
<div>
<StuffTable rows={data} changeRows={(data) => {setData(data); console.log("hou",data);}} />
</div>
);
};
export default MyStuffPage;
Here is Codesandbox with this problem:
https://codesandbox.io/s/festive-gould-i1jf7
You need to call onQueryChange whenever you want to render new data or state to the datatable, make these changes:
at the begining create a ref like so:
const tableRef = useRef(null);
then use it in the material table:
<MaterialTable
//add this
tableRef={tableRef}
options={{
actionsColumnIndex: -1,
search: true,
exportButton: true,
exportDelimiter: ";"
}}
then inside your changeRow function after updating the start and the necessary work add this:
tableRef.current.onQueryChange()
this will tell the table to render the new data with the correct state of the switch

How to loop data and print in a table using material ui

I am working with Material-UI and getting data from the backend. There is no issue with the backend, but I don't know how to loop data and print it in a table format using Material-UI.
Can anyone guide me on how to print data in a table format?
Here is my code so far:
import React, { useState, useEffect } from "react";
import { Link } from "react-router-dom";
import { getProducts } from "../../services/products";
import MaterialTable, { MTableToolbar } from "material-table";
const productsList = props => {
const [data, setData] = useState([]);
const [state] = React.useState({
columns: [
{ title: "Brand", field: "brand" }, //assume here my backend schema is brand
{ title: "Price", field: "price" }, //here price
{ title: "Model no", field: "model" } //here model
]
});
const getProducts = async () => {
try {
const res = await getProducts();
setData(res.data);
console.log(res.data);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
getProducts();
}, []);
return (
<MaterialTable
components={{
Toolbar: props => {
return (
<div>
<MTableToolbar {...props} />
</div>
);
}
}}
options={{
actionsColumnIndex: 5,
selection: true
}}
/>
);
};
export default function Company() {
return <productsList />;
}
You have to set the data and columns value. So try it like this:
import React, { useState, useEffect } from "react";
import MaterialTable, { MTableToolbar } from "material-table";
const fakeFetch = () => {
return new Promise(resolve => {
resolve({
data: [
{ brand: "brand 1", price: 1, model: "123" },
{ brand: "brand 2", price: 1, model: "456" },
{ brand: "brand 3", price: 1, model: "789" }
]
});
});
};
export default function App() {
const [data, setData] = useState([]);
// When the columns don't change you don't need to hold it in state
const columns = [
{ title: "Brand", field: "brand" }, //assume here my backend schema is brand
{ title: "Price", field: "price" }, //here price
{ title: "Model no", field: "model" } //here model
];
const getProducts = async () => {
try {
const res = await fakeFetch();
setData(res.data);
} catch (error) {
console.log(error);
}
};
useEffect(() => {
getProducts();
}, []);
return (
<MaterialTable
columns={columns} // <-- Set the columns on the table
data={data} // <-- Set the data on the table
components={{
Toolbar: props => {
return (
<div>
<MTableToolbar {...props} />
</div>
);
}
}}
options={{
actionsColumnIndex: 5,
selection: true
}}
/>
);
}
To make it even easier you could also provide your fetch function (fakeFetch in this case) as the data value;
data={fakeFetch} // <-- Using this you wouldn't need the [data, setData], getProducts and useEffect code.
Working sandbox link
As per the material-table approach, you have to put your whole fetched data on the data prop inside the MaterialTable component. So as far as I can understand, there is no looping made in this case by using the material-table library.
Assuming the attributes in your data object match the field names specified in your columns prop (if not, create an array of objects from your fetched data that matches the column fields or vice-versa).
And the code would be just the addition of the data prop in your table:
<MaterialTable
// ... existing props
data={data}
/>
Keep in mind that you could also use the remote data approach as described in the documentation which gives you the means to immediately query your data and fetch it inside the data prop of the table.

React Hooks useCallback & memo list re-rendering

I'm new to Hooks and trying to create a PureComponent equivalent version with Hooks.
My goal is to create a multiselectable list with a child component that is reponsible for rendering the list items:
const Movie: FunctionComponent<{
title: string,
score: number,
id: number,
isSelected: boolean,
setSelected: React.Dispatch<React.SetStateAction<{}>>
}> = React.memo(({ title, score, isSelected, setSelected, id }) => {
const selectMovie = (): void => {
if (isSelected === null)
return;
setSelected(id);
};
const selected = {
backgroundColor: "blue"
}
console.log("render movie")
return (
<div onClick={selectMovie} style={isSelected ? selected : {}}>
{title}, {score}
</div>
)
})
The parent component have the data as well as the logic for the selection:
const App: FunctionComponent = () => {
const data = [
{
id: 1,
title: "Pulp fiction",
score: 9
},
{
id: 2,
title: "Heat",
score: 8
},
{
id: 3,
title: "American pie",
score: 7
}
]
const [selectedItems, setSelected] = React.useState<{}>({});
const selectMovie = React.useCallback((id: any) => {
const sel: any = {...selectedItems};
if (sel.hasOwnProperty(id)) {
delete sel[id]
} else {
sel[id] = true;
}
setSelected(sel);
}, [selectedItems])
return (
<div>
{
data.map(e => <Movie key={e.id} {...e} setSelected={selectMovie} isSelected={selectedItems.hasOwnProperty(e.id)}/>)
}
</div>
)
}
I made a sandbox where you can try it out: https://codesandbox.io/s/old-sun-38n4u
The selection state is maintained in the parent component in an object and supplied to the child as a boolean. The problem is when I click on a movie all 3 list items re-renders (you can see the log in the console). I have used React.memo and useCallback as well to avoid arrow function re-creation for the props comparison. I'm pretty new to hooks, so it must be something silly I'm overlooking...
That is because your selectMovie is changing every time due to selectedItems dependency changing.
setSelected function can also take a function and you can get the selectedItems value so you don't need to set it as a dependency
Here is the working sandbox

Resources