Standard Validation Copy Link
The Grid provides built-in validation for all Provided Cell Editors, such as the Text, Large Text, Number and Date editors. These editors support validation automatically by checking the constraints defined in the column configuration. For example:
TextandLarge Texteditors will respect themaxLengthproperty.Numbereditors validate against min and max constraints.Dateeditors ensure the value is a valid date string.
Provided editors validate as their value changes and run a final validation when editing ends. The Grid handles invalid values based on the selected Validation Modes.
"use client";
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
ClientSideRowModelApiModule,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
DateEditorModule,
GridApi,
GridOptions,
GridReadyEvent,
ModuleRegistry,
NumberEditorModule,
TextEditorModule,
ValueFormatterParams,
enableDevValidations,
} from "ag-grid-community";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
NumberEditorModule,
TextEditorModule,
DateEditorModule,
ClientSideRowModelApiModule,
ClientSideRowModelModule,
];
interface IModifiedOlympicData extends IOlympicData {
dateObj: Date | null;
}
const stringToDate = (date: string): Date | null => {
const [day, month, year] = (date || "").split("/");
if (day == null || month == null || year == null) {
return null;
}
return new Date(Number(year), Number(month) - 1, Number(day));
};
const dateToIso = (date: string | null): string => {
const [day, month, year] = (date || "").split("/");
if (day == null || month == null || year == null) {
return "";
}
return `${year}-${month}-${day}`;
};
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [rowData, setRowData] = useState<IModifiedOlympicData[]>();
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{
field: "athlete",
headerName: "Athlete (maxLength 10)",
cellEditor: "agTextCellEditor",
cellEditorParams: {
maxLength: 10,
},
},
{
field: "age",
headerName: "Age (>= 0 and <= 100)",
cellEditor: "agNumberCellEditor",
cellEditorParams: {
min: 0,
max: 100,
},
},
{
field: "dateObj",
headerName: "Date (< 2009)",
cellEditor: "agDateCellEditor",
valueFormatter: (params: ValueFormatterParams<any, Date>) => {
if (!params.value) {
return "";
}
const month = params.value.getMonth() + 1;
const day = params.value.getDate();
return `${params.value.getFullYear()}-${month < 10 ? "0" + month : month}-${day < 10 ? "0" + day : day}`;
},
cellEditorParams: {
max: new Date("2008-12-31"),
},
},
{
field: "date",
headerName: "Date as String (> 2008)",
cellEditor: "agDateStringCellEditor",
cellEditorParams: {
min: "2008-12-31",
},
},
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
editable: true,
cellDataType: false,
};
}, []);
const onGridReady = useCallback((params: GridReadyEvent) => {
fetch("https://www.ag-grid.com/example-assets/olympic-winners.json")
.then((resp) => resp.json())
.then((data: IModifiedOlympicData[]) =>
setRowData(
data.map((rec: IOlympicData) => ({
...rec,
date: dateToIso(rec.date),
dateObj: stringToDate(rec.date),
})),
),
);
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact<IModifiedOlympicData>
rowData={rowData}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
onGridReady={onGridReady}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
Overriding Validation Copy Link
To customise validation in a Provided Editor, use the getValidationErrors() callback inside ICellEditorParams. The callback receives the editor's internalErrors, and its return value replaces the Provided Editor's validation result. Include internalErrors in the returned array if the built-in constraints should still apply alongside your custom rules.
Properties available on the ICellEditorParams<TData = any, TValue = any, TContext = any> interface.
Optional validation callback that will override the getValidationErrors() of Provided Editors. Use this to return your own custom errors.
Returns: An array of non-empty, user-facing error messages, or null if the editor is valid.
|
const [columnDefs, setColumnDefs] = useState([
{
field: 'athlete',
cellEditorParams: {
getValidationErrors: (params) => {
const { value, internalErrors } = params;
const errors = [...(internalErrors ?? [])];
if (!value || value.length < 3) {
errors.push('The value has to be at least 3 characters long.');
}
return errors.length ? errors : null;
},
},
},
]);
<AgGridReact columnDefs={columnDefs} />If the callback returns errors, the Grid will show the errors in a tooltip when hovering the editor and discard the edit value before completing (depending on the Validation Modes).
This is demonstrated in the following example, note that:
Athletehas to be at least3characters.Agehas to be different than18.
"use client";
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import {
ClientSideRowModelApiModule,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
GridApi,
GridOptions,
IErrorValidationParams,
ModuleRegistry,
NumberEditorModule,
TextEditorModule,
enableDevValidations,
} from "ag-grid-community";
import { IOlympicData } from "./interfaces";
import { useFetchJson } from "./useFetchJson";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
NumberEditorModule,
TextEditorModule,
ClientSideRowModelApiModule,
ClientSideRowModelModule,
];
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{
field: "athlete",
cellEditorParams: {
getValidationErrors: (params: IErrorValidationParams) => {
const { value } = params;
if (!value || value.length < 3) {
return ["The value has to be at least 3 characters long."];
}
return null;
},
},
},
{
field: "age",
cellEditorParams: {
getValidationErrors: (params: IErrorValidationParams) => {
const { value } = params;
if (value != null && value == 18) {
return ["Value has to be different than 18"];
}
return null;
},
},
},
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
editable: true,
};
}, []);
const { data, loading } = useFetchJson<IOlympicData>(
"https://www.ag-grid.com/example-assets/olympic-winners.json",
);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact<IOlympicData>
rowData={data}
loading={loading}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} import { useState, useEffect } from 'react';
/**
* Fetch example Json data
* Not recommended for production use!
*/
export const useFetchJson = <T,>(url:string, limit?: number) => {
const [data, setData] = useState<T[]>();
const [loading, setLoading] = useState(true);
useEffect(() => {
// StrictMode runs this effect twice: drop the superseded run's response rather than applying both.
let cancelled = false;
const fetchData = async () => {
setLoading(true);
// Note error handling is omitted here for brevity
const response = await fetch(url);
const json = await response.json();
const data = limit ? json.slice(0, limit) : json;
if (cancelled) {
return;
}
setData(data);
setLoading(false);
};
fetchData();
return () => {
cancelled = true;
};
}, [url, limit]);
return { data, loading };
}; Validation Modes Copy Link
The Grid supports two modes for handling invalid edits, configured via the grid option invalidEditValueMode:
| Mode | Description |
|---|---|
'revert' (default) | Cancels the edit and reverts the cell to its original value if the value is invalid. |
'block' | Keeps the invalid editing session open until a valid value is provided or the edit is cancelled. Full Row Editing still allows navigation between editors in the same row. |
Use the 'block' mode when you want to strictly enforce valid input before allowing the user to proceed.
const invalidEditValueMode = 'block';
<AgGridReact invalidEditValueMode={invalidEditValueMode} />"use client";
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./styles.css";
import {
ClientSideRowModelApiModule,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
EditValidationCommitType,
GridApi,
GridOptions,
ModuleRegistry,
NumberEditorModule,
TextEditorModule,
enableDevValidations,
} from "ag-grid-community";
import { IOlympicData } from "./interfaces";
import { useFetchJson } from "./useFetchJson";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
NumberEditorModule,
TextEditorModule,
ClientSideRowModelApiModule,
ClientSideRowModelModule,
];
const GridExample = () => {
const gridRef = useRef<AgGridReact<IOlympicData>>(null);
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{
field: "athlete",
headerName: "Athlete (maxLength 10)",
cellEditor: "agTextCellEditor",
cellEditorParams: {
maxLength: 10,
},
},
{
field: "age",
headerName: "Age (>= 0 and <=100)",
cellEditor: "agNumberCellEditor",
cellEditorParams: {
min: 0,
max: 100,
},
},
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
editable: true,
};
}, []);
const { data, loading } = useFetchJson<IOlympicData>(
"https://www.ag-grid.com/example-assets/olympic-winners.json",
);
const onValidationModeSelect = useCallback(() => {
const value: "revert" | "block" = document.querySelector<HTMLSelectElement>(
"#select-validation-mode",
)?.value as EditValidationCommitType;
gridRef.current!.api.setGridOption("invalidEditValueMode", value);
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div className="example-wrapper">
<div className="example-header">
<label>
<span>Cell Editor Validation Mode: </span>
<select
id="select-validation-mode"
onChange={onValidationModeSelect}
>
<option value="revert">revert</option>
<option value="block">block</option>
</select>
</label>
</div>
<div style={gridStyle}>
<AgGridReact<IOlympicData>
ref={gridRef}
rowData={data}
loading={loading}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
invalidEditValueMode={"revert"}
/>
</div>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
.example-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
.example-header {
margin-bottom: 10px;
}
#myGrid {
flex: 1 1 0px;
width: 100%;
}
export interface IOlympicData {
athlete: string,
age: number,
country: string,
year: number,
date: string,
sport: string,
gold: number,
silver: number,
bronze: number,
total: number
} import { useState, useEffect } from 'react';
/**
* Fetch example Json data
* Not recommended for production use!
*/
export const useFetchJson = <T,>(url:string, limit?: number) => {
const [data, setData] = useState<T[]>();
const [loading, setLoading] = useState(true);
useEffect(() => {
// StrictMode runs this effect twice: drop the superseded run's response rather than applying both.
let cancelled = false;
const fetchData = async () => {
setLoading(true);
// Note error handling is omitted here for brevity
const response = await fetch(url);
const json = await response.json();
const data = limit ? json.slice(0, limit) : json;
if (cancelled) {
return;
}
setData(data);
setLoading(false);
};
fetchData();
return () => {
cancelled = true;
};
}, [url, limit]);
return { data, loading };
}; Full Row Editing Validation Copy Link
When using Full Row Editing, the Grid will validate each cell editor in the row individually, using the same mechanisms described in the previous sections.
In addition, the Grid can also perform cross-field validation by using the optional callback getFullRowEditValidationErrors(params). This allows you to implement logic that checks relationships between fields — for example, ensuring that one field is greater than another.
This callback should return an array of non-empty, user-facing error strings if the row is in an invalid state. If no errors are found, it should return null.
The row data is not updated until the edit is committed. Use editorsState to validate the proposed row values; each entry contains the column ID together with its old and new values.
Validates the Full Row Edit. Return non-empty, user-facing error messages, or null when the row is valid.
Only relevant when editType="fullRow". |
const getFullRowEditValidationErrors = ({ editorsState }) => {
const values = Object.fromEntries(
editorsState.map(({ colId, newValue }) => [colId, newValue]),
);
const min = Number(values.min);
const max = Number(values.max);
if (min > max) {
return ['Min cannot be greater than Max'];
}
return null;
};
<AgGridReact getFullRowEditValidationErrors={getFullRowEditValidationErrors} />A row edit will only complete successfully if both the individual cell editors and the full-row validation return no errors.
Accessibility Copy Link
When a cell or full-row validation error is added, or its wording changes, the Grid announces the relevant error details to screen reader users. Revalidating unchanged errors, for example when navigating between editors in the same row, does not repeat the announcement.
If invalidEditValueMode is set to 'block' and prevents the user from completing a full-row edit, the Grid announces a summary of the current errors even if they were announced previously. Cell editor errors are identified by their column name. Errors returned by getFullRowEditValidationErrors apply to the row as a whole, so each error message should identify the fields or condition that the user needs to correct.
The full-row announcement text can be customised through the ariaFullRowValidationError and ariaFullRowEditValidationFailed localisation keys. In a blocked-completion summary, ariaRowIndex identifies errors that belong to another edited row and distinguishes errors from multiple rows.
This is demonstrated in the following example. Note the following validation rules:
Weighthas to be between0and500, inclusive.Heighthas to be between0and300, inclusive.- Full Row Edit Validation ensures that the Body Mass Index (BMI), calculated using height and weight, is between
10and80.
"use client";
import React, {
useCallback,
useMemo,
useRef,
useState,
StrictMode,
} from "react";
import { createRoot } from "react-dom/client";
import { AgGridReact, AgGridProvider } from "ag-grid-react";
import "./styles.css";
import {
ClientSideRowModelModule,
ColDef,
ColGroupDef,
EditStrategyType,
EditValidationCommitType,
GetFullRowEditValidationErrors,
GridApi,
GridOptions,
ModuleRegistry,
NumberEditorModule,
SelectEditorModule,
TextEditorModule,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
} from "ag-grid-enterprise";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
ClientSideRowModelModule,
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
SelectEditorModule,
TextEditorModule,
NumberEditorModule,
];
function getRowData() {
const rowData = [
{ name: "Alice", weight: 68, height: 165 },
{ name: "Bob", weight: 85, height: 178 },
{ name: "Charlie", weight: 72, height: 172 },
{ name: "Diana", weight: 54, height: 160 },
{ name: "Ethan", weight: 90, height: 182 },
{ name: "Fiona", weight: 63, height: 168 },
{ name: "George", weight: 77, height: 175 },
{ name: "Hannah", weight: 59, height: 162 },
{ name: "Ian", weight: 95, height: 185 },
{ name: "Julia", weight: 70, height: 170 },
];
return rowData;
}
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [rowData, setRowData] = useState<any[]>(getRowData());
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{
field: "name",
},
{
field: "weight",
headerName: "Weight (kg)",
cellDataType: "number",
cellEditorParams: {
min: 0,
max: 500,
},
},
{
field: "height",
headerName: "Height (cm)",
cellDataType: "number",
cellEditorParams: {
min: 0,
max: 300,
},
},
{
headerName: "BMI",
cellDataType: "number",
valueGetter: (params) => {
const { weight, height } = params.data ?? {};
if (!weight || !height) return null;
const heightM = height / 100;
return weight / (heightM * heightM);
},
valueFormatter: (params) => params.value?.toFixed(2),
editable: false,
},
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
editable: true,
cellDataType: false,
};
}, []);
const getFullRowEditValidationErrors = useCallback(({ editorsState }) => {
const values = Object.fromEntries(
editorsState.map(({ colId, newValue }) => [colId, newValue]),
);
const weight = parseFloat(values["weight"]);
const height = parseFloat(values["height"]);
const heightM = height / 100;
const bmi = weight / (heightM * heightM);
const errors: string[] = [];
if (!Number.isFinite(bmi)) {
errors.push(
"BMI cannot be calculated. Enter valid Weight and Height values.",
);
} else if (bmi < 10 || bmi > 80) {
errors.push(
`BMI is ${bmi.toFixed(2)}. It must be between 10 and 80. Check Weight and Height.`,
);
}
return errors.length ? errors : null;
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact
rowData={rowData}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
editType={"fullRow"}
invalidEditValueMode={"block"}
getFullRowEditValidationErrors={getFullRowEditValidationErrors}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
.example-wrapper {
display: flex;
flex-direction: column;
height: 100%;
}
#myGrid {
flex: 1 1 0px;
width: 100%;
}
Validation of Custom Editors Copy Link
Custom Cell Editors can participate in the Grid's validation system by optionally implementing the following methods:
Properties available on the ICellEditor<TValue = any> interface.
Optional: Returns the element to use for validation feedback. Called by the grid in two contexts: tooltip: true → used as the anchor for validation tooltips. tooltip: false → receives the invalid CSS class for visual feedback. tooltip - Whether the element is for a tooltip or direct styling.
Returns: An HTML element for feedback, or null/undefined to use default behavior.
|
Optional: The error messages associated with the Editor. Each error should be a non-empty, user-facing message.
|
These methods are called automatically before the Grid attempts to complete the edit. You can also manually trigger validation by calling the validate() method available in the cellEditorParams, for example:
cellEditorParams.validate();This is useful if you want to validate input during editing, such as in response to an onInput event in the Custom Phone Editor.
'use client';
import React, { StrictMode, useMemo, useState } from "react";
import { createRoot } from "react-dom/client";
import type { ColDef } from "ag-grid-community";
import {
ClientSideRowModelModule,
CustomEditorModule,
NumberEditorModule,
TextEditorModule,
enableDevValidations,
} from "ag-grid-community";
import { RichSelectModule } from "ag-grid-enterprise";
import { AgGridProvider, AgGridReact } from "ag-grid-react";
import { getData } from "./data";
import PhoneEditor from "./phoneEditor";
import "./styles.css";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
NumberEditorModule,
TextEditorModule,
CustomEditorModule,
ClientSideRowModelModule,
RichSelectModule,
];
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [rowData, setRowData] = useState<any[]>(getData());
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "name" },
{
field: "phone",
headerName: "Custom Phone Editor",
cellEditor: PhoneEditor,
},
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
editable: true,
flex: 1,
minWidth: 100,
};
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact
rowData={rowData}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
.phone-cell-editor {
width: 100%;
height: 100%;
box-sizing: border-box;
border: 1px solid transparent;
padding: 0.25rem 0.5rem;
}
.phone-cell-editor:focus {
outline: none;
}
.phone-cell-editor:focus:not(:invalid) {
border-color: blue;
}
export function getData(): any[] {
return [
{ name: "Alice Johnson", phone: "(415) 555-1234" },
{ name: "Brian Smith", phone: "(212) 555-9876" },
{ name: "Catherine Lee", phone: "(310) 555-4567" },
{ name: "Daniel Kim", phone: "(646) 555-7890" },
{ name: "Emily Davis", phone: "(408) 555-3210" },
{ name: "Franklin Moore", phone: "(702) 555-6543" },
{ name: "Grace Patel", phone: "(503) 555-8888" },
{ name: "Henry Clark", phone: "(214) 555-4321" },
{ name: "Isabella Torres", phone: "(617) 555-1122" },
{ name: "James O'Neil", phone: "(303) 555-3344" },
];
}
import React, { memo, useCallback, useEffect, useRef, useState } from "react";
import { useGridCellEditor } from "ag-grid-react";
import type { CustomCellEditorProps } from "ag-grid-react";
export default memo(
({
value,
onValueChange,
validate,
cellStartedEdit,
eventKey,
}: CustomCellEditorProps) => {
const inputRef = useRef<HTMLInputElement>(null);
const [internalValue, setInternalValue] = useState(value || "");
const phoneRegex = /^\(\d{3}\)\s\d{3}-\d{4}$/;
const getValidationErrors = useCallback(() => {
const trimmed = internalValue.trim();
return phoneRegex.test(trimmed)
? null
: ["Invalid phone format. Use (123) 456-7890"];
}, [internalValue]);
const getValidationElement = useCallback(() => {
return inputRef.current!;
}, []);
useGridCellEditor({
getValidationErrors,
getValidationElement,
});
useEffect(() => {
inputRef.current?.focus();
inputRef.current?.select();
if (cellStartedEdit && eventKey?.length === 1) {
setInternalValue(eventKey);
}
}, []);
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const val = e.target.value;
setInternalValue(val);
onValueChange(val);
setTimeout(() => {
validate?.(); // AG Grid will now call getValidationErrors using latest value
});
};
const onBlur = () => {
validate?.();
};
return (
<input
ref={inputRef}
type="text"
className="phone-cell-editor"
value={internalValue}
onChange={onChange}
onBlur={onBlur}
pattern="^\(\d{3}\)\s\d{3}-\d{4}$"
placeholder="(123) 456-7890"
/>
);
},
);