I have a Data Grid table of users, the last column of the table is meant to be a sub-menu of options that each open up a modal. However, when I try to pass the data of a given row to a modal, it will only pass the final row of the data set (i.e. if I have 10 users, it passes rowId:10).
export default function Users() {
const [userData, setUserData] = React.useState([]);
React.useEffect(() => {
let apiClient = new APIClient();
apiClient.getUser().then((response) => {
setUserData(response);
console.log(response);
});
}, []);
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const [showEditModal, setShowEditModal] = React.useState<boolean>(false);
const [showDeleteModal, setShowDeleteModal] = React.useState<boolean>(false);
//for each modal
const open = Boolean(anchorEl);
const moreOptions = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const columns: GridColDef[] = [
{ headerName: 'Name', field: 'name', flex: 1 },
{ headerName: 'Email', field: 'email', flex: 1 },
{ headerName: 'Date Added', field: 'added', flex: 0.7 },
{ headerName: 'Reports To', field: 'reports_to', flex: 1 },
{
field: ' ',
flex: 0.2,
renderCell: (params) => {
return (
<>
<IconButton
id="basic-button"
aria-controls={open ? 'basic-menu' : undefined}
aria-haspopup="true"
aria-expanded={open ? 'true' : undefined}
onClick={moreOptions}>
<MoreIcon />
</IconButton><Menu
id="basic-menu"
anchorEl={anchorEl}
open={open}
onClose={handleClose}
MenuListProps={{
'aria-labelledby': 'basic-button',
}}
>
<MenuItem>
<UserNotification />
</MenuItem>
<MenuItem onClick={() => { setShowEditModal(true); handleClose(); } }>
<UserEdit />
</MenuItem>
<MenuItem onClick={() => { setShowDeleteModal(true); handleClose(); } }>
<UserDelete />
</MenuItem>
</Menu>
<EditUserModal userData={params.row} show={showEditModal} toggleModal={(value) => { setShowEditModal(value); } } />
<DeleteUserModal userData={params.row} show={showDeleteModal} toggleModal={(value) => { setShowDeleteModal(value); } } />
</>
);
}
},
];
return (
<div style={{ height: 'auto', width: "100%" }}>
<StripedDataGrid rows={userData} columns={columns}
getRowClassName={(params) =>
params.indexRelativeToCurrentPage % 2 === 0 ? 'even' : 'odd'
}
hideFooter={true}
autoHeight={true}
components={{ Toolbar: GridToolbar }}
/>
</div>
);
}
The Data Grid documentation does not have a clear guide on how to pass props of the grid forward. Attempting to populate userData instead of params gives me an overload error. What is the proper way to handle this?
This is my first time using react-table, after I decided to redesign a component in my app. I'm almost there, but I found myself stuck while trying to pass some data to a dialog pop-up on button click.
In my previous table design, using MUI Table, I used array.map() to render each row entry. Then, all I needed to do was using props to pass data to the Dialog components, so every action button would load the data from their respective entries (including data not displayed, like _id). Now with react-table I don't know how to achieve the same result, due to the logic change. The Edit and Delete buttons are there, they trigger the dialog opening, but my progress ends here.
Entries.jsx
function Entries(props) {
const navigate = useNavigate();
const [openEdit, setOpenEdit] = useState(false);
const [openDelete, setOpenDelete] = useState(false);
const handleEdit = () => {
setOpenEdit(true);
};
const handleDelete = () => {
setOpenDelete(true);
};
const handleClose = () => {
setOpenEdit(false);
setOpenDelete(false);
};
const data = props.data;
const columns = useMemo(
() => [
{
Header: "#",
id: "row",
Cell: ({ row }) => {
return <div>{row.index + 1}</div>;
},
},
{
Header: "Title",
accessor: "title",
className: "column-left",
Cell: ({ cell: { value }, row: { original } }) => (
<Link
sx={{
fontSize: "14px",
fontWeight: 500,
"&:hover": { color: "#00D67E" },
}}
color="#fff"
underline="none"
style={{ cursor: "pointer" }}
onClick={() => navigate(`/details/${original.movieId}`)}
>
{value}
</Link>
),
},
{
Header: "Year",
accessor: "year",
className: "column-right",
},
{
Header: "Rating",
accessor: "details[0].rating",
className: "column-right",
Cell: ({ cell: { value } }) => (
<Rating
size="small"
value={value}
readOnly
emptyIcon={<StarIcon fontSize="inherit" />}
/>
),
},
{
Header: "Watched",
accessor: "date",
className: "column-right",
Cell: ({ cell: { value } }) => format(new Date(value), "MMM dd, yyyy"),
},
{
Header: "View Count",
accessor: "details[0].view_count",
className: "column-right",
},
{
Header: "Review",
accessor: "details[0].review",
className: "column-right",
Cell: ({ cell: { value } }) =>
!value ? null : <CheckIcon color="primary" />,
},
{
Header: "Actions",
className: "column-right",
Cell: () => (
<div>
<IconButton
aria-label="edit"
style={{ color: "#e0e0e0" }}
onClick={handleEdit}
>
<EditIcon fontSize="small" />
</IconButton>
<IconButton
aria-label="delete"
style={{ color: "#e0e0e0", paddingRight: 0 }}
onClick={handleDelete}
>
<DeleteIcon fontSize="small" />
</IconButton>
</div>
),
},
],
[]
);
return (
<div>
{/* TABLE */}
<CustomTable columns={columns} data={data} />
{/* DIALOGS */}
<EditDialog
isOpen={openEdit}
onClose={handleClose}
id={data._id}
movieId={data.movieId}
date={data.date}
rating={data.details[0].rating}
review={data.details[0].review}
onUpdate={props.onUpdate}
/>
<DeleteDialog
isOpen={openDelete}
onClose={handleClose}
title="Delete this entry?"
message={
<span>
<strong>
{data.title} ({data.date})
</strong>{" "}
will be removed. This action cannot be undone.
</span>
}
onRemove={() => {
props.onRemove(data._id, data.movieId);
}}
/>
</div>
);
}
export default Entries;
Later on I even tried something to mimic the logic of my previous design, since the mapping is handled by the rows.map((row) => { prepareRow(row) } in my CustomTable.jsx component. Then I came up with this:
{
Header: "Actions",
className: "column-right",
Cell: () => (
<div>
<IconButton
aria-label="edit"
style={{ color: "#e0e0e0" }}
onClick={({row}) => {
handleEdit();
return (
<EditDialog
isOpen={openEdit}
onClose={handleClose}
id={row._id}
movieId={row.movieId}
date={row.date}
rating={row.details[0].rating}
review={row.details[0].review}
onUpdate={props.onUpdate}
/>
);
}}
>
<EditIcon fontSize="small" />
</IconButton>
<IconButton
aria-label="delete"
style={{ color: "#e0e0e0", paddingRight: 0 }}
onClick={handleDelete}
>
<DeleteIcon fontSize="small" />
</IconButton>
</div>
),
}
But it didn't work either. It stops right at the first prop and throws an error saying it can't read properties row._id. That was my last shot.
That is simple, you can store the selected row to the state and then use it in your Edit and Delete components. Try this
function Entries(props) {
const navigate = useNavigate();
const [openEdit, setOpenEdit] = useState(false);
const [openDelete, setOpenDelete] = useState(false);
const [selectedRow, setSelectedRow] = useState();
const handleEdit = (row) => {
setOpenEdit(true);
setSelectedRow(row);
};
const handleDelete = (row) => {
setOpenDelete(true);
setSelectedRow(row);
};
const handleClose = () => {
setOpenEdit(false);
setOpenDelete(false);
setSelectedRow();
};
const data = props.data;
const columns = useMemo(
() => [
{
Header: "#",
id: "row",
Cell: ({ row }) => {
return <div>{row.index + 1}</div>;
}
},
{
Header: "Title",
accessor: "title",
className: "column-left",
Cell: ({ cell: { value }, row: { original } }) => (
<Link
sx={{
fontSize: "14px",
fontWeight: 500,
"&:hover": { color: "#00D67E" }
}}
color="#fff"
underline="none"
style={{ cursor: "pointer" }}
onClick={() => navigate(`/details/${original.movieId}`)}
>
{value}
</Link>
)
},
{
Header: "Year",
accessor: "year",
className: "column-right"
},
{
Header: "Rating",
accessor: "details[0].rating",
className: "column-right",
Cell: ({ cell: { value } }) => (
<Rating
size="small"
value={value}
readOnly
emptyIcon={<StarIcon fontSize="inherit" />}
/>
)
},
{
Header: "Watched",
accessor: "date",
className: "column-right",
Cell: ({ cell: { value } }) => format(new Date(value), "MMM dd, yyyy")
},
{
Header: "View Count",
accessor: "details[0].view_count",
className: "column-right"
},
{
Header: "Review",
accessor: "details[0].review",
className: "column-right",
Cell: ({ cell: { value } }) =>
!value ? null : <CheckIcon color="primary" />
},
{
Header: "Actions",
className: "column-right",
Cell: ({ row }) => (
<div>
<IconButton
aria-label="edit"
style={{ color: "#e0e0e0" }}
onClick={() => handleEdit(row.original)}
>
<EditIcon fontSize="small" />
</IconButton>
<IconButton
aria-label="delete"
style={{ color: "#e0e0e0", paddingRight: 0 }}
onClick={() => handleEdit(row.original)}
>
<DeleteIcon fontSize="small" />
</IconButton>
</div>
)
}
],
[]
);
return (
<div>
{/* TABLE */}
<CustomTable columns={columns} data={data} />
{/* DIALOGS */}
<EditDialog
isOpen={openEdit}
onClose={handleClose}
id={selectedRow?._id}
movieId={selectedRow?.movieId}
date={selectedRow?.date}
rating={selectedRow?.details[0]?.rating}
review={selectedRow?.details[0]?.review}
onUpdate={props.onUpdate}
/>
<DeleteDialog
isOpen={openDelete}
onClose={handleClose}
title="Delete this entry?"
message={
<span>
<strong>
{selectedRow?.title} ({selectedRow?.date})
</strong>{" "}
will be removed. This action cannot be undone.
</span>
}
onRemove={() => {
props.onRemove(selectedRow?._id, selectedRow?.movieId);
}}
/>
</div>
);
}
export default Entries;
i'm using the following ant component : https://ant.design/components/table/
and i'm really struggling to add the following search option in my table :
here's the table i've created so far :
i'm pretty new to both React and Typescript , since the project is not in js but in typescript i cannot get my code to work properly looking at ant documentation ; i've turned my component to a functional component as i'm gonna be using hooks and added some code to get out most of the compiler errors (type declaration for ts) , still have some syntax errors :
heres my code:
import React, { Component, useState } from 'react';
import {connect} from "react-redux";
// #ts-ignore
import Highlighter from 'react-highlight-words';
//**External Libraries */
//REACT MOSAIC
import {ExpandButton, MosaicWindow, RemoveButton} from "react-mosaic-component";
import {MosaicBranch} from "react-mosaic-component/src/types";
//BLUEPRINTJS
import {InputGroup} from "#blueprintjs/core";
//ANTDESIGN
import { Table , Button ,Tag , Input , Space} from "antd";
import 'antd/dist/antd.css'; // or 'antd/dist/antd.less'
import {CaretRightOutlined , SearchOutlined} from '#ant-design/icons';
//STYLES
import './styles.css'
//API
import {useGetAllUtenti} from '../../../api/index';
import { Utente } from '../../../api/types';
const UserSummaryWindow: React.FC<any> = (props) => {
//HOOKS STATE FOR TABLE SEARCH
const [searchText , setSearchText] = useState('');
const [searchedColumn , setSearchedColumn] = useState('');
const {path} = props;
const dataSource: Object | any = useGetAllUtenti().data;
const ruoli: any = [
{
text: 'User',
value: 'user',
},
{
text: 'Administrator',
value: 'administrator',
},
{
text: 'NumeroVerde',
value: 'numeroVerde',
},
]
const stati: any = [
{
text: 'Attivo',
value: 1,
},
{
text: 'Non Attivo',
value: 0,
}
]
const columns: any = [
{
title: 'Nome',
dataIndex: 'nome',
key: 'nome',
defaultSortOrder: 'descend',
sorter: (a:any, b:any) => { return a.nome.localeCompare(b.nome)},
},
{
title: 'Cognome',
dataIndex: 'cognome',
key: 'cognome',
sorter: (a:any, b:any) => { return a.cognome.localeCompare(b.cognome)},
},
{
title: 'Ruolo',
dataIndex: 'ruolo',
key: 'ruolo',
filters: ruoli,
onFilter: (value:any, record:any) => record.ruolo.indexOf(value) === 0,
sorter: (a:any, b:any) => { return a.ruolo.localeCompare(b.ruolo)},
render: (text: string) => (
<>
{
<Tag color={renderTagColor(text)}>
{text.toUpperCase()}
</Tag>
}
</>)
},
{
title: 'Gestore',
dataIndex: 'gestore',
key: 'gestore',
sorter: (a:any, b:any) => { return a.gestore.localeCompare(b.gestore)},
},
{
title: 'Username',
dataIndex: 'username',
key: 'username',
sorter: (a:any, b:any) => { return a.username.localeCompare(b.username)},
},
// {
// title: 'Password',
// dataIndex: 'password',
// key: 'password',
// },
// {
// title: 'IDEnte',
// dataIndex: 'idEnte',
// key: 'idEnte',
// },
{
title: 'Tipo',
dataIndex: 'tipo',
key: 'tipo',
sorter: (a:any, b:any) => a.tipo - b.tipo,
},
{
title: 'Stato',
dataIndex: 'stato',
key: 'stato',
filters: stati,
onFilter: (value:any, record:any) => record.stato.indexOf(value) === 0,
sorter: (a:any, b:any) => a.stato - b.stato,
render :(stato: number) => (
<>
{
(stato == 1) ?
(
<Tag color="#00cc00">
Attivo
</Tag>
) :
(
<Tag color="#ff0000">
Non Attivo
</Tag>
)
}
</>)
},
{
title: 'Ultimo aggiornamento password',
dataIndex: 'ultimoAggiornamentoPassword',
key: 'ultimoAggiornamentoPassword',
sorter: (a:any, b:any) => a.ultimoAggiornamentoPassword.localeCompare(b.ultimoAggiornamentoPassword)
},
{
title: '',
dataIndex: 'idUtente',
key: 'idUtente',
render: () => (
//da inserire link per andare al dettaglio / modifica utente
<Button type="primary"><CaretRightOutlined /></Button>
),
},
]
const toolbarControls = React.Children.toArray([
<ExpandButton/>,
<RemoveButton/>
]);
function renderTagColor(text:string): string {
switch(text) {
case 'user':
return 'gold';
case 'administrator':
return 'red';
case 'numeroVerde':
return 'green';
default:
return '';
}
}
getColumnSearchProps = dataIndex => ({
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
<div style={{ padding: 8 }}>
<Input
ref={node => {
this.searchInput = node;
}}
placeholder={`Search ${dataIndex}`}
value={selectedKeys[0]}
onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
style={{ width: 188, marginBottom: 8, display: 'block' }}
/>
<Space>
<Button
type="primary"
onClick={() => handleSearch(selectedKeys, confirm, dataIndex)}
icon={<SearchOutlined />}
size="small"
style={{ width: 90 }}
>
Search
</Button>
<Button onClick={() => handleReset(clearFilters)} size="small" style={{ width: 90 }}>
Reset
</Button>
</Space>
</div>
),
filterIcon: filtered => <SearchOutlined style={{ color: filtered ? '#1890ff' : undefined }} />,
onFilter: (value, record) =>
record[dataIndex].toString().toLowerCase().includes(value.toLowerCase()),
onFilterDropdownVisibleChange: visible => {
if (visible) {
setTimeout(() => this.searchInput.select());
}
},
render: (text:string) =>
searchText === dataIndex ? (
<Highlighter
highlightStyle={{ backgroundColor: '#ffc069', padding: 0 }}
searchWords={[searchText]}
autoEscape
textToHighlight={text.toString()}
/>
) : (
text
),
});
function handleSearch (selectedKeys:any, confirm:any, dataIndex:any) {
confirm();
setSearchText(selectedKeys[0]);
setSearchedColumn(dataIndex);
};
function handleReset (clearFilters:any) {
clearFilters();
setSearchText('');
};
return (
<MosaicWindow<string>
title="Riepilogo Utenti"
path={path}
toolbarControls={toolbarControls}
>
<Table
dataSource={dataSource}
columns={columns}
bordered={true}
//pagination={{ pageSize:3,position: ['bottomCenter'] }}
pagination={{position: ['bottomCenter'] }}
rowKey={'idUtente'}
//stile per righe striped
rowClassName={(record, index) => index % 2 === 0 ? 'table-row-light' : 'table-row-dark'}
/>
</MosaicWindow>
);
};
export default UserSummaryWindow;
The part which is giving me headeache is (need to convert it to typescript [strict option in config file is enabled]):
getColumnSearchProps = dataIndex => ({
filterDropdown: ({ setSelectedKeys, selectedKeys, confirm, clearFilters }) => (
<div style={{ padding: 8 }}>
<Input
ref={node => {
this.searchInput = node;
}}
placeholder={`Search ${dataIndex}`}
value={selectedKeys[0]}
onChange={e => setSelectedKeys(e.target.value ? [e.target.value] : [])}
onPressEnter={() => this.handleSearch(selectedKeys, confirm, dataIndex)}
style={{ width: 188, marginBottom: 8, display: 'block' }}
/>
<Space>
<Button
type="primary"
onClick={() => handleSearch(selectedKeys, confirm, dataIndex)}
icon={<SearchOutlined />}
size="small"
style={{ width: 90 }}
>
Search
</Button>
<Button onClick={() => handleReset(clearFilters)} size="small" style={{ width: 90 }}>
Reset
</Button>
</Space>
</div>
),
filterIcon: filtered => <SearchOutlined style={{ color: filtered ? '#1890ff' : undefined }} />,
onFilter: (value, record) =>
record[dataIndex].toString().toLowerCase().includes(value.toLowerCase()),
onFilterDropdownVisibleChange: visible => {
if (visible) {
setTimeout(() => this.searchInput.select());
}
},
render: (text:string) =>
searchText === dataIndex ? (
<Highlighter
highlightStyle={{ backgroundColor: '#ffc069', padding: 0 }}
searchWords={[searchText]}
autoEscape
textToHighlight={text.toString()}
/>
) : (
text
),
});
I'm not here asking you to correct my code , just wanted to know if anyone could share a typescript implementation of that getColumnSearchProps function , or an example of the table component from ant design with typescript code..
Sorry for such a late reply, i was having the same problem.
I found the solution on a github repo someone made. i really wonder why the antd devs don't support typescript in these days.
anyways, here is the repo.
https://github.com/freewind-demos/typescript-react-antd-table-search-column-demo
Basically All the hardwork of moving the old JS code to typescript is done, although there are some linting errors here and there.
I hope anyone who comes looking for this answer really questions their decision to use antd instead of react-table or material-design.
Same answer as Lav Hinsu, but I'll paste the full code here, just in case that link dies.
import "antd/dist/antd.css";
import React from "react";
import ReactDOM from "react-dom";
import { SearchOutlined } from "#ant-design/icons";
import { Table, Button, Input } from "antd";
import { ColumnType } from "antd/lib/table";
function tableColumnTextFilterConfig<T>(): ColumnType<T> {
const searchInputHolder: { current: Input | null } = { current: null };
return {
filterDropdown: ({
setSelectedKeys,
selectedKeys,
confirm,
clearFilters,
}) => (
<div style={{ padding: 8 }}>
<Input
ref={(node) => {
searchInputHolder.current = node;
}}
placeholder="Search"
value={selectedKeys[0]}
onChange={(e) =>
setSelectedKeys(e.target.value ? [e.target.value] : [])
}
onPressEnter={() => confirm()}
style={{ width: 188, marginBottom: 8, display: "block" }}
/>
<Button
type="primary"
onClick={() => confirm()}
icon={<SearchOutlined />}
size="small"
style={{ width: 90, marginRight: 8 }}
>
Search
</Button>
<Button size="small" style={{ width: 90 }} onClick={clearFilters}>
Reset
</Button>
</div>
),
filterIcon: (filtered) => (
<SearchOutlined style={{ color: filtered ? "#1890ff" : undefined }} />
),
onFilterDropdownVisibleChange: (visible) => {
if (visible) {
setTimeout(() => searchInputHolder.current?.select());
}
},
};
}
type Data = {
key: string;
name: string;
};
const data: Data[] = [
{
key: "1",
name: "John Brown",
},
{
key: "2",
name: "Jim Green",
},
{
key: "3",
name: "Joe Black",
},
];
function Hello() {
return (
<div>
<Table
columns={[
{
title: "Name",
dataIndex: "name",
render: (text: string) => text,
...tableColumnTextFilterConfig<Data>(),
onFilter: (value, record) => {
return record.name
.toString()
.toLowerCase()
.includes(value.toString().toLowerCase());
},
},
]}
dataSource={data}
/>
</div>
);
}
ReactDOM.render(<Hello />, document.body);
In short, you can just extract the tableColumnTextFilterConfig function and implement the onFilter property on the desired column.
I am using react-table and here's my table:
<ReactTable
data={this.props.data}
columns={[
{
Header: "id",
accessor: "id",
width: 50
},
{
Header: "Name",
accessor: "name",
width: 200
},
{
Header: "",
id: "id",
Cell: ({ row }) => (
<button onClick={e => this.handleButtonClick(e, row)}>
Click Me
</button>
)
}
]}
defaultPageSize={10}
showPaginationBottom
/>
the action after the button click
handleButtonClick = (e, row) => {
this.setState({ visible: true});
return
<Modal
title="title"
visible={this.state.visible}
>
// show data here
</Modal>
};
So this is how I am working right now, but the modal isn't getting displayed. Can anyone help me with this? What am I doing wrong?
Why you return the modal with handleButtonClick function instead of add it with ReactTable
<ReactTable
data={this.props.data}
columns={[
{
Header: "id",
accessor: "id",
width: 50
},
{
Header: "Name",
accessor: "name",
width: 200
},
{
Header: "",
id: "id",
Cell: ({ row }) => (
<button onClick={e => this.handleButtonClick(e, row)}>
Click Me
</button>
)
}
]}
defaultPageSize={10}
showPaginationBottom
/>
{this.state.visible && <Modal
title="title"
visible={this.state.visible}
>
// show data here
</Modal>}
handleButtonClick = (e, row) => {
this.setState({ visible: true});
};