I'm using a custom CategoryFilter for my categories AgGrid:
const columnDefs = [
{
field: 'categoryId',
headerName: t('category', { ns: 'common' }),
cellRendererSelector: (params: ICellRendererParams<IProduct>) => ({
component: CategoryColumn,
params: {
...params,
defaultLanguage
}
}),
filter: CategoryFilter,
floatingFilterComponentParams: {
suppressFilterButton: false,
},
}
]
When I filter by category, I can enter the value in the input field, but it's still not showing up under the column's name:
How do I display the selected category under the "Category" column title?
You can pass the <CategoryColumn/> reference to the parent component via forwardRef and access the parent model's value via function params of useImperativeHandle inside the <CategoryColumn/>.
Example
CategoryColumn
import React, {
Fragment,
forwardRef,
useImperativeHandle,
useRef,
} from 'react';
import { IFloatingFilterParams } from 'ag-grid-community';
export default forwardRef((props: IFloatingFilterParams<any>, ref) => {
const inputRef = useRef<HTMLInputElement>(null);
// expose AG Grid Filter Lifecycle callbacks
useImperativeHandle(ref, () => {
return {
onParentModelChanged(parentModel: number | null) {
console.log(parentModel)
// When the filter is empty we will receive a null value here
if (parentModel == null) {
inputRef.current!.value = '';
} else {
inputRef.current!.value = parentModel + '';
}
},
};
});
return (
<Fragment>
>{' '}
<input
ref={inputRef}
style={{ width: '30px' }}
type="number"
min="0"
/>
</Fragment>
);
});
index.tsx
...
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: 'athlete', filter: 'agTextColumnFilter' },
{
field: 'gold',
floatingFilterComponent: NumberFloatingFilterComponent,
floatingFilterComponentParams: {
suppressFilterButton: true,
},
filter: NumberFilterComponent,
},
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
filter: true,
floatingFilter: true,
};
}, []);
...
Have read of Ag-grid Floating Filter!
Related
I am using ag-grid and I would like to pass the type of the data the component expects. When I pass in a type, I get an error message:
Expected 0 type arguments, but got 1. The component composes
AgGridReact using forwardRef.
How do I pass in a type without encountering an error or how do I modify the Table component to accept generic types?
This is my code:
App.tsx
import * as React from 'react';
import { AgGridReact } from 'ag-grid-react';
import { ColDef, ColGroupDef } from 'ag-grid-enterprise';
import { GridOptions } from 'ag-grid-community';
import {
FirstDataRenderedEvent,
GridReadyEvent,
} from 'ag-grid-community/dist/lib/events';
import Table from './Table';
import './style.scss';
export interface ITerritoriesByZip {
TERR_ID: string;
TERR_NAME: string;
FULL_NAME: string;
WORK_EMAIL: string;
OFFICE_PHONE: string;
MOBILE_PHONE: string;
ADDRESS1: string;
CITY: string;
STATE: string;
ZIP: string;
}
export const columnDefs: (ColDef | ColGroupDef)[] = [
{
headerName: 'TERR_ID',
field: 'TERR_ID',
},
{
headerName: 'TERR_NAME',
field: 'TERR_NAME',
},
{
headerName: 'Full Name',
field: 'FULL_NAME',
},
{
headerName: 'Work Email',
field: 'WORK_EMAIL',
},
{
headerName: 'Office Phone',
field: 'OFFICE_PHONE',
},
{
headerName: 'Mobile Phone',
field: 'MOBILE_PHONE',
},
{
headerName: 'Address 1',
field: 'ADDRESS1',
},
{
headerName: 'City',
field: 'CITY',
},
{
headerName: 'State',
field: 'STATE',
},
{
headerName: 'ZIP',
field: 'ZIP',
},
];
const gridOptions: GridOptions = {
alignedGrids: [],
defaultColDef: {
editable: true,
sortable: true,
resizable: true,
filter: false,
flex: 1,
minWidth: 100,
},
};
export default function App() {
const currentMainGridRef = React.useRef<AgGridReact<ITerritoriesByZip>>(null);
const data = [];
const onGridReady = React.useCallback((params: GridReadyEvent) => {
params.api.sizeColumnsToFit();
}, []);
const onCurrentFirstDataRendered = React.useCallback(
(params: FirstDataRenderedEvent) => {
params.api.sizeColumnsToFit();
},
[]
);
return (
<div>
<h1>Hello StackBlitz!</h1>
<p>Start editing to see some magic happen :)</p>
<Table<ITerritoriesByZip>
ref={currentMainGridRef}
rowData={data}
rowCount={data.length}
columnDefs={columnDefs}
isLoading={false}
isSuccess={true}
pagination={false}
onGridReady={onGridReady}
onFirstDataRendered={onCurrentFirstDataRendered}
gridOptions={gridOptions}
/>
</div>
);
}
Table.tsx
import { Box, LinearProgress, Typography } from '#mui/material';
import React, {
ForwardedRef,
forwardRef,
useEffect,
useMemo,
useState,
} from 'react';
import { AgGridReact } from 'ag-grid-react';
import {
AgGridReactProps,
AgReactUiProps,
} from 'ag-grid-react/lib/shared/interfaces';
import { has, isEmpty, isFunction, isNull, map } from 'lodash';
import { renderToStaticMarkup } from 'react-dom/server';
import { GridApi } from 'ag-grid-community/dist/lib/gridApi';
import { ColumnApi } from 'ag-grid-community/dist/lib/columns/columnApi';
import { GridOptions } from 'ag-grid-community';
import { ColumnState } from 'ag-grid-community/dist/lib/columns/columnModel';
import {
ColDef,
ColGroupDef,
} from 'ag-grid-community/dist/lib/entities/colDef';
import { GridReadyEvent } from 'ag-grid-community/dist/lib/events';
import CustomPagination from './PaginationRenderer';
interface ITable<T> extends AgGridReactProps, AgReactUiProps {
isLoading: boolean;
isSuccess: boolean;
id?: string;
pagination: boolean;
rowData?: T[] | null;
title?: string;
gridOptions?: GridOptions;
rowCount: number;
columnDefs?: (ColDef | ColGroupDef)[] | null;
onGridReady?: (params: GridReadyEvent) => void;
}
export interface AutoSizeRef {
autoSizeAll: () => void;
}
const AgGridTable = <T extends {}>(
props: ITable<T>,
ref: ForwardedRef<AgGridReact<T>>
) => {
const [colDefState, setColDefState] = useState<ColumnState[]>([]);
const autoSizeAll = (): void => {
if (ref != null && !isFunction(ref)) {
const allColumnIds = map(
(ref.current?.columnApi as ColumnApi).getColumns(),
(column) => {
return (column as any).getId();
}
);
(ref.current?.columnApi as ColumnApi).autoSizeColumns(
allColumnIds,
false
);
}
};
// useImperativeHandle(ref, () => ({
// autoSizeAll,
// }));
useEffect(() => {
if (
ref != null &&
!isFunction(ref) &&
!isNull(ref.current) &&
!isEmpty(ref.current) &&
has(ref.current, 'api')
) {
setTimeout(() => autoSizeAll(), 400);
}
}, [ref]);
const containerStyle = useMemo(() => ({ width: '100%', height: '100%' }), []);
const gridStyle = useMemo(() => ({ height: '100%', width: '100%' }), []);
const defaultExcelExportParams = useMemo(() => {
return {
allColumns: true,
};
}, []);
const { isLoading, pagination, id, isSuccess, onGridReady, ...rest } = props;
useEffect(() => {
if (ref != null && !isFunction(ref)) {
if (
isLoading &&
!isNull(ref.current) &&
!isEmpty(ref.current) &&
has(ref.current, 'api')
) {
(ref.current?.api as GridApi).showLoadingOverlay();
}
if (
isEmpty(props.rowData) &&
!isNull(ref.current) &&
!isEmpty(ref.current) &&
has(ref.current, 'api')
) {
(ref.current?.api as GridApi).showNoRowsOverlay();
}
}
}, [isLoading, ref, props.rowData]);
const onTableGridReady = (params: GridReadyEvent) => {
setColDefState(params.columnApi.getColumnState());
};
return (
<Box className="pt-2" style={containerStyle}>
{isLoading && <LinearProgress variant="indeterminate" />}
<Box className="ag-theme-alpine" id={id} style={gridStyle}>
<AgGridReact<T>
animateRows
ref={ref}
pagination={pagination}
domLayout="autoHeight"
paginationPageSize={100}
onGridReady={(params) => {
onTableGridReady(params);
if (onGridReady) {
onGridReady(params);
}
}}
suppressPaginationPanel
statusBar={{
statusPanels: [
{
statusPanel: CustomPagination,
statusPanelParams: {
dataCount: props.rowCount,
paginationEnabled: pagination,
},
},
],
}}
gridOptions={props.gridOptions}
defaultExcelExportParams={defaultExcelExportParams}
overlayLoadingTemplate={
'<span class="ag-overlay-loading-center">Please wait while your rows are loading</span>'
}
{...rest}
/>
</Box>
</Box>
);
};
const Table = forwardRef(AgGridTable);
export default Table;
Stackblitz Link: Link.
Any help is appreciated.
in Table.tsx do the following:
const TableRef = forwardRef(AgGridTable);
const Table = <T extends {}>({
myRef,
...rest
}: ITable<T> & { myRef: ForwardedRef<AgGridReact<T>> }) => (
<TableRef {...rest} ref={myRef} />
);
export default Table;
Check the fork
Another option is to use wrapper:
const AgGridTable = <T extends {}>() => forwardRef<AgGridReact<T>, ITable<T>>((props, ref) => {...}
const Table = AgGridTable();
export default Table;
So I have a component which uses a CellRenderer which gets some data:
import { useEffect, useMemo, useState } from "react";
import "ag-grid-community/dist/styles/ag-grid.min.css";
import "ag-grid-community/dist/styles/ag-theme-material.min.css";
import Grid from "../Common/Grid";
import axios from "axios";
import SelectJudetCellRenderer from "./SelectJudetCellRenderer";
function GetJudete() {
return axios
.get("http://localhost:5266/api/judete")
.then((response) => {
let data = response.data;
return data;
})
.catch((err) => {
console.log("Eroare la aducerea datelor.");
});
}
function Localitati() {
let [judete, setJudete] = useState([]);
useEffect(() => {
async function GetJudeteAsync() {
const result = await GetJudete();
setJudete(result);
}
GetJudeteAsync();
}, []);
const [columnDefs] = useState([
{ field: "nume", filter: "agTextColumnFilter", editable: true },
{ field: "judet", filter: "agTextColumnFilter", editable: true, cellRenderer: SelectJudetCellRenderer, cellRendererParams: {judete: judete} },
]);
return (
<Grid
baseLink="http://localhost:5266/api/localitati"
columnDefs={columnDefs}
/>
);
}
export default Localitati;
Here's my Cell renderer:
import { ICellRendererParams } from 'ag-grid-community';
export interface JudeteCellRendererParams extends ICellRendererParams {
judete: any[];
}
function SelectJudetCellRenderer(props: JudeteCellRendererParams) {
console.log(props.judete)
return (
<select name="judete">
{
props.judete.map((judet) =>
<option value={judet.id}>{judet.name}</option>
)
}
</select>
)
}
export default SelectJudetCellRenderer;
The problem is that after the Async call Judete is getting new data but my cell renderer does not get the new data.
The console.log() from the CellRenderer returns an empty array.
Why is this happening and how can I fix it?
Thanks.
You need to tell AG Grid to refresh the rendered cell, this is not very well documented, see https://www.ag-grid.com/javascript-data-grid/component-cell-renderer/#cell-renderer-component
Here is a simple example using Angular (should be similar for class based React)
Notice the refresh() method:
// gets called whenever the user gets the cell to refresh
refresh(params: ICellRendererParams) {
// set value into cell again
this.cellValue = this.getValueToDisplay(params);
}
https://plnkr.co/edit/yFqQHfNjxMLrPb9f.
For functional components you should explicitly call the api.refreshCells() when the data is available.
See here for more details: https://www.ag-grid.com/react-data-grid/component-cell-renderer/#component-refresh
A possible solution (although I think it would be more simple to switch to a class component renderer)
function Localitati() {
let [judete, setJudete] = useState([]);
// get hold of AG Grid gridApi
const gridApiRef = React.useRef<GridApi>();
// update the 'judete' column when new data is available (this will re-invoke the cell renderers)
useEffect(() => {
gridApiRef.current.refreshCells({columns: 'judet'});
}, [judete]);
useEffect(() => {
async function GetJudeteAsync() {
const result = await GetJudete();
setJudete(result);
}
GetJudeteAsync();
}, []);
const [columnDefs] = useState([
{ field: "nume", filter: "agTextColumnFilter", editable: true },
{ field: "judet", filter: "agTextColumnFilter", editable: true, cellRenderer: SelectJudetCellRenderer, cellRendererParams: {judete: judete} },
]);
return (
<Grid
baseLink="http://localhost:5266/api/localitati"
columnDefs={columnDefs}
onGridReady={({ api }) => {
gridApiRef.current = api;
}}
/>
);
}
I suspect it is a syntax issue. When I use the same approach for instead of I do get the expected results. The data is coming from an endpoint through the import of getAssembly which is generated via await fetch and the results are being rendered as JSON before the data is imported.
The commented out code on line how I assume the commanded need to be executed and I get no error, but no data is rendered. The code on line 50 works fine but does not provide the filtering. I intend the use the value in the typeName variable once the filtering is working.
import { useState, useEffect } from "react";
import MenuItem from "#mui/material/MenuItem";
import Select from "#mui/material/Select";
import InputLabel from "#mui/material/InputLabel";
import ListItemText from "#mui/material/ListItemText";
import { DataGrid } from "#mui/x-data-grid";
import { getAssembly } from "./GetAssembly";
const columns = [
{ field: "id", headerName: "ID", width: 300 },
{ field: "status", headerName: "Status", width: 90 },
{ field: "atype", headerName: "AType", width: 80 },
{ field: "name", headerName: "Name", width: 350 },
{ field: "time", headerName: "Start Time", width: 150 },
{ field: "org", headerName: "Organization", width: 350 },
];
export default function SelectAssembly() {
const [typeName, setTypeName] = useState([""]);
// const [assemRows, setAssemRows] = useState([]);
const [state, setState] = useState({
assembly: [],
assembtypename: [],
unsignenList: [],
});
const handleTypeChange = (event) => {
const {
target: { value },
} = event;
setTypeName(value);
};
console.log(typeName);
useEffect(() => {
console.log("useEffect");
getAssembly().then((res) => {
setState((prevState) => ({ ...prevState, assembly: res.assemblies }));
});
}, []);
const typeSelection = [
...new Set(state.assembly.map((item) => item.assemblyType)),
];
//const assemList = state.assembly.filter(assem => assem === "batch").map(assem => {
const assemList = state.assembly.map((assem) => {
return {
id: assem.assemblyId,
status: assem.status,
atype: assem.assemblyType,
name: assem.name,
time: assem.timeStarted,
org: assem.organizationId,
asid: assem.referenceId,
pmap: assem.propertiesMap,
};
});
// const unsignedList = assemList.filter((str) => {
// //str can include or str can equal with === (return str.includes("import");)
// return str === "import";
// });
return (
<div>
<InputLabel sx={{ fontSize: 12 }}>Assembly Type</InputLabel>
<Select
label="Type"
value={typeName}
sx={{ height: 35, fontSize: 10 }}
fullWidth
focused
onChange={handleTypeChange}
>
{typeSelection.map((types) => {
return (
<MenuItem key={types.indexOf(types) > -1} value={types}>
<ListItemText primary={types} />
</MenuItem>
);
})}
</Select>
<br />
<br />
<DataGrid
density="compact"
hideFooterRowCount
rows={assemList}
// rows={unsignedList}
columns={columns}
pageSize={15}
rowsPerPageOptions={[15]}
/>
</div>
);
}
Thanks to Jim Ptak at Southwire for helping me see the problem. In the filter method I did not specify the particular element to filter on. Once the code was modified as follows:
const assemList = state.assembly.filter(assem => assem.assemblyType === "MO import").map(assem => {
//const assemList = state.assembly.map((assem) => {
return {
id: assem.assemblyId,
status: assem.status,
atype: assem.assemblyType,
name: assem.name,
time: assem.timeStarted,
org: assem.organizationId,
asid: assem.referenceId,
pmap: assem.propertiesMap,
};
});
the contents of the datagrid filters perfectly.
I am using material-table to build a table of users from a call to my API. The data returns just fine in the console, but when I got to render it, I get an error. Here is an image of my error.
And my code:
import React, { useState, useEffect, useRef, Fragment } from 'react';
import axios from 'axios';
import { API } from '../../config';
import Layout from '../../components/Layout';
import MaterialTable from 'material-table';
const PendingUser = () => {
const [pendingUser, setPendingUser] = useState({
firstName: '',
lastName: '',
email: '',
agency: ''
});
const isMountedVal = useRef(1);
useEffect(() => {
isMountedVal.current = 1;
return () => {
isMountedVal.current = 0;
};
getPendingUsers();
setPendingUser(pendingUser);
}, []);
const getPendingUsers = async () => {
const { data } = await axios.get(`${API}/admin/pendinguser`);
await data.filter(user => {
user.accountApproved ? setPendingUser(user) : setPendingUser();
setPendingUser(user);
});
};
const handleClick = (name, rowData, index, email) => e => {
e.preventDefault();
try {
if (name === 'deny') {
axios.delete(`${API}/admin/pendinguser/${name}/${rowData._id}`);
} else {
name === 'approve';
axios.put(`${API}/admin/pendinguser/${name}/${rowData._id}`);
}
} catch (error) {
console.log(error);
}
};
const columns = [
{
title: 'First Name',
field: 'firstName'
},
{
title: 'Last Name',
field: 'lastName'
},
{
title: 'Email',
field: 'email'
},
{
title: 'Law Enforcement Agency',
field: 'leAgency'
},
{
title: 'Approve',
field: 'approve',
render: rowData => (
<i
className='far fa-check-circle fa-2x'
style={{ color: 'green' }}
onClick={handleClick('approve', rowData)}
></i>
)
},
{
title: 'Deny',
field: 'deny',
render: rowData => (
<i
className='far fa-times-circle fa-2x'
style={{ color: 'red' }}
onClick={handleClick('deny', rowData)}
></i>
)
},
{
title: 'Denial Reason',
field: 'denialReason',
render: rowData => (
<select>
<option value='Not Law Enforcement'>Not Law Enforcement</option>
<option value='Non US Law Enforcement'>Non US Law Enfrocement</option>
</select>
)
}
];
console.log(pendingUser);
return (
<Layout>
<MaterialTable
title='Pending Users'
columns={columns}
data={pendingUser}
isLoading={!pendingUser.length}
options={{
headerStyle: {
backgroundColor: '#249DCD',
color: 'white',
fontWeight: 'bold'
}
}}
/>
</Layout>
);
};
export default PendingUser;
If I remove the data from the columns render just fine, but what is the point if I cant get the data to render.
Material Table requires data to be either an array or a function. You are instead setting it as an object. So material-table first checks if its an array, its not, so it assumes its a function and tries to invoke it, resulting in the above error.
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.