Frontend
Dec 17, 2023
| Badescu Petre Cosmin
Every once in a while, when we begin a project, we have to think about utilizing a data grid solution. Our need for a more robust one may arise from the complexity of the requirements. Ag Grid is a highly configurable Javascript data grid that makes it easier to integrate features like row selection, grouping, filtering, and pagination and works well in scenarios where we need to process data in multiple ways.
Key features
Among Ag Grid's essential characteristics are:
The purpose of the following sections is to show how to easily combine Ag Grid with React and create a grid structure that can support numerous grid instances. For this project, we will use vite as a building tool, so the first step is to create a react app using vite.
A key consideration here is to install only the modules that are needed for our application, avoiding the installation of Ag Grid's packages and modules in combination, which would result in a bundle size that is greater than what is actually needed. You can read more about it here.
Having this in mind, let's install the following modules that we need for this project:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// grid.component.tsx
import { forwardRef } from "react";
import "@ag-grid-community/styles/ag-grid.css";
import "@ag-grid-community/styles/ag-theme-alpine.css";
import { AgGridReact, AgGridReactProps } from "@ag-grid-community/react";
import { ClientSideRowModelModule } from "@ag-grid-community/client-side-row-model";
import { IGridRef } from "./grid.types";
import { useGrid } from "./grid.hook";
import "./grid.styles.css";
const Grid = forwardRef<IGridRef, AgGridReactProps>(({ ...props }, ref) => {
const { getRowId, onFirstDataRendered, gridRef, gridOptions } = useGrid(ref);
return (
<div className="ag-theme-alpine ag-theme-custom container">
<AgGridReact
ref={gridRef}
getRowId={getRowId}
onFirstDataRendered={onFirstDataRendered}
gridOptions={gridOptions}
modules={[ClientSideRowModelModule]}
{...props}
/>
</div>
);
});
export default Grid;
This is the central point where the configuration of the grid is defined. The AgGridReact takes a number of properties, including:
- ref (it will help us to control the grid from the parent component, which is what we will be doing in the next steps).
- getRowId (pointing to the AG Grid, which field from our configuration represents the unique id of the row).
- gridOptions (this is also very important to be defined because it enables or disables some of the key features of the grid and also controls some styling properties).
Additionally, we use the custom hook useGrid. Its role is to manage the business logic of the main grid configuration, and it looks like this:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// grid.hook.ts
import { GetRowIdParams, GridOptions } from "@ag-grid-community/core";
import { AgGridReact } from "@ag-grid-community/react";
import { useRef, useImperativeHandle, useCallback } from "react";
import { CustomerProps } from "../Customer/customer.types";
import { IGridRef, TransactionActionProps } from "./grid.types";
const gridOptions: GridOptions = {
groupHeaderHeight: 34,
headerHeight: 30,
defaultColDef: {
resizable: true,
sortable: true,
editable: true,
filter: "agTextColumnFilter",
menuTabs: ["filterMenuTab"],
},
};
export const useGrid = (ref: React.ForwardedRef<IGridRef>) => {
const gridRef = useRef<AgGridReact<CustomerProps>>(null);
useImperativeHandle(ref, () => ({
applyTransaction: (
data: CustomerProps | { id: number },
action: TransactionActionProps = TransactionActionProps.add
) =>
gridRef.current?.api.applyTransaction({
[action]: [{ ...data }],
}),
}));
const onFirstDataRendered = useCallback(() => {
gridRef.current &&
gridRef.current.api.sizeColumnsToFit({
defaultMinWidth: 100,
});
}, []);
const getRowId = useCallback(
(params: GetRowIdParams<CustomerProps>) => String(params.data.id),
[]
);
return {
getRowId,
onFirstDataRendered,
gridRef,
gridOptions,
};
};
The key thing here is the useImperativeHandle hook that is exposing the grid APIs to the parent component, in our case, applyTransaction. More about grid APIs here.
From this hook, we control all the core CSS properties of the grid (header height, group header height) and also the available features of the grid. In addition to being editable and resizable, our grid also has filtering enabled. We can set all of this up by making changes to the gridOptions object.
Additionally, after the grid is rendered, the onFirstDataRendered function is called. In this case, we want to resize the columns as needed to fit them in the grid and prevent overflow behavior.
The grid comes with default theme configuration. We can also extend the theme with custom css variables
1
2
3
4
5
6
7
8
9
10
// grid.styles.css
.ag-theme-custom {
--ag-alpine-active-color: #1260cc;
--ag-value-change-value-highlight-background-color: #82caff;
}
.container {
height: 400px;
width: 1400px;
}
The grid component that was created above can be used in multiple instances, and depending on the circumstances, each instance can override the grid core functionalities.
For example, let's create a customer grid configuration that should show information about customers.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
// customerColumns.config.tsx
import { ColDef, ValueGetterParams } from "@ag-grid-community/core";
import { useMemo } from "react";
import { CustomerProps } from "./customer.types";
export const useColumns = (): ColDef<CustomerProps>[] =>
useMemo(
() => [
{
field: "id",
enableCellChangeFlash: true,
suppressMovable: true,
sortable: false,
resizable: false,
},
{ field: "name", sort: "asc", enableCellChangeFlash: true },
{
field: "username",
enableCellChangeFlash: true,
},
{
field: "email",
enableCellChangeFlash: true,
cellRenderer: (props: { value: string }) => (
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
width="14"
height="14"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
style={{ marginRight: "5px" }}
>
<rect x="1" y="3" width="22" height="18" rx="2" ry="2" />
<polyline points="1 10 12 15 23 10" />
</svg>
{props.value}
</div>
),
},
{
field: "phone",
enableCellChangeFlash: true,
},
{
enableCellChangeFlash: true,
headerName: "Addresss",
children: [
{
field: "Street address",
valueGetter: (params: ValueGetterParams<CustomerProps, string>) =>
params.data?.address.street,
enableCellChangeFlash: true,
},
{ field: "address.zipcode", enableCellChangeFlash: true },
{ field: "address.number", enableCellChangeFlash: true },
],
},
],
[]
);
We assigned certain properties to every column. Another important thing to add is the ability to override the default configuration and reject the resizable and sortable feature for a single column—in our example, the id column.
We can also use cellRenderer in the column configuration to change the cell content. In our case, for the email column, we used cellRenderer to manipulate the result by appending an extra email icon to the cell's value.
The object's property that we want to show in the row has to match the field value of the configuration object.
In case this is not possible, we have the valueGetter that can help us pick the right value to display in the cell.
The rows are represented by the data that generally comes from an external API. Let's create a hook that should retrieve the customers from a mock data array.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
// customer.hook.ts
import { useEffect, useState } from "react";
import { useColumns } from "./customerColumns.config";
import { CustomerProps } from "./customer.types";
export const MOCK_DATA = [
{
id: 1,
name: "Jane Doe",
username: "janeDoe23",
email: "janeDoe@gmail.com",
address: {
street: "melody drive",
zipcode: 12345,
number: 23,
},
phone: "0762131223",
},
{
id: 2,
name: "John Will",
username: "john2@",
email: "johnwill@gmail.com",
address: {
street: "new st",
zipcode: 44223,
number: 31,
},
phone: "2323114212",
},
{
id: 3,
name: "Sam Doe",
username: "samm",
email: "samdoe@gmail.com",
address: {
street: "loch ness road",
zipcode: 44421,
number: 23,
},
phone: "4231234211",
},
{
id: 4,
name: "Jane Lee",
username: "lee32",
email: "janelee@gmail.com",
address: {
street: "gramdios drive",
zipcode: 31255,
number: 31,
},
phone: "444828281",
},
{
id: 5,
name: "Crow Ross",
username: "ross",
email: "ross@gmail.com",
address: {
street: "gramdios drive",
zipcode: 31255,
number: 55,
},
phone: "4141231241",
},
];
const getData = (): Promise<CustomerProps[]> =>
new Promise((resolve) => {
setTimeout(() => resolve(MOCK_DATA), 500);
});
export const useGetCustomers = () => {
const [customers, setCustomers] = useState<CustomerProps[]>([]);
const columns = useColumns();
useEffect(() => {
getData()
.then((res) => setCustomers(res))
.catch((err) => console.log(err));
}, []);
return { customers, columns };
};
The only thing left to do is to create a grid using the mock data and the configuration file we defined above.
Our main grid component expects (columnsDefs and rowData) to display data so we can safely send these two properties.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// customer.component.tsx
import { useRef } from "react";
import Grid from "../Grid/grid.component";
import { IGridRef, TransactionActionProps } from "../Grid/grid.types";
import { useGetCustomers } from "./customer.hook";
export const Customers = () => {
const { customers, columns } = useGetCustomers();
const gridRef = useRef<IGridRef | null>(null);
return (
<>
<button
onClick={() =>
gridRef.current?.applyTransaction(
{
id: 1,
name: "Will Joe",
username: "will11",
email: "willjoe@gmail.com",
address: {
street: "lorem street",
zipcode: Math.floor(Math.random() * 1000),
number: Math.floor(Math.random() * 100),
},
phone: "087132113",
},
TransactionActionProps.update
)
}
>
Update Row with id 1
</button>
<button
onClick={() =>
gridRef.current?.applyTransaction(
{
id: 3,
},
TransactionActionProps.remove
)
}
>
Remove row with id 3
</button>
<Grid ref={gridRef} columnDefs={columns} rowData={customers} />
</>
);
};
Just for demonstration, we added two buttons that trigger one of the grid APIs (applyTransaction). In our case, we updated the grid information and also removed one row from the grid.
The final project can be seen here
You can access the source code from this link
You can play around with the grid, hiding, rearranging, sorting, and even filtering some of the columns based on different parameters.
Ag Grid is a reliable data grid system that can easily scale to meet even the most demanding needs. In the event that a chart is required to display the results, it even includes a chart package. A chart integration is also attached to the GitHub repository. The typescript support is the only drawback (I sometimes have trouble finding the appropriate types), but overall, I believe it's a very good option to take into consideration.