This post illustrates how to perform CRUD operations using ag-Grid server-side row model and JSON-server, GraphQL, Apollo-client and React/TypeScript. We've built an example to demonstrate how to implement the necessary operations to have a fully-functional ag-Grid in this scenario. I hope this helps you effectively use ag-Grid with GraphQL and React!
Live sample
You can download the project from GitHub and easily reuse its code to implement this in your project. Please see a quick demo of the sample below showing the CRUD operations:
Architecture
Let's go over the components/frameworks and their roles in this:
- JSON-server: Provides a full fake REST API that we'll use to create, read, update and delete records
- GraphQL: Allows requesting the exact data we need from our server
- Apollo-client: Allows building queries directly from the front-end
- React/TypeScript: Used to bootstrap a single-page React application using create-react-app
Please see this illustrated in the diagram below:
Contents
Installation
Follow the steps below to set up the sample project:
- Clone or download the sample project from this Github repository.
- Install the dependencies:
$ npm install
$ npm run client-dependencies
3. Run the project
$ npm run dev
- JSON server runs on port 3000 (default)
- GraphQL server runs on port 5000 (see
/server.js
) - Client runs on port 3006 (see
/client/package.json
)
Backend
JSON-server
JSON server will serve as the backend of our sample. The script that generates our JSON server is shown below:
"json:server": "json-server --watch data.json"
and here is the file being watched:
{
"olympicWinners": [
{
"athlete": "Michael Phelps",
"age": 23,
"country": "United States",
"year": 2008,
"date": "24/08/2008",
"sport": "Swimming",
"gold": 8,
"silver": 0,
"bronze": 0,
"total": 8,
"id": "0"
},
We are now able to make HTTP requests (GET
/ POST
/ PUT
/ PATCH
/ DELETE
) to interact with this data as if it were a real backend.
GraphQL
When requesting rows, ag-Grid executes the getRows(params)
callback provided in your application's server-side datasource. Rows fetched from the server are then provided back to the grid using the params.successCallback(rows, lastIndex)
method with the parameters below:
(1) the records to be displayed in the grid
(2) the total number of rows in your dataset (optional - for configuring the grid's vertical scrollbar).
const ResponseType = new GraphQLObjectType({
name: 'Response',
fields: () => ({
lastRow: { type: GraphQLInt },
rows: { type: new GraphQLList(OlympicWinnerType) }
})
})
The params
passed to getRows
method contains all the information required by the server when fetching the data. Metadata is included for sorting, filtering, grouping and pivoting data.
In this post, we'll only focus on implementing sorting in our GraphQL service using the code below:
const RootQuery = new GraphQLObjectType({
name: 'RootQueryType',
fields: {
getRows: {
type: ResponseType,
args: {
startRow: { type: GraphQLNonNull(GraphQLInt) },
endRow: { type: GraphQLNonNull(GraphQLInt) },
sortModel: { type: new GraphQLList(SortModelType) }
// filterModel: {}
// groupKeys: []
// pivotCols: []
// pivotMode: false
// rowGroupCols: []
// valueCols: []
},
resolve(parentValue, args) {
let endPoint = JSON_SERVER_ENDPOINT;
const isRequestSorting = args.sortModel && args.sortModel.length > 0;
if (isRequestSorting) {
const fields = [];
const orders = [];
args.sortModel.forEach(sM => {
fields.push(sM.colId);
orders.push(sM.sort)
});
// sorting
endPoint += `?_sort=${fields.join(',')}&_order=${orders.join(',')}`;
// starting from start row with a limit of endRow - startRows rows
endPoint += `&_start=${args.startRow}&_limit=${args.endRow - args.startRow}`;
} else {
endPoint += `?_start=${args.startRow}&_end=${args.endRow}`;
}
return axios.get(endPoint)
.then(res => {
return {
rows: res.data,
lastRow: res.headers["x-total-count"]
}
})
.catch(err => console.log(err));
}
},
// ...
Frontend
Apollo-client
To communicate with the GraphQL server we've built, we're using Apollo-client, instantiated like this:
const client = new ApolloClient({
uri: 'http://localhost:5000/graphql',
cache: new InMemoryCache()
});
We can now easily write GraphQL queries on our frontend (more specially, in our server-side datasource) in the GraphQL Schema Definition Language.
ag-Grid Datasource
Now that all the groundwork has been laid, configuring the ag-Grid server-side datasource is simple. You use the ag-Grid server-side row model when you want to load the data from the data source in chunks as it's needed instead of all in one go. You can see a comparison between the different row models in our documentation.
In this sample we're using the ag-Grid with the server-side row model to load data in chunks, which is useful for very large datasets. Whenever ag-Grid requests rows, we make queries to GraphQL using our Apollo-Client instance. We pass the parameters startRow
and endRow
to our query in order to retrieve the desired rows, and the parameter sortModel
to apply whatever sorting is currently being applied on our ag-Grid instance.
const createServerSideDatasource = function (): IServerSideDatasourceWithCRUD {
return {
getRows: function (params: IServerSideGetRowsParams) {
let {
startRow,
endRow,
sortModel,
// filterModel,
// groupKeys,
// pivotCols,
// pivotMode,
// rowGroupCols,
// valueCols
}: IServerSideGetRowsRequest = params.request;
sortModel = sortModel.length > 0 ? sortModel : undefined;
const visibleColumnIds: string[] = params
.columnApi.getAllDisplayedColumns().map(col => col.getColId());
client.query({
query: gql`
query GetRows($startRow: Int!, $endRow: Int!, $sortModel: [SortModel]) {
getRows(startRow: $startRow, endRow: $endRow, sortModel: $sortModel) {
lastRow
rows {
id,
${visibleColumnIds.join('\n')}
}
}
}
`,
variables: {
startRow,
endRow,
sortModel
}
})
.then(res => res.data.getRows)
.then(({ lastRow, rows }: { lastRow: number, rows: IOlympicWinner[] }) => {
params.successCallback(rows, lastRow);
})
.catch(err => {
params.failCallback();
});
},
// ...
Note how we are leveraging GraphQL's ability to only request the exact data we need by listing the fields of each record that are visible in ag-Grid (in addition to the ID of each record).
getRows(startRow: $startRow, endRow: $endRow, sortModel: $sortModel) {
lastRow
rows {
id,
${visibleColumnIds.join('\n')}
}
}
This works well with our application as we have a columns tool panel accessory that allows us to select/deselect what data fields we would like to show on our grid. This delivers faster query execution and a smaller memory footprint on the client as ag-Grid only requests the data it needs to show. See how we're selecting/de-selecting columns using the columns tool panel in the image below:
Finally, we'll register our datasource in our App component:
const App: FunctionComponent = (): React.ReactElement => {
const datasource: IServerSideDatasourceWithCRUD = createServerSideDatasource();
const onGridReady = (params: GridReadyEvent) => {
params.api.setServerSideDatasource(datasource);
}
// ...
So far we've only used the GET
HTTP protocol in our application. We'll know focus on adding CRUD operations so that we can interact with our back-end more!
Implementing CRUD Operations
Now that we've set up the frontend and backend components of our sample, let's look at how to implement support for adding/updating/deleting records via ag-Grid.
Creating Records
When creating a record, we open a form for the user to fill in as shown below:
When the form is submitted, we execute the function below to add this row:
const addRow: IFormSubmitHandler = (data: IOlympicWinner) => {
datasource
.createRow(data)
.then(() => {
gridApi.purgeServerSideCache();
})
};
Our server-side datasource then sends the following query:
createRow(data: IOlympicWinner): Promise {
return client.mutate({
mutation: gql`
mutation CreateRow($data: OlympicWinnerInput!) {
createRow(data: $data) {
id
athlete
age
country
year
date
sport
gold
silver
bronze
total
}
}`,
variables: {
data
}
})
.then(res => {
return res.data.createRow;
})
.catch(err => console.log('err', err));
}
GraphQL responds to the mutation and sends a POST
request to our JSON-server.
createRow: {
type: OlympicWinnerType,
args: {
data: {
type: GraphQLNonNull(OlympicWinnerInputType)
},
},
resolve(parentValue, args) {
return axios.post(JSON_SERVER_ENDPOINT, args.data)
.then(res => res.data)
.catch(err => console.log(err));
}
}
Note: Newly-created records are added to the bottom of the dataset.
Reading and Updating Records
Since our application only requests the fields of data that are currently shown in the grid, we first request to read all of a selected record (athlete's) data before opening up a form for the user to update said record (athlete).
const updateSelectedRowHandler: React.MouseEventHandler = () => {
const selectedNode = getSelectedNode();
if (selectedNode) {
const selectedRowId: string = selectedNode.id;
// first query all of the rows data before passing it to the form
datasource
.readRow(selectedRowId)
.then((selectedRow: IOlympicWinner) => {
openForm(selectedRow, updateRow);
});
}
};
After submitting the form, a similar process to creating a row is performed - we first execute a method on our server-side data source, then send a query to GraphQL.
Here's the code executed to refresh the server-side data source:
const updateRow: IFormSubmitHandler = (data: IOlympicWinner) => {
datasource
.updateRow(data)
.then(() => {
gridApi.purgeServerSideCache();
})
};
This is then followed by sending a query to GraphQL:
updateRow(data: IOlympicWinner): Promise<any> {
return client.mutate({
mutation: gql`
mutation UpdateRow($data: OlympicWinnerInput!) {
updateRow(data: $data) {
id
athlete
age
country
year
date
sport
gold
silver
bronze
total
}
}
`,
variables: {
data
}
})
.then(res => {
return res.data.updateRow;
})
.catch(err => console.log('err', err));
}
GraphQL responds to this query and updates the record on our JSON-server via a PATCH
request.
updateRow: {
type: OlympicWinnerType,
args: {
data: { type: GraphQLNonNull(OlympicWinnerInputType) },
},
resolve(parentValue, args) {
return axios.patch(`${JSON_SERVER_ENDPOINT}/${args.data.id}`, args.data)
.then(res => res.data)
.catch(err => console.log(err));
}
}
Deleting Records
Let's now add the ability to delete records - please see this illustrated below:
On selecting a row and clicking the 'Delete Selected Row' button, the following handler is executed:
const deleteSelectedRowHandler: React.MouseEventHandler = () => {
const selectedNode = getSelectedNode();
if (selectedNode && window.confirm('Are you sure you want to delete this node?')) {
const selectedRowId: string = selectedNode.id;
datasource
.deleteRow(selectedRowId)
.then(() => {
gridApi.purgeServerSideCache();
});
}
}
Again, our datasource uses Apollo-client to write up the mutation and send it to GraphQL.
deleteRow(id: string): Promise {
return client.mutate({
mutation: gql`
mutation DeleteRow($id: ID!) {
deleteRow(id: $id) {
id
athlete
age
country
year
date
sport
gold
silver
bronze
total
}
}`,
variables: {
id
}
})
.then(res => {
return res.data.deleteRow;
})
.catch(err => console.log('err', err));
}
GraphQL picks this up and sends a DELETE
request to our JSON-server.
deleteRow: {
type: OlympicWinnerType,
args: {
id: { type: GraphQLNonNull(GraphQLID) }
},
resolve(parentValue, args) {
return axios.delete(`${JSON_SERVER_ENDPOINT}/${args.id}`)
.then(res => res.data)
.catch(err => console.log(err))
}
}
What's next?
We hope that you find this article useful if you're looking to use GraphQL with ag-Grid. Remember you can download the project from GitHub and easily reuse the code I've described above to implement this in your project.
If you would like to try out ag-Grid check out our getting started guides (JS / React / Angular / Vue)
Happy coding!