I want to pass a callback function in a prop of type object. I have created a custom component for data table in that component I am passing columns, data and actions as props.
actions props is an array of objects with handler callback function linked to each element.
Here is my Data table component code
const DataTable = ({ columns, data, actions }) => {
return <>
<TableContainer>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell>#</TableCell>
{
columns.map((item) => {
return (<TableCell key={item.key}>{item.label}</TableCell>);
})
}
{
actions && <TableCell>Actions</TableCell>
}
</TableRow>
</TableHead>
<TableBody>
{data.map((row, index) => (
<TableRow
key={row.id}
sx={{ "&:last-child td, &:last-child th": { border: 0 } }}
>
<TableCell>
{index + 1}
</TableCell>
{
columns.map(item => {
return (<TableCell key={item.key}>{row[item.key] ? row[item.key] : ''}</TableCell>);
})
}
<TableCell>
{
actions.map((element) => {
return (<React.Fragment key={element.label}><span onClick={(row) => {element.handler(row.id)}}>{element.icon}</span></React.Fragment>)
})
}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
<Pagination count={1} />
</TableContainer>
</>
}
export default DataTable;
and here is code of using that data table
const [data, setData] = useState([]);
const columns = [
{
key: "provider",
label: "Provider",
},
{
key: "phone_number",
label: "Phone Number",
},
{
key: "status",
label: "Status",
},
];
const actions = [
{
icon: <VisibilityIcon sx={{ color: '#1976d2', cursor: 'pointer' }} />,
label: 'View',
handler: ''
},
{
icon: <DeleteIcon sx={{ color: '#1976d2', marginLeft: '4px', cursor: 'pointer' }} />,
label: 'Delete',
handler: deleteHandler
}
];
// Fetching data from api
const deleteHandler = async (id) => {
// Calling delete api
}
return <>
<Card>
<CardContent>
<DataTable columns={columns} data={data} actions={actions} />
</CardContent>
</Card></>;
Now on clicking on any action button I am getting this error
TypeError: element.handler is not a function
Is there any specific way using which i can pass these callback function inside prop object.
You have to check if the element.handelr is a function or not
// in your child component
const actionFn = (id, handler) => {
if (typeof handler !== 'function' ) {
return;
}
handler(row.id);
}
// JSX
<TableCell>
{
actions.map((element) => {
return <React.Fragment key={element.label}>
<span onClick={(row) => {actionFn(row.id, element.handler)}}>{element.icon}</span>
</React.Fragment>
})
}
</TableCell>
Also move const deleteHandler above const actions
Related
I'm trying to use arrays in Grommet DataTable. My data looks like this :
{
customer: [
'BANANA',
'Banana',
'banana',
'republic of banana'
],
somethingelse: ['ABC','123','DEF']
}
In a regular Grommet Table , I'm able to use every cell by defining the first value from the array as title - for example customer[0] - and create an expandable arrow to show the rest of the data in 'customer' :
But I don't get how to do this on a cell basis for a Grommet DataTable ?
Here is the way I'm using it in the regular Grommet Table :
<TableCell scope="row" pad={{ left: '2px', righ: '3px' }}>
<TextInput name="tags" size="xsmall" />
</TableCell>
</TableRow>
{searchResults.length > 0 &&
searchResults.map((searchResult, index) => (
<TableRow key={index}>
<TableCell>
<Box direction="row">
<Text size="xsmall">{searchResult.customer[0]}</Text>
{searchResult.customer.length > 1 && (
<Button
plain
hoverIndicator={false}
icon={
isExpanded[index] ? (
<FormDown size="18px" />
) : (
<FormNext size="18px" />
)
}
onClick={() => toggleOpen(index)}
/>
)}
</Box>
<Box>
{isExpanded[index] && listElements(searchResult.customer)}
</Box>
</TableCell>
Here is my Form , using DataTable :
return (
<Form value={formData} onSubmit={onSubmit} onChange={onChange}>
...
<DataTable
fill
border={{ body: 'bottom' }}
paginate
columns={columns}
data={searchResults}
select={select}
onClickRow={(e) => console.log(e.datum)}
onSelect={() => {}}
step={8}
rowDetails={(row) => { // I'm able to use rowDetails to expand and display some data , but how can I use this to 1. Use the [0] element of the array as title and 2. apply to all cells in the row/table.
for (const cell in row) {
// if (cell.length > 1) {
// return listElements(cell);
// }
console.log(cell);
}
}}
...
/>
...
</Form>
);
I was able to achieve that by using the render function and passing a CellElement to it, in which I have created my rules :
const columns = [
{
property: 'customer',
header: <FormField label="Customer" name="customer" size="xsmall" />,
render: (datum) => <CellElement val={datum.customer} />,
},
CellElement.js
import { Box, Text, Button } from 'grommet';
import { FormNext, FormDown } from 'grommet-icons';
import React, { useState } from 'react';
const CellElement = ({ val }) => {
const title = Array.isArray(val) ? val[0] : val;
const [isExpanded, setIsExpanded] = useState({});
const toggleOpen = (category) => {
setIsExpanded({
...isExpanded,
[category]: !isExpanded[category],
});
};
const listElements = (arr) => {
return arr.slice(1).map((el, index) => (
<Text key={index} size="xsmall">
{el}
</Text>
));
};
return (
<Box>
<Box direction="row">
<Text size="xsmall">{title}</Text>
{Array.isArray(val) && val.length > 1 && (
<Button
plain
hoverIndicator={false}
icon={
isExpanded[title] ? (
<FormDown size="18px" />
) : (
<FormNext size="18px" />
)
}
onClick={() => toggleOpen(title)}
/>
)}
</Box>
<Box>{isExpanded[title] && listElements(val)}</Box>
</Box>
);
};
export default CellElement;
I'm trying to fetch data from api and show it with React.
However I could see that errors and I'm difficult with parsing json response from api.
I think that it will be solved if I make response as array.
I don't know how to make it.
page.js
I try to fetch data from API by Native hook and useEffect
I checked API by Postman. So it is working well.
function Customers(props) {
const [data, setData] = useState({});
const [error, setError] = useState(null);
useEffect(() => {
fetch("http://localhost:8080/contacts")
.then((response) => {
if (response.status !== 200) {
setError("Invalid response: ", response.status);
} else {
setError(null);
}
return response.json();
})
.then((json) => {
setData(json);
});
});
if (error !== null) {
return <div>Error: {error.message}</div>
} else {
return (
<DashboardLayout>
<>
<Head>
<title>
Data
</title>
</Head>
<Box
component="main"
sx={{
flexGrow: 1,
py: 8
}}
>
<Container maxWidth={false}>
<Box sx={{ mt: 3 }}>
<CustomerListResults customers={data} />
</Box>
</Container>
</Box>
</>
</DashboardLayout>
);
}
}
export default Customers;
list_component.js
I made a table. I would like to show API data this table.
I try to use slice and map to parsing data. but it is not working.
export const CustomerListResults = ({ customers, ...rest }) => {
const [limit, setLimit] = useState(25);
const [page, setPage] = useState(0);
const handlePageChange = (event, newPage) => {
setPage(newPage);
};
return (
<Card {...rest}>
<PerfectScrollbar>
<Box sx={{ minWidth: 1050 }}>
<Table size="small">
<TableHead>
<TableRow>
<TableCell>
Date
</TableCell>
<TableCell>
Name
</TableCell>
</TableRow>
</TableHead>
<TableBody>
{customers.slice(0,limit).map((customer) => (
<TableRow
hover
key={customers.id}
>
<TableCell>
<Box
sx={{
alignItems: 'center',
display: 'flex'
}}
>
<Typography
color="textPrimary"
variant="body2"
>
{customers.created_date}
</Typography>
</Box>
</TableCell>
<TableCell>
{customers.user_idx}
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</Box>
</PerfectScrollbar>
<TablePagination
component="div"
count={customers.length}
onPageChange={handlePageChange}
page={page}
rowsPerPage={limit}
/>
</Card>
);
};
CustomerListResults.propTypes = {
customers: PropTypes.array.isRequired
};
That's because data on initial load is equal to an empty object, and object doesn't have a slice method as it's a method for an array.
One solution is set an inital value for data to an empty array.
const [data, setData] = useState([]);
I'm working on a simple React app alongside TypeScript, and I'm using JSONPlaceholder for API calls simulation. I have implemented everything I need, but facing a problem when it comes to re-rendering components that shows response data from API. The thing is that if I do HTTP GET to all data after I do DELETE or PUT, I will again get all same data because data changes don't actually apply on the server.
I will appreciate it if you can help me how to edit my functions for DELETE and PUT.
When I console.log my responses inside components, I get staus ok 200, so the API call part is fine, the only thing that I struggle with is how to re-render components properly
There is a component that shows all data, and also make calls to DELETE endpoint, and shows modal where I call PUT endpoint:
const PostsTable: React.FunctionComponent = () => {
const [posts, setPosts] = useState<Array<IPost>>([]);
const [selectedPost, setSelectedPost] = useState<IPost>({});
const [dialogOpen, setDialogOpen] = useState<boolean>(false);
const fetchPosts = () => {
PostService.getAllPosts()
.then((response: any) => {
setPosts(response.data);
})
.catch((e: Error) => {
console.log(e);
});
};
useEffect(() => {
fetchPosts();
}, []);
const editPost = (post: IPost) => (event: any) => {
setSelectedPost(post);
setDialogOpen(true);
};
const handleClose = () => {
setDialogOpen(false);
};
const deletePost =
(id: any): any =>
(event: Event) => {
event.stopPropagation();
PostService.deletePost(id)
.then((response: any) => {
setPosts(posts);
console.log(response);
})
.catch((e: Error) => {
console.log(e);
});
};
return (
<Container fixed>
{!posts || posts.length < 1 ? (
<div style={{ display: 'flex', justifyContent: 'center' }}>
<CircularProgress color="primary" size={100} />
</div>
) : (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="simple table">
<TableHead>
<TableRow>
<TableCell align="left">User Id</TableCell>
<TableCell align="left">Post Id</TableCell>
<TableCell align="left">Title</TableCell>
<TableCell align="left">Actions</TableCell>
</TableRow>
</TableHead>
<TableBody>
{posts.map((post: IPost) => (
<TableRow
key={post.id}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableCell>{post.userId}</TableCell>
<TableCell align="left">{post.id}</TableCell>
<TableCell align="left">{post.title}</TableCell>
<TableCell align="left">
<Tooltip title="Delete">
<IconButton onClick={editPost(post)}>
<EditIcon />
</IconButton>
</Tooltip>
<Tooltip title="Edit">
<IconButton onClick={deletePost(post.id)}>
<DeleteIcon />
</IconButton>
</Tooltip>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
{
<EditPostDialog
open={dialogOpen}
handleClose={handleClose}
selectedPost={selectedPost}
/>
}
</Container>
);
};
export default PostsTable;
And also, there is a modal component with PUT function:
const EditPostDialog: React.FunctionComponent<IEditPostDialog> = (
props: IEditPostDialog
) => {
const { open, handleClose, selectedPost } = props;
const [post, setPost] = useState<IPost>({});
useEffect(() => {
const newPost = {
id: selectedPost.id,
userId: selectedPost.userId,
title: selectedPost.title,
body: selectedPost.body,
};
setPost(newPost);
}, [selectedPost]);
const handleChange = (event: any) => {
setPost({ ...post, [event.target.name]: event.target.value });
};
const handleSubmit = () => {
PostService.updatePost(post.id, post)
.then((response: any) => {
console.log(response);
})
.catch((e: Error) => {
console.log(e);
});
handleClose();
};
return (
<Dialog onClose={handleClose} open={open}>
<DialogTitle id="simple-dialog-title">Post info</DialogTitle>
<DialogContent classes={{ root: 'dialog-content' }}>
<TextField
id="userId"
label="User Id"
name="userId"
variant="outlined"
value={post.userId}
onChange={handleChange}
style={{ marginTop: 16 }}
/>
<TextField
id="title"
label="Title"
name="title"
variant="outlined"
value={post.title}
onChange={handleChange}
/>
<TextField
id="body"
label="Body"
name="body"
variant="outlined"
value={post.body}
onChange={handleChange}
multiline
/>
</DialogContent>
<DialogActions>
<Button onClick={handleClose}>Close</Button>
<Button onClick={handleSubmit}>Submit</Button>
</DialogActions>
</Dialog>
);
};
export default EditPostDialog;
And at the end, my PostService.ts and HTTP files where is axios code to call API points:
const getAllPosts = () => {
return http.get<Array<IPost>>('/posts');
};
const getSinglePost = (id: number | undefined) => {
return http.get<IPost>(`/posts/${id}`);
};
const createPost = (data: IPost) => {
return http.post<IPost>('/posts', data);
};
const updatePost = (id: number | undefined, data: IPost) => {
return http.put<any>(`/posts/${id}`, data);
};
const deletePost = (id: number | undefined) => {
return http.delete<any>(`/posts/${id}`);
};
const PostService = {
getAllPosts,
getSinglePost,
createPost,
updatePost,
deletePost,
};
export default axios.create({
baseURL: 'https://jsonplaceholder.typicode.com',
headers: {
'Content-type': 'application/json',
},
});
Thank you all guys :)
Your delete function uses "setPosts(posts)" which I'm pretty sure is not what you meant. Also, your modal does not tell your main component that a post has been edited, I guess that's the problem ? Pass a callback to the modal to update the posts state in the main component.
I came from Vue.js Vuetify.js background.Vuetify.js has v-data-table component.
Simply we pass headers and items to generate a nice table.
<v-data-table
:headers="headers"
:items="desserts"
></v-data-table>
If we want to add a button, image, or something like that to a table cell
What we do is
<v-data-table :headers="headers" :items="items" >
<template v-slot:item.images="{ item }">
<v-img
v-if="item.images"
max-width="150px"
:src="item.images"
contain
></v-img>
</template>
<template v-slot:item.update="{ item }">
<v-btn
#click="
$router.replace({
path: '/create-product',
query: { id: item.id },
})
"
>
<v-icon>edit</v-icon>
</v-btn></template
>
</v-data-table>
This is very clean and easy.
In React.js also I could achieve the first thing using this
export default function ReusableTable({ headers, items }) {
return (
<Grid container>
<Grid item>
<Card>
<CardContent>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
{headers.map((header, i) => (
<TableCell key={i}>{header.text.toUpperCase()}</TableCell>
))}
</TableRow>
</TableHead>{' '}
<TableBody>
{items.map((item, i) => (
<TableRow key={i}>
{headers.map(({ value }) => (
<TableCell key={value}>{item[value]}</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</CardContent>
</Card>
</Grid>
</Grid>
);
}
Here also I pass the headers and items.
I want to display buttons, links, images, chips (UI) for certain columns in the table. How do I achieve that in the React world?
If I further explain, I want to pass items array (array of object). Object's imageSRC property should render with an img tag. Something like that.
Something like this should work. This is conditionally rendering an image tag if there is an item.images as stated in your question. Next it will render a Material Button if item.update exists. Alternatively, it simply renders the item[value].
Here is abbreviated code:
export default function ReusableTable({ headers, items }) {
const dynamicRender = (item, value)=>{
if(item && item.images){
return <img src=`${item.images}`/>
} else if(item && item.update){
return <Button href="/create-product">Link</Button>
} else {
return item[value];
}
}
return (
<TableBody>
{items.map((item, i) => (
<TableRow key={i}>
{headers.map(({ value }) => (
<TableCell key={value}>{dynamicRender(item, value)}</TableCell>
))}
</TableRow>
))}
</TableBody>
);
}
Try something like this
import React from "react";
import TableContainer from "#material-ui/core/TableContainer";
import Table from "#material-ui/core/Table";
import Paper from "#material-ui/core/Paper";
import TableHead from "#material-ui/core/TableHead";
import TableRow from "#material-ui/core/TableRow";
import TableCell from "#material-ui/core/TableCell";
import TableBody from "#material-ui/core/TableBody";
import { getSlots } from 'helpers/Slots'
const BaseTable = ({ headers, items, children, ...otherProps }) => {
const [actions] = getSlots(['actions'], children)
const tableConfig = {
size: "small",
}
const rowConfig = {
...otherProps,
}
const dynamicRenderer = (item, value) => {
if (value === 'actions') {
return children(item)
} else {
return item[value]
}
}
return (
<>
<TableContainer component={Paper}>
<Table {...tableConfig}>
<TableHead>
<TableRow {...rowConfig}>
{headers.map((header, i) => (
<TableCell key={i}>{header.label}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{items.map((item, i) => (
<TableRow key={i} {...rowConfig}>
{headers.map(({ value }) => (
<TableCell key={value}>{dynamicRenderer(item, value)}</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
</>
)
}
export default BaseTable
Then added helpers
import React from "react";
const Slot = ({ children }) => <>{children}</>
const getSlots = ( names, children) => {
return names.map(name => {
let slot = null;
React.Children.forEach(children, child => {
if (!React.isValidElement(child)) {
return;
}
if (child.type === Slot && (child.props).name === name) {
slot = React.cloneElement(child);
}
});
return slot;
});
}
export { Slot, getSlots }
import React, { useState, useEffect } from "react"
import Grid from '#material-ui/core/Grid'
import BaseTable from 'helpers/Table'
import { Slot } from 'helpers/Slots'
import PencilBoxOutline from 'mdi-react/PencilBoxIcon'
import DeleteOutline from 'mdi-react/DeleteIcon'
const headers = [
{value: 'name', label: 'Name'},
{value: 'age', label: 'Age'},
{value: 'gender', label: 'Gender'},
{value: 'registeredDate', label: 'Date of Registration'},
{value: 'requirements', label: 'Semester Requirements'},
{value: 'percentage', label: 'Percentage (%)'},
{value: 'actions', label: 'Actions'},
]
const items = [
{
id: 1,
requirements: 'Pay at least 50% ',
percentage: '10%',
name: "John Doe",
age: 30,
registeredDate: "2021/10/30",
gender: "Male"
},
{
id: 2,
requirements: 'Just go with it',
percentage: '50%',
name: "Jane Doe",
age: 40,
registeredDate: "2021/10/30",
gender: "Female"
},
]
const Test = () => {
return (
<Grid container spacing={4}>
<Grid item xs={12}>
<Grid container justifyContent="space-between" spacing={2}>
<Grid item></Grid>
<Grid item sm={9}></Grid>
</Grid>
<BaseTable headers={headers} items={items}>
{(item) => (
<Slot name="actions">
<PencilBoxOutline onClick={(item) => onOpenDialog(item)}/>
<DeleteOutline onClick={(item) => onDelete(item)} />
</Slot>
)}
</BaseTable>
</Grid>
</Grid>
)
}
export default Test
my problem is to color the column headers according to the function that is to be performed on it. like if it is column 0 and we choose to perform Addition in that column then it has to be red, If we choose column 5 and we choose to do multiply then it has be to be green. again if we choose 2 and we choose to do addition in that then it has to be red. how to style this?
<TableRow className={classes.tableRow}>
{location.state.columnsData.map(column => (
<TableCell className={classes.tableCell_header} key={column.orderPosition}>
<div>
<Button
className={ column.orderPosition in [0,1,2] ? classes.buttonSelected : classes.buttonNormal }
aria-controls="simple-menu"
aria-haspopup="true"
onClick={()=> handleClickOpen(column.orderPosition)}
variant="outlined"
>
</Button>
</div>
</TableCell>
</TableRow>
I had to make a lot of assumptions about your code based on the limited info provided (using Material-UI, <Button> is in <TableHead> as opposed to <TableBody>, etc.), but I think this should give you what you are looking for:
const {
Button,
Menu,
MenuItem,
Table,
TableBody,
TableCell,
TableHead,
TableRow,
withStyles
} = MaterialUI;
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
columnsData: [
{ orderPosition: "0" },
{ orderPosition: "1" },
{ orderPosition: "2" },
{ orderPosition: "3" },
{ orderPosition: "4" },
{ orderPosition: "5" }
],
anchorEl: null
};
}
state = {
columnsData: [
{ orderPosition: "0" },
{ orderPosition: "1" },
{ orderPosition: "2" },
{ orderPosition: "3" },
{ orderPosition: "4" },
{ orderPosition: "5" }
],
anchorEl: null
};
handleClickOpen = (event, orderPosition) => {
const columnsData = this.state.columnsData.map(column => {
column.clicked = false;
return column;
});
columnsData[orderPosition].clicked = true;
this.setState({ columnsData });
const anchorEl = event.currentTarget;
this.setState({ anchorEl });
};
handleClose = styleClass => {
const { classes } = this.props;
if (this.state.anchorEl !== null) {
var headerEl = this.state.anchorEl.parentElement.parentElement;
headerEl.classList.remove(
classes.addition,
classes.subtraction,
classes.multiplication
);
headerEl.classList.add(styleClass);
const anchorEl = null;
this.setState({ anchorEl });
}
};
render() {
const { classes } = this.props;
return (
<div>
<Table>
<TableHead>
<TableRow className={classes.tableRow}>
{this.state.columnsData.map(column => (
<TableCell
className={classes.tableCell_header}
key={column.orderPosition}
>
<div>
<Button
className=""
aria-controls="simple-menu"
aria-haspopup="true"
onClick={event =>
this.handleClickOpen(event, column.orderPosition)
}
variant="outlined"
children=""
>
Button
</Button>
</div>
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
<TableRow>
{this.state.columnsData.map(column => (
<TableCell key={column.orderPosition}>
Column {column.orderPosition}
</TableCell>
))}
</TableRow>
</TableBody>
</Table>
<Menu
id="simple-menu"
anchorEl={this.state.anchorEl}
keepMounted
open={Boolean(this.state.anchorEl)}
onClose={() => this.handleClose}
>
<MenuItem onClick={() => this.handleClose(classes.addition)}>
Addition
</MenuItem>
<MenuItem onClick={() => this.handleClose(classes.subtraction)}>
Subtraction
</MenuItem>
<MenuItem onClick={() => this.handleClose(classes.multiplication)}>
Multiplication
</MenuItem>
</Menu>
</div>
);
}
}
const styles = theme => ({
tableRow: {
width: "100%"
},
tableCell_header: {
padding: "10px",
textAlign: "center"
},
addition: {
background: "red"
},
subtraction: {
background: "blue"
},
multiplication: {
background: "green"
}
});
App = withStyles(styles)(App);
ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.12.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.11.0/umd/react-dom.production.min.js"></script>
<script src="https://unpkg.com/#material-ui/core#4.9.0/umd/material-ui.production.min.js"></script>