What is AG-grid?
Be it that you want to display some data from your database or have an advanced way of editing information in a table in your application, you probably need a robust, easy-to-use grid component for managing that goal. That is where AG-grid comes up.
With over 600'000 weekly downloads it is one of the best data-grid libraries in the JavaScript world. Besides the obvious popularity it still boasts an enormous performance boost even when working with huge data sets and still manages to have a ton of useful features for even the most complex use cases.
That kind of a complex use case we are going to go explain in this post.
The problem
For this tutorial we are going to tackle a rather known problem, going over monthly expenses. What we would like to have is a table in which we can enter our expenses (rows) for separate months (columns).
Now this seems fine and dandy, but what happens if you want to try and edit multiple cells at the same time or somehow input the same value for multiple months?
This is where the advanced cell editing of ag-grid comes up. We can override the simple text editing of the grid with a popup which knows how edit multiple cells at one time.
The solution
First thing we need to setup is a basic HTML file which will hold a div
with an id
so we can reference the grid from inside our script file. Besides that we can also define a preexisting theme for the grid. (More about themes can be found here).
<!DOCTYPE html>
<html lang="en">
<head>
<title>AG grid input widget popup</title>
<script src="https://unpkg.com/@ag-grid-community/all-modules@23.0.2/dist/ag-grid-community.min.js"></script>
</head>
<body>
<div id="myGrid" style="height: 100%;" class="ag-theme-balham"></div>
<script src="index.js"></script>
</body>
</html>
Once that is set up we can also add some default styling for the grid so it looks proper.
html, body {
height: 100%;
width: 100%;
margin: 0;
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
}
html {
position: absolute;
top: 0;
left: 0;
padding: 0;
overflow: auto;
}
body {
padding: 1rem;
overflow: auto;
}
td, th {
text-align: left;
padding: 8px;
}
#monthSelection, #inputValue {
width: 100%;
}
.input-widget-popup {
width: 250px;
height: 150px;
}
For the styling applied to the td
and tr
elements and the specific id and class selectors below them - we will go over them in detail when implementing the popup cell editor.
After we have set up the basic HTML skeleton of our grid we now have to head over to the JavaScript side and somehow wire up the grid so we can display some data in it.
What we need to do now is create and index.js file and create the grid with some configuration.
const rowData = [
{
expenses: 'Rent',
january: 1000,
february: 1000
},
{
expenses: 'Food',
january: 150,
february: 125
},
{
expenses: 'Car',
january: 100,
february: 200
},
{
expenses: 'Electricity',
january: 100,
february: 200
},
];
const columnDefs = [
{ field: 'expenses', editable: false },
{ field: 'january', headerName: 'January' },
{ field: 'february', headerName: 'February' },
{ field: 'march', headerName: 'March' },
{ field: 'april', headerName: 'April' },
{ field: 'may', headerName: 'May' },
{ field: 'june', headerName: 'June' },
{ field: 'july', headerName: 'July' },
{ field: 'august', headerName: 'August' },
{ field: 'september', headerName: 'September' },
{ field: 'october', headerName: 'October' },
{ field: 'november', headerName: 'November' },
{ field: 'december', headerName: 'December' }
];
const gridOptions = {
columnDefs,
rowData,
defaultColDef: {
editable: true,
sortable: true
}
};
document.addEventListener('DOMContentLoaded', () => {
const gridDiv = document.querySelector('#myGrid');
new agGrid.Grid(gridDiv, gridOptions);
});
OK, so this might look a little bit overwhelming, but bear with me - we will go over the points and explain it.
- First we need to somehow the element from the DOM. (Remember we introduced a
div
with anid
ofmyGrid
in the HTML file) - After that we just create a new ag grid instance by calling the constructor made available by the ag-grid library
new agGrid.Grid
with thediv
element as an argument and the grid options. - The
gridOptions
are where the magic happens and all of the configurations can be done. - We define the row data (a simple JavaScript array of objects) which holds the data that we want to display
- We define the
columnDefs
- an array of objects that hasfield
which is a unique identifier of a column and aheaderName
which is the text that is displayed in the header of a column - The
defaulColDef
is exactly what the name says - it acts as a default option and adds the defined properties in it to all the other column definitions.
Now that we have the grid setup and all the fields are editable we can go over into wiring up our custom cell editor.
We first need to extend the defaultColDef
with another property cellEditor
which will hold a reference to our custom class for the cell editor.
const gridOptions = {
columnDefs,
rowData,
defaultColDef: {
editable: true,
sortable: true,
cellEditor: ExpensePopupCellEditor
}
};
We will also need to update the first columnDef
for the expenses to use the default cell renderer so for now we can just initialize the cellRenderer
property as an empty string.
javascript
{ field: 'expenses', editable: false, cellRenderer: '' }
For the cell editor we will define a JavaScript class called ExpensePopupCellEditor which will hold our custom logic.
javascript
class ExpensePopupCellEditor {
// gets called once after the editor is created
init(params) {
this.container = document.createElement('div');
this.container.setAttribute('class', 'input-widget-popup');
this._createTable(params);
this._registerApplyListener();
this.params = params;
}
// Return the DOM element of your editor,
// this is what the grid puts into the DOM
getGui() {
return this.container;
}
// Gets called once by grid after editing is finished
// if your editor needs to do any cleanup, do it here
destroy() {
this.applyButton.removeEventListener('click', this._applyValues);
}
// Gets called once after GUI is attached to DOM.
// Useful if you want to focus or highlight a component
afterGuiAttached() {
this.container.focus();
}
// Should return the final value to the grid, the result of the editing
getValue() {
return this.inputValue.value;
}
// Gets called once after initialised.
// If you return true, the editor will appear in a popup
isPopup() {
return true;
}
}
Most of the methods in the popup are self describing so the most interesting part here would be to dive into the init
method.
- First we create the container element which will contain the whole popup and apply the CSS
class
we defined earlier in our HTML file. - After that we create the table structure and register the click listener for the
Apply
button - At the end we also save the
params
object for later use.
_createTable(params) {
this.container.innerHTML = `
<table>
<tr>
<th></th>
<th>From</th>
<th>To</th>
</tr>
<tr>
<td></td>
<td>${params.colDef.headerName}</td>
<td><select id="monthSelection"></select></td>
</tr>
<tr></tr>
<tr>
<td>${params.data.expenses}</td>
<td></td>
<td><input id="inputValue" type="number"/></td>
</tr>
<tr>
<td></td>
<td></td>
<td><button id="applyBtn">Apply</button></td>
</tr>
</table>
`;
this.monthDropdown = this.container.querySelector('#monthSelection');
for (let i = 0; i < months.length; i++) {
const option = document.createElement('option');
option.setAttribute('value', i.toString());
option.innerText = months[i];
if (params.colDef.headerName === months[i]) {
option.setAttribute('selected', 'selected');
}
this.monthDropdown.appendChild(option);
}
this.inputValue = this.container.querySelector('#inputValue');
this.inputValue.value = params.value;
}
In this _createTable(params)
method we create the necessary HTML structure of our popup. We have generated three rows of data for the column headers, the cell input, the dropdown for our months selection and the Apply
button. Note that we also set the cell input value to be the same as the one in the cell that is currently edited.
The months
variable is generated at the start as an array based on the columnDefs
.
let months = columnDefs
.filter(colDef => colDef.field !== 'expenses')
.map(colDef => colDef.headerName);
The last thing to do is to add a listener to the Apply
button and execute logic when it is clicked.
_registerApplyListener() {
this.applyButton = this.container.querySelector('#applyBtn');
this.applyButton.addEventListener('click', this._applyValues);
}
_applyValues = () => {
const newData = { ...this.params.data };
const startingMonthIndex = months.indexOf(this.params.colDef.headerName);
const endMonthIndex = parseInt(this.monthDropdown.value);
const subset = startingMonthIndex > endMonthIndex
? months.slice(endMonthIndex, startingMonthIndex)
: months.slice(startingMonthIndex, endMonthIndex + 1);
subset
.map(month => month.toLowerCase())
.forEach(month => {
newData[month] = this.inputValue.value;
});
this.params.node.setData(newData);
this.params.stopEditing();
}
After the registering the _applyValues
callback to the click
event on the button we do the following:
- Create a copy of the
data
object on theparams
- In this case the
data
holds the whole row data as one object from therowData
array, based on which cell is edited
- In this case the
- Then we need to determing the starting index (based on the currently edited cell) and ending index (based on the selected month from the dropdown) of the months
- After this we can generate an sub array of month keys based on the selection
- While looping through that array we can set the input value for all months from the subset and set that
newData
to therowNode
For example:
A cell edit that stemmed in the March
column for the Rent
expenses and a selection for the ending month of June
with an input value of 500
would generate an object like this:
{
expenses: 'Rent',
january: 1000, // preexisting value
february: 1000, // preexisting value
march: 500,
april: 500,
may: 500,
june: 500
}
At the end we call the stopEditing()
method on the params
after which the grid will close the popup automatically and take over the new values from the newData
object.
As a bonus - we can also have a simple custom cell renderer which will render the cell values as monetary values. We only need to extend the defaultColDef
with another property and define the renderer class similar to the one we did for the editor.
defaultColDef: {
...
cellRenderer: ExpensesCellRenderer,
cellEditor: ExpensePopupCellEditor
}
class ExpensesCellRenderer {
init(params) {
this.gui = document.createElement('span');
if (this._isNotNil(params.value)
&& (this._isNumber(params.value) || this._isNotEmptyString(params.value))) {
this.gui.innerText = `$ ${params.value.toLocaleString()}`;
} else {
this.gui.innerText = '';
}
}
_isNotNil(value) {
return value !== undefined && value !== null;
}
_isNotEmptyString(value) {
return typeof value === 'string' && value !== '';
}
_isNumber(value) {
return !Number.isNaN(Number.parseFloat(value)) && Number.isFinite(value);
}
getGui() {
return this.gui;
}
}
In contrast to the editor - the renderer only needs to define the getGui
method which will return the DOM element of the renderer and the init
which will create the element with the necessary values.
Conclusion
And basically that's all of it!
We saw how easy is to implement a more complex use case of custom editing of cells in AG-grid with only JavaScript, HTML and CSS.
P.S.
The full source code can be found in the following repo on github.
Feel free to raise an issue or open a PR.
Cheers!
Top comments (1)
Hey! Thanks for the post.
In my case, getValue() is not returning the editor value. Do you know the reasons for this?