AG-Grid-React-Series-#25-Validation-in-AG-Grid-Cell-Editing
Data integrity is paramount in any application, and nowhere is this more critical than when users are directly editing data within a grid. AG-Grid, a powerful data grid for React applications, provides robust mechanisms to implement validation during cell editing, ensuring that only valid and meaningful data makes it into your dataset.
This installment in our AG-Grid React series dives deep into various strategies for validating user input during cell editing, from simple synchronous checks to more complex asynchronous validations and visual feedback.
Why is Cell Editing Validation Important?
Without proper validation, your application risks:
- Incorrect Data: Users might inadvertently or intentionally enter malformed or out-of-range data.
- Application Errors: Backend systems or other parts of your frontend might break when processing invalid data types or formats.
- Poor User Experience: Users need immediate feedback when their input is incorrect, rather than discovering errors later.
- Data Inconsistency: Leading to unreliable reports and business decisions.
AG-Grid's Validation Hooks
AG-Grid offers several key points where you can inject validation logic during the cell editing lifecycle:
cellEditorParams.validator: For synchronous validation *within* the cell editor itself, before the value is applied.colDef.valueSetter: A powerful hook that intercepts the value *before* it's set on the row node. This allows for validation, transformation, and even preventing the value from being set.colDef.onCellValueChanged: Triggered *after* the value has been successfully set. Useful for reacting to changes, triggering side effects, or secondary validations.colDef.cellClassRules/colDef.cellStyle: For visually indicating invalid cells.
1. Synchronous Validation with cellEditorParams.validator
The simplest form of validation is to define a validator function directly within the cellEditorParams. This function runs synchronously when the user attempts to exit the cell editor (e.g., by pressing Enter, Tab, or clicking away). If the validator returns false, the value is considered invalid, and the editor remains open.
Let's say we want to ensure an 'Age' column only accepts positive numbers.
import React, { useState, useRef, useCallback } from 'react';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
const MyGridComponent = () => {
const gridRef = useRef();
const [rowData] = useState([
{ id: 1, name: 'Alice', age: 30 },
{ id: 2, name: 'Bob', age: 24 },
{ id: 3, name: 'Charlie', age: 40 },
]);
const [columnDefs] = useState([
{ field: 'id', editable: false },
{ field: 'name', editable: true },
{
field: 'age',
editable: true,
type: 'numericColumn', // Helps with alignment and filtering
cellEditor: 'agNumberCellEditor',
cellEditorParams: {
min: 0, // Visual hint in editor, but validator is for strict enforcement
max: 150,
validator: (params) => {
const value = parseInt(params.newValue, 10);
if (isNaN(value) || value <= 0 || value > 120) {
alert('Age must be a positive number between 1 and 120!');
return false; // Invalid input, keep editor open
}
return true; // Valid input
},
},
},
]);
const defaultColDef = {
flex: 1,
minWidth: 100,
resizable: true,
};
return (
<div className="ag-theme-alpine" style={{ height: 400, width: 600 }}>
<AgGridReact
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
>
</AgGridReact>
</div>
);
};
export default MyGridComponent;
Pros:
- Simple to implement for basic, immediate checks.
- Keeps the editor open, forcing the user to correct the input.
Cons:
- Only works with built-in editors or custom editors that respect the
validatorparameter. - Can't prevent the value from being *stored* if the editor is eventually closed with an invalid value (though typically this is combined with other hooks).
- The alert box can be disruptive; a better UX would be inline error messages within a custom cell editor.
2. Robust Validation with colDef.valueSetter
The valueSetter function is executed *after* the cell editor attempts to provide a new value, but *before* that value is officially set on the underlying row data. This is a crucial point for validation because:
- You can inspect the new value and the current row data.
- You can choose to return
true(value should be set) orfalse(value should NOT be set). - You can perform side effects (e.g., show a toast message, update other data).
Let's enhance our age validation to be more robust and prevent invalid ages from being set at all, along with a custom message:
import React, { useState, useRef, useCallback } from 'react';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
const MyGridComponent = () => {
const gridRef = useRef();
const [rowData, setRowData] = useState([
{ id: 1, name: 'Alice', age: 30, status: 'Active' },
{ id: 2, name: 'Bob', age: 24, status: 'Active' },
{ id: 3, name: 'Charlie', age: 40, status: 'Inactive' },
]);
const [columnDefs] = useState([
{ field: 'id', editable: false },
{ field: 'name', editable: true },
{
field: 'age',
editable: true,
type: 'numericColumn',
cellEditor: 'agNumberCellEditor',
valueSetter: (params) => {
const newValue = parseInt(params.newValue, 10);
const oldValue = params.oldValue;
if (isNaN(newValue) || newValue <= 0 || newValue > 120) {
console.error('Validation failed: Age must be a number between 1 and 120.');
// You would typically show a toast or a more user-friendly error here
alert('Invalid Age! Please enter a number between 1 and 120.');
return false; // Prevent the value from being set
}
// Optionally, you can also perform cross-field validation here
// if (params.data.status === 'Inactive' && newValue > 60) {
// alert('Inactive users cannot be over 60 years old.');
// return false;
// }
// If validation passes, return true to allow the value to be set
params.data.age = newValue; // Manually set the value if you want to modify it, or AG-Grid will do it.
// It's generally better to let AG-Grid set it unless you're transforming.
return true;
},
},
{ field: 'status', editable: true,
cellEditor: 'agSelectCellEditor',
cellEditorParams: {
values: ['Active', 'Inactive'],
},
},
]);
const defaultColDef = {
flex: 1,
minWidth: 100,
resizable: true,
};
const onGridReady = useCallback((params) => {
// Example of accessing row data after changes
params.api.onFilterChanged();
}, []);
return (
<div className="ag-theme-alpine" style={{ height: 400, width: 800 }}>
<AgGridReact
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
onGridReady={onGridReady}
>
</AgGridReact>
</div>
);
};
export default MyGridComponent;
Pros:
- Prevents invalid data from ever touching your row's state.
- Allows for complex validation logic, including cross-field checks.
- Can be used to perform data transformations before setting.
Cons:
- Doesn't keep the editor open; the editor closes, but the value reverts to its original state.
- Requires more explicit feedback mechanisms (e.g., toast, console error).
3. Reacting to Changes with colDef.onCellValueChanged
The onCellValueChanged callback fires *after* a value has been successfully changed and committed to the row's data. This hook is not primarily for preventing a value change, but rather for reacting to it.
Use cases for onCellValueChanged:
- Triggering dependent calculations (e.g., if quantity changes, update total price).
- Initiating server-side updates or asynchronous validation (e.g., checking uniqueness against a database).
- Updating the UI elsewhere based on the change.
- Secondary, non-blocking validation, where you might simply highlight the cell as invalid rather than preventing the change.
// ... (MyGridComponent setup similar to above) ...
const [columnDefs] = useState([
// ... other columns ...
{
field: 'price',
editable: true,
type: 'numericColumn',
onCellValueChanged: (params) => {
const newValue = parseFloat(params.newValue);
const oldValue = parseFloat(params.oldValue);
if (newValue < 0) {
console.warn(`Price for ${params.data.name} cannot be negative. Value set to ${newValue}.`);
// Here, you might show a toast, or even revert the value manually if strict validation
// wasn't performed by valueSetter. However, it's generally better to use valueSetter for prevention.
// For demonstrating, we'll just log and let it pass if no valueSetter was used.
// Example: Trigger an API call
// updateProductPrice(params.data.id, newValue).then(response => {
// console.log('Price updated on server:', response);
// }).catch(error => {
// console.error('Failed to update price on server:', error);
// // Optionally, revert the cell value on client if server update fails
// params.node.setDataValue('price', oldValue);
// gridRef.current.api.refreshCells({ rowNodes: [params.node], columns: ['price'] });
// });
} else {
console.log(`Price for ${params.data.name} changed from ${oldValue} to ${newValue}.`);
}
},
},
]);
// ... rest of MyGridComponent ...
Pros:
- Perfect for reactions and side effects.
- Ideal for initiating asynchronous processes.
Cons:
- Cannot prevent the value from being set.
- Requires manual handling if you need to revert a value based on asynchronous validation failure.
4. Visual Feedback: Highlighting Invalid Cells with cellClassRules
Beyond preventing invalid data, it's crucial to provide clear visual feedback to the user. AG-Grid's cellClassRules are perfect for this, allowing you to dynamically apply CSS classes based on cell values or other row data properties.
First, define your CSS:
/* In your CSS file (e.g., index.css or App.css) */
.ag-theme-alpine .ag-cell.ag-cell-invalid {
background-color: #ffcccc; /* Light red background */
border: 1px solid #ff0000; /* Red border */
color: #cc0000; /* Dark red text */
}
/* Optional: style for tooltip if you're using one for error messages */
.ag-theme-alpine .ag-tooltip-custom-error {
background-color: #fdd;
color: #a00;
border: 1px solid #f00;
padding: 5px;
border-radius: 3px;
box-shadow: 2px 2px 5px rgba(0,0,0,0.2);
}
Then, modify your colDef:
import React, { useState, useRef, useCallback } from 'react';
import { AgGridReact } from 'ag-grid-react';
import 'ag-grid-community/styles/ag-grid.css';
import 'ag-grid-community/styles/ag-theme-alpine.css';
// Ensure your CSS is imported
// import './App.css'; // Or wherever your custom CSS is
const MyGridComponent = () => {
const gridRef = useRef();
const [rowData, setRowData] = useState([
{ id: 1, name: 'Alice', age: 30, score: 85 },
{ id: 2, name: 'Bob', age: 24, score: 92 },
{ id: 3, name: 'Charlie', age: 40, score: 60 },
]);
const [columnDefs] = useState([
{ field: 'id', editable: false },
{ field: 'name', editable: true },
{
field: 'age',
editable: true,
type: 'numericColumn',
valueSetter: (params) => {
const newValue = parseInt(params.newValue, 10);
if (isNaN(newValue) || newValue <= 0 || newValue > 120) {
params.data.ageValid = false; // Mark row data as invalid
console.error('Invalid Age: Must be between 1 and 120');
// alert('Invalid Age! Please enter a number between 1 and 120.'); // Optional: Keep existing alert for immediate feedback
return false; // Prevent value from being set
}
params.data.age = newValue;
params.data.ageValid = true; // Mark as valid
return true;
},
cellClassRules: {
'ag-cell-invalid': (params) => !params.data.ageValid, // Apply class if ageValid is false
},
tooltipValueGetter: (params) => { // Optional: Add a tooltip for specific error message
return !params.data.ageValid ? 'Age must be between 1 and 120' : null;
}
},
{
field: 'score',
editable: true,
type: 'numericColumn',
valueSetter: (params) => {
const newValue = parseInt(params.newValue, 10);
if (isNaN(newValue) || newValue < 0 || newValue > 100) {
// Here, instead of preventing, we let it through but mark it for styling
params.data.score = newValue; // Set the value
params.data.scoreValid = false; // Mark as invalid
return true; // Allow the value to be set, but it will be styled
}
params.data.score = newValue;
params.data.scoreValid = true;
return true;
},
cellClassRules: {
'ag-cell-invalid': (params) => !params.data.scoreValid,
},
tooltipValueGetter: (params) => {
return !params.data.scoreValid ? 'Score must be between 0 and 100' : null;
}
},
]);
const defaultColDef = {
flex: 1,
minWidth: 100,
resizable: true,
// Ensure cellClassRules are re-evaluated
// (This happens automatically if `setDataValue` or `setRowData` is used)
};
// This callback is crucial for AG-Grid to detect changes that trigger cellClassRules
const onCellValueChanged = useCallback((event) => {
// Force refresh cells to re-evaluate cellClassRules based on the updated rowData
// (Only needed if the rule depends on data properties *other* than the one just edited,
// or if using valueSetter that modifies rowData directly without `setDataValue`)
event.api.refreshCells({
rowNodes: [event.node],
columns: [event.column.getColId()],
force: true, // Force refresh even if AG-Grid thinks nothing changed
});
}, []);
return (
<div className="ag-theme-alpine" style={{ height: 400, width: 800 }}>
<AgGridReact
ref={gridRef}
rowData={rowData}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
onCellValueChanged={onCellValueChanged}
>
</AgGridReact>
</div>
);
};
export default MyGridComponent;
In this example, for the 'Age' column, we use valueSetter to prevent invalid values and mark a ageValid flag on the row data. For the 'Score' column, we *allow* the value to be set, but still mark a scoreValid flag, demonstrating how you might accept "soft" invalid entries while still highlighting them. The onCellValueChanged ensures the styling is re-evaluated.
Best Practices for AG-Grid Validation
- Layered Validation: Combine client-side (
cellEditorParams.validator,valueSetter) with server-side validation. Client-side provides immediate UX; server-side is the ultimate gatekeeper. - Clear User Feedback: Don't just block input silently. Use alerts (sparingly), toast notifications, inline error messages (with custom cell editors), and visual cues (
cellClassRules) to guide the user. - Meaningful Error Messages: "Invalid input" is unhelpful. "Age must be between 1 and 120" is much better.
- Validation State Management: For complex grids, consider storing validation messages or valid flags directly within your row data, or in a separate validation state object, to drive
cellClassRulesand custom tooltips. - Asynchronous Validation: If your validation involves server calls (e.g., checking for unique usernames), consider using
onCellValueChangedto trigger the check and then update the cell's validity status/style once the result returns. During the async check, you might display a "checking..." indicator. - Accessibility: Ensure your error messages and visual cues are accessible to users with disabilities (e.g., using ARIA attributes for screen readers if building custom components).
Conclusion
Implementing robust validation in AG-Grid cell editing is a multi-faceted task, but AG-Grid provides all the necessary hooks to achieve a highly interactive and reliable user experience. By strategically using cellEditorParams.validator for immediate feedback, valueSetter for preventing invalid data, and cellClassRules for visual indicators, you can ensure data integrity while maintaining a smooth editing workflow for your users. Remember to prioritize clear communication of errors and combine client-side efficiency with server-side ultimate truth.