A Cell Editor Component is the UI that appears, normally inside the Cell, that takes care of the Edit operation. You can select from the Provided Cell Editors or create your own Custom Cell Editor Components.
The example below shows some Provided Editor Components and some Custom Editor Components.
'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 GenderRenderer from "./genderRenderer";
import MoodEditor from "./moodEditor";
import MoodRenderer from "./moodRenderer";
import SimpleTextEditor from "./simpleTextEditor";
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: "first_name", headerName: "Provided Text" },
{
field: "last_name",
headerName: "Custom Text",
cellEditor: SimpleTextEditor,
},
{
field: "age",
headerName: "Provided Number",
cellEditor: "agNumberCellEditor",
},
{
field: "gender",
headerName: "Provided Rich Select",
cellRenderer: GenderRenderer,
cellEditor: "agRichSelectCellEditor",
cellEditorParams: {
cellRenderer: GenderRenderer,
values: ["Male", "Female"],
},
},
{
field: "mood",
headerName: "Custom Mood",
cellRenderer: MoodRenderer,
cellEditor: MoodEditor,
cellEditorPopup: true,
},
]);
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>,
);
.mood-renderer {
width: 100%;
height: 100%;
display: flex;
align-items: center;
}
.mood {
border-radius: 15px;
border: 1px solid grey;
background-color: #e6e6e6;
padding: 15px;
text-align: center;
display: inline-block;
outline: none;
}
.default {
border: 1px solid transparent !important;
padding: 4px;
}
.selected {
border: 1px solid lightgreen !important;
padding: 4px;
}
.numeric-input {
box-sizing: border-box;
padding-left: var(--ag-spacing);
width: 100%;
height: 100%;
}
.my-simple-editor {
box-sizing: border-box;
padding-left: var(--ag-spacing);
width: 100%;
height: 100%;
}
export function getData(): any[] {
const cloneObject = (obj: any) => JSON.parse(JSON.stringify(obj));
const students = [
{
first_name: "Bob",
last_name: "Harrison",
age: 15,
gender: "Male",
mood: "Happy",
},
{
first_name: "Mary",
last_name: "Wilson",
gender: "Female",
age: 11,
mood: "Sad",
},
{
first_name: "Zahid",
last_name: "Khan",
gender: "Male",
age: 12,
mood: "Happy",
},
{
first_name: "Jerry",
last_name: "Mane",
gender: "Male",
age: 12,
mood: "Happy",
},
];
// double the array twice, make more data!
students.forEach((item) => {
students.push(cloneObject(item));
});
students.forEach((item) => {
students.push(cloneObject(item));
});
students.forEach((item) => {
students.push(cloneObject(item));
});
return students;
}
import { RectangleHorizontal } from "lucide-react";
import React from "react";
import type { CustomCellRendererProps } from "ag-grid-react";
export default (props: CustomCellRendererProps) => {
const icon = props.value === "Male" ? "fa-male" : "fa-female";
return props.value ? (
<span>
<i className={`fa ${icon}`}></i> {props.value}
</span>
) : (
<React.Fragment></React.Fragment>
);
};
import React, { memo, useEffect, useRef, useState } from "react";
import type { CustomCellEditorProps } from "ag-grid-react";
export default memo(
({ value, onValueChange, stopEditing }: CustomCellEditorProps) => {
const isHappy = (value: string) => value === "Happy";
const [ready, setReady] = useState(false);
const refContainer = useRef<HTMLDivElement>(null);
const checkAndToggleMoodIfLeftRight = (event: any) => {
if (ready) {
if (["ArrowLeft", "ArrowRight"].indexOf(event.key) > -1) {
// left and right
const isLeft = event.key === "ArrowLeft";
onValueChange(isLeft ? "Happy" : "Sad");
event.stopPropagation();
}
}
};
useEffect(() => {
refContainer.current?.focus();
setReady(true);
}, []);
useEffect(() => {
window.addEventListener("keydown", checkAndToggleMoodIfLeftRight);
return () => {
window.removeEventListener("keydown", checkAndToggleMoodIfLeftRight);
};
}, [checkAndToggleMoodIfLeftRight, ready]);
const onClick = (happy: boolean) => {
onValueChange(happy ? "Happy" : "Sad");
stopEditing();
};
const mood = {
borderRadius: 15,
border: "1px solid grey",
backgroundColor: "#e6e6e6",
padding: 15,
textAlign: "center" as const,
display: "inline-block",
};
const unselected = {
paddingLeft: 10,
paddingRight: 10,
border: "1px solid transparent",
padding: 4,
};
const selected = {
paddingLeft: 10,
paddingRight: 10,
border: "1px solid lightgreen",
padding: 4,
};
const happyStyle = isHappy(value) ? selected : unselected;
const sadStyle = !isHappy(value) ? selected : unselected;
return (
<div
ref={refContainer}
style={mood}
tabIndex={1} // important - without this the key presses wont be caught
>
<img
src="https://www.ag-grid.com/example-assets/smileys/happy.png"
onClick={() => onClick(true)}
style={happyStyle}
/>
<img
src="https://www.ag-grid.com/example-assets/smileys/sad.png"
onClick={() => onClick(false)}
style={sadStyle}
/>
</div>
);
},
);
import React, { useMemo } from "react";
import type { CustomCellRendererProps } from "ag-grid-react";
export default (props: CustomCellRendererProps) => {
const imageForMood = (mood: string) =>
"https://www.ag-grid.com/example-assets/smileys/" +
(mood === "Happy" ? "happy.png" : "sad.png");
const mood = useMemo(() => imageForMood(props.value), [props.value]);
return (
<div className="mood-renderer">
<img width="20px" src={mood} />
</div>
);
};
import React, { useEffect, useRef } from "react";
import type { ICellEditor } from "ag-grid-community";
import type { CustomCellEditorProps } from "ag-grid-react";
export interface MySimpleInterface extends ICellEditor {
myCustomFunction(): { rowIndex: number; colId: string };
}
export default (
{ value, onValueChange, eventKey, rowIndex, column }: CustomCellEditorProps,
ref,
) => {
const updateValue = (val: string) => {
onValueChange(val === "" ? null : val);
};
useEffect(() => {
let startValue;
if (eventKey === "Backspace") {
startValue = "";
} else if (eventKey && eventKey.length === 1) {
startValue = eventKey;
} else {
startValue = value;
}
if (startValue == null) {
startValue = "";
}
updateValue(startValue);
refInput.current?.focus();
}, []);
const refInput = useRef<HTMLInputElement>(null);
return (
<input
value={value || ""}
ref={refInput}
onChange={(event) => updateValue(event.target.value)}
className="my-simple-editor"
/>
);
};
The provided editors' input fields share the grid-wide input behaviour (clear button on supported inputs, browser autocomplete) described in Input Fields.
Custom Components Copy Link
Custom Cell Editor Components are Controlled Components, which receive a value as part of the props, and pass value updates back to the grid via the onValueChange callback. The value is not set until editing stops.
The provided props follow the CustomCellEditorProps interface which is listed below under API Reference.
export default ({ value, onValueChange }) => {
return (
<input
type="text"
value={value || ''}
onChange={({ target: { value }}) => onValueChange(value === '' ? null : value)}
/>
);
}In previous versions of the grid, custom components were declared in an imperative way. See Migrating to Use reactiveCustomComponents for details on how to migrate to the current format.
The following callbacks can be passed to the useGridCellEditor hook (CustomCellEditorCallbacks interface). All the callbacks are optional. The hook only needs to be used if callbacks are provided.
Optional: Gets called once after initialised. If you return true, the editor will not be used and the grid will continue editing. Use this to make a decision on editing inside the init() function, eg maybe you want to only start editing if the user hits a numeric key, but not a letter, if the editor is for numbers.
|
Optional: Gets called once after editing is complete. If your return true, then the new value will not be used. The editing will have no impact on the record. Use this if you do not want a new value from your gui, i.e. you want to cancel the editing.
|
Optional: If doing full line edit, then gets called when focus should be put into the editor
|
Optional: If doing full line edit, then gets called when focus is leaving the editor
|
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.
|
The custom numeric editor in the example below demonstrates the useGridCellEditor hook:
'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,
TextEditorModule,
TextFilterModule,
enableDevValidations,
} from "ag-grid-community";
import { AgGridProvider, AgGridReact } from "ag-grid-react";
import NumericEditor from "./numericEditor";
import "./styles.css";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
TextEditorModule,
TextFilterModule,
CustomEditorModule,
ClientSideRowModelModule,
];
const GridExample = () => {
const [rowData] = useState([
{ name: "Bob", mood: "Happy", number: 10 },
{ name: "Harry", mood: "Sad", number: 3 },
{ name: "Sally", mood: "Happy", number: 20 },
{ name: "Mary", mood: "Sad", number: 5 },
{ name: "John", mood: "Happy", number: 15 },
{ name: "Jack", mood: "Happy", number: 25 },
{ name: "Sue", mood: "Sad", number: 43 },
{ name: "Sean", mood: "Sad", number: 1335 },
{ name: "Niall", mood: "Happy", number: 2 },
{ name: "Alberto", mood: "Happy", number: 123 },
{ name: "Fred", mood: "Sad", number: 532 },
{ name: "Jenny", mood: "Happy", number: 34 },
{ name: "Larry", mood: "Happy", number: 13 },
]);
const columnDefs = useMemo<ColDef[]>(
() => [
{
headerName: "Provided Text",
field: "name",
width: 300,
},
{
headerName: "Custom Numeric",
field: "number",
cellEditor: NumericEditor,
editable: true,
width: 280,
},
],
[],
);
const defaultColDef = useMemo(
() => ({
editable: true,
flex: 1,
minWidth: 100,
filter: true,
}),
[],
);
return (
<AgGridProvider modules={modules}>
<div style={{ width: "100%", height: "100%" }}>
<div
style={{
height: "100%",
width: "100%",
}}
>
<AgGridReact
columnDefs={columnDefs}
rowData={rowData}
defaultColDef={defaultColDef}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
.mood {
border-radius: 15px;
border: 1px solid grey;
background-color: #e6e6e6;
padding: 15px;
text-align: center;
display: inline-block;
outline: none;
}
.default {
border: 1px solid transparent !important;
padding: 4px;
}
.selected {
border: 1px solid lightgreen !important;
padding: 4px;
}
.doubling-input,
.numeric-input {
font-size: calc(var(--ag-font-size) + 1px);
padding-left: calc(var(--ag-cell-horizontal-padding) - 1px);
box-sizing: border-box;
width: 100%;
height: 100%;
}
/* Completely fill the space for number arrow buttons */
.doubling-input::-webkit-outer-spin-button,
.doubling-input::-webkit-inner-spin-button {
width: 25px;
position: absolute;
top: 0px;
right: 1px;
height: 100%;
}
import React, { memo, useCallback, useEffect, useRef } from "react";
import type { CustomCellEditorProps } from "ag-grid-react";
import { useGridCellEditor } from "ag-grid-react";
// backspace starts the editor on Windows
const KEY_BACKSPACE = "Backspace";
const KEY_F2 = "F2";
const KEY_ENTER = "Enter";
const KEY_TAB = "Tab";
export default memo(
({ value, onValueChange, eventKey, stopEditing }: CustomCellEditorProps) => {
const updateValue = (val: string) => {
onValueChange(val === "" ? null : parseInt(val));
};
useEffect(() => {
let startValue;
let highlightAllOnFocus = true;
if (eventKey === KEY_BACKSPACE) {
// if backspace or delete pressed, we clear the cell
startValue = "";
} else if (eventKey && eventKey.length === 1) {
// if a letter was pressed, we start with the letter
startValue = eventKey;
highlightAllOnFocus = false;
} else {
// otherwise we start with the current value
startValue = value;
if (eventKey === KEY_F2) {
highlightAllOnFocus = false;
}
}
if (startValue == null) {
startValue = "";
}
updateValue(startValue);
// get ref from React component
const eInput = refInput.current!;
eInput.focus();
if (highlightAllOnFocus) {
eInput.select();
} else {
// when we started editing, we want the caret at the end, not the start.
// this comes into play in two scenarios:
// a) when user hits F2
// b) when user hits a printable character
const length = eInput.value ? eInput.value.length : 0;
if (length > 0) {
eInput.setSelectionRange(length, length);
}
}
}, []);
const refInput = useRef<HTMLInputElement>(null);
const isLeftOrRight = (event: any) => {
return ["ArrowLeft", "ArrowLeft"].indexOf(event.key) > -1;
};
const isCharNumeric = (charStr: string) => {
return !!/^\d+$/.test(charStr);
};
const isNumericKey = (event: any) => {
const charStr = event.key;
return isCharNumeric(charStr);
};
const isBackspace = (event: any) => {
return event.key === KEY_BACKSPACE;
};
const finishedEditingPressed = (event: any) => {
const key = event.key;
return key === KEY_ENTER || key === KEY_TAB;
};
const onKeyDown = (event: any) => {
if (isLeftOrRight(event) || isBackspace(event)) {
event.stopPropagation();
return;
}
if (!finishedEditingPressed(event) && !isNumericKey(event)) {
if (event.preventDefault) event.preventDefault();
}
if (finishedEditingPressed(event)) {
stopEditing();
}
};
// Gets called once before editing starts, to give editor a chance to
// cancel the editing before it even starts.
const isCancelBeforeStart = useCallback(() => {
return (
!!eventKey &&
eventKey.length === 1 &&
"1234567890".indexOf(eventKey) < 0
);
}, [eventKey]);
// Gets called once when editing is finished (eg if Enter is pressed).
// If you return true, then the result of the edit will be ignored.
const isCancelAfterEnd = useCallback(() => {
// will reject the number if it greater than 1,000,000
// not very practical, but demonstrates the method.
return value != null && value > 1000000;
}, [value]);
useGridCellEditor({
isCancelBeforeStart,
isCancelAfterEnd,
});
return (
<input
ref={refInput}
value={value == null ? "" : value}
onChange={(event: any) => updateValue(event.target.value)}
onKeyDown={(event: any) => onKeyDown(event)}
className="numeric-input"
/>
);
},
);
Selecting Components Copy Link
Cell Editor Components are configured using the cellEditor property of the Column Definition.
Provide your own cell editor component for this column's cells. |
const [columnDefs, setColumnDefs] = useState([
{
field: 'name',
editable: true,
// uses a provided editor, referenced by name
cellEditor: 'agTextCellEditor'
},
{
field: 'name',
editable: true,
// uses a custom editor, referenced directly
cellEditor: 'CustomEditorComp'
},
]);
<AgGridReact columnDefs={columnDefs} />See Registering Custom Components for details on how to register your custom grid components.
Dynamic Selection Copy Link
The colDef.cellEditorSelector function allows setting different Editor Components for different Rows within a Column.
Callback to select which cell editor to be used for a given row within the same column. |
The params passed to cellEditorSelector are the same as those passed to the Editor Component. Typically the selector will use this to check the row's contents and choose an editor accordingly.
The result is an object with component and params to use instead of cellEditor and cellEditorParams.
This following shows the Selector always returning back the provided Rich Select Editor:
cellEditorSelector: params => {
return {
component: 'agRichSelectCellEditor',
params: { values: ['Male', 'Female'] }
};
}However a selector only makes sense when a selection is made. The following demonstrates selecting between Cell Editors:
cellEditorSelector: params => {
if (params.data.type === 'age') {
return {
component: NumericCellEditor,
}
}
if (params.data.type === 'gender') {
return {
component: 'agRichSelectCellEditor',
params: {
values: ['Male', 'Female']
}
}
}
if (params.data.type === 'mood') {
return {
component: MoodEditor,
popup: true,
popupPosition: 'under'
}
}
return undefined
}Here is a full example:
- The column 'Value' holds data of different types as shown in the column 'Type' (numbers/genders/moods).
colDef.cellEditorSelectoris a function that returns the name of the component to use to edit based on the type of data for that row- Edit a cell by double clicking to observe the different editors used.
'use client';
import React, { StrictMode, useCallback, useMemo, useState } from "react";
import { createRoot } from "react-dom/client";
import type {
CellEditingStartedEvent,
CellEditingStoppedEvent,
CellEditorSelectorResult,
ColDef,
ICellEditorParams,
RowEditingStartedEvent,
RowEditingStoppedEvent,
} from "ag-grid-community";
import {
ClientSideRowModelModule,
NumberEditorModule,
TextEditorModule,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
RichSelectModule,
} from "ag-grid-enterprise";
import { AgGridProvider, AgGridReact } from "ag-grid-react";
import type { IRow } from "./data";
import { getData } from "./data";
import MoodEditor from "./moodEditor";
import NumericCellEditor from "./numericCellEditor";
import "./styles.css";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
NumberEditorModule,
TextEditorModule,
ClientSideRowModelModule,
ColumnMenuModule,
ContextMenuModule,
ColumnsToolPanelModule,
RichSelectModule,
];
const cellEditorSelector: (
params: ICellEditorParams<IRow>,
) => CellEditorSelectorResult | undefined = (
params: ICellEditorParams<IRow>,
) => {
if (params.data.type === "age") {
return {
component: NumericCellEditor,
};
}
if (params.data.type === "gender") {
return {
component: "agRichSelectCellEditor",
params: {
values: ["Male", "Female"],
},
};
}
if (params.data.type === "mood") {
return {
component: MoodEditor,
popup: true,
popupPosition: "under",
};
}
return undefined;
};
const GridExample = () => {
const containerStyle = useMemo(() => ({ width: "100%", height: "100%" }), []);
const gridStyle = useMemo(() => ({ height: "100%", width: "100%" }), []);
const [rowData, setRowData] = useState<IRow[]>(getData());
const [columnDefs, setColumnDefs] = useState<ColDef[]>([
{ field: "type" },
{
field: "value",
editable: true,
cellEditorSelector: cellEditorSelector,
},
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
cellDataType: false,
};
}, []);
const onRowEditingStarted = useCallback((event: RowEditingStartedEvent) => {
console.log("never called - not doing row editing");
}, []);
const onRowEditingStopped = useCallback((event: RowEditingStoppedEvent) => {
console.log("never called - not doing row editing");
}, []);
const onCellEditingStarted = useCallback((event: CellEditingStartedEvent) => {
console.log("cellEditingStarted");
}, []);
const onCellEditingStopped = useCallback((event: CellEditingStoppedEvent) => {
console.log("cellEditingStopped");
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact<IRow>
rowData={rowData}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
onRowEditingStarted={onRowEditingStarted}
onRowEditingStopped={onRowEditingStopped}
onCellEditingStarted={onCellEditingStarted}
onCellEditingStopped={onCellEditingStopped}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
.mood {
border-radius: 15px;
border: 1px solid grey;
background-color: #e6e6e6;
padding: 15px;
text-align: center;
display: inline-block;
outline: none;
}
.default {
border: 1px solid transparent !important;
padding: 4px;
}
.selected {
border: 1px solid lightgreen !important;
padding: 4px;
}
.simple-input-editor {
box-sizing: border-box;
padding-left: var(--ag-spacing);
width: 100%;
height: 100%;
}
export interface IRow {
value: string | number;
type: "age" | "gender" | "mood";
}
export function getData(): IRow[] {
return [
{ value: 14, type: "age" },
{ value: "Female", type: "gender" },
{ value: "Happy", type: "mood" },
{ value: 21, type: "age" },
{ value: "Male", type: "gender" },
{ value: "Sad", type: "mood" },
];
}
import React, { memo, useEffect, useRef, useState } from "react";
import type { CustomCellEditorProps } from "ag-grid-react";
export default memo(
({ value, onValueChange, stopEditing }: CustomCellEditorProps) => {
const isHappy = (value: string) => value === "Happy";
const [ready, setReady] = useState(false);
const refContainer = useRef<HTMLDivElement>(null);
const checkAndToggleMoodIfLeftRight = (event: any) => {
if (ready) {
if (["ArrowLeft", "ArrowRight"].indexOf(event.key) > -1) {
// left and right
const isLeft = event.key === "ArrowLeft";
onValueChange(isLeft ? "Happy" : "Sad");
event.stopPropagation();
}
}
};
useEffect(() => {
refContainer.current?.focus();
setReady(true);
}, []);
useEffect(() => {
window.addEventListener("keydown", checkAndToggleMoodIfLeftRight);
return () => {
window.removeEventListener("keydown", checkAndToggleMoodIfLeftRight);
};
}, [checkAndToggleMoodIfLeftRight, ready]);
const onClick = (happy: boolean) => {
onValueChange(happy ? "Happy" : "Sad");
stopEditing();
};
const happyClass = isHappy(value) ? "selected" : "default";
const sadClass = !isHappy(value) ? "selected" : "default";
return (
<div
ref={refContainer}
className="mood"
tabIndex={1} // important - without this the key presses wont be caught
>
<img
src="https://www.ag-grid.com/example-assets/smileys/happy.png"
onClick={() => onClick(true)}
className={happyClass}
/>
<img
src="https://www.ag-grid.com/example-assets/smileys/sad.png"
onClick={() => onClick(false)}
className={sadClass}
/>
</div>
);
},
);
import React, { memo, useCallback, useEffect, useRef } from "react";
import type { CustomCellEditorProps } from "ag-grid-react";
import { useGridCellEditor } from "ag-grid-react";
// backspace starts the editor on Windows
const KEY_BACKSPACE = "Backspace";
const KEY_F2 = "F2";
const KEY_ENTER = "Enter";
const KEY_TAB = "Tab";
const KEY_ARROW_LEFT = "ArrowLeft";
const KEY_ARROW_RIGHT = "ArrowRight";
export default memo(
({ value, onValueChange, eventKey, stopEditing }: CustomCellEditorProps) => {
const updateValue = (val: string) => {
onValueChange(val === "" ? null : parseInt(val));
};
useEffect(() => {
let startValue;
let highlightAllOnFocus = true;
if (eventKey === KEY_BACKSPACE) {
// if backspace or delete pressed, we clear the cell
startValue = "";
} else if (eventKey && eventKey.length === 1) {
// if a letter was pressed, we start with the letter
startValue = eventKey;
highlightAllOnFocus = false;
} else {
// otherwise we start with the current value
startValue = value;
if (eventKey === KEY_F2) {
highlightAllOnFocus = false;
}
}
if (startValue == null) {
startValue = "";
}
updateValue(startValue);
// get ref from React component
const eInput = refInput.current!;
eInput.focus();
if (highlightAllOnFocus) {
eInput.select();
} else {
// when we started editing, we want the caret at the end, not the start.
// this comes into play in two scenarios:
// a) when user hits F2
// b) when user hits a printable character
const length = eInput.value ? eInput.value.length : 0;
if (length > 0) {
eInput.setSelectionRange(length, length);
}
}
}, []);
const refInput = useRef<HTMLInputElement>(null);
const isLeftOrRight = (event: any) => {
return [KEY_ARROW_LEFT, KEY_ARROW_RIGHT].indexOf(event.key) > -1;
};
const isCharNumeric = (charStr: string) => {
return !!/^\d+$/.test(charStr);
};
const isNumericKey = (event: any) => {
const charStr = event.key;
return isCharNumeric(charStr);
};
const isBackspace = (event: any) => {
return event.key === KEY_BACKSPACE;
};
const finishedEditingPressed = (event: any) => {
const key = event.key;
return key === KEY_ENTER || key === KEY_TAB;
};
const onKeyDown = (event: any) => {
if (isLeftOrRight(event) || isBackspace(event)) {
event.stopPropagation();
return;
}
if (!finishedEditingPressed(event) && !isNumericKey(event)) {
if (event.preventDefault) event.preventDefault();
}
if (finishedEditingPressed(event)) {
stopEditing();
}
};
// Gets called once before editing starts, to give editor a chance to
// cancel the editing before it even starts.
const isCancelBeforeStart = useCallback(() => {
return (
!!eventKey &&
eventKey.length === 1 &&
"1234567890".indexOf(eventKey) < 0
);
}, [eventKey]);
// Gets called once when editing is finished (eg if Enter is pressed).
// If you return true, then the result of the edit will be ignored.
const isCancelAfterEnd = useCallback(() => {
// will reject the number if it greater than 1,000,000
// not very practical, but demonstrates the method.
return value != null && value > 1000000;
}, [value]);
useGridCellEditor({
isCancelBeforeStart,
isCancelAfterEnd,
});
return (
<input
ref={refInput}
value={value}
onChange={(event: any) => updateValue(event.target.value)}
onKeyDown={(event: any) => onKeyDown(event)}
className="simple-input-editor"
/>
);
},
);
Custom Props Copy Link
The property colDef.cellEditorParams allows custom props to be passed to editors.
Params to be passed to the cellEditor component. |
colDef = {
cellEditor: MyCellEditor,
cellEditorParams: {
// make "country" value available to cell editor
country: 'Ireland'
},
// ...other props
} Dynamic Props Copy Link
The colDef.cellEditorParams function allows dynamic props independently of the Editor selection. For example you might have a 'City' column that has values based on the 'Country' column.
cellEditorParams: params => {
const selectedCountry = params.data.country;
if (selectedCountry === 'Ireland') {
return {
values: ['Dublin', 'Cork', 'Galway']
};
} else {
return {
values: ['New York', 'Los Angeles', 'Chicago', 'Houston']
};
}
}Below shows an example with dynamic props. The following can be noted:
- Column Gender uses a Cell Component for both the grid and the editor.
- Column Country allows country selection, with
cellHeightbeing used to make each entry 50px tall. If the currently selected city for the row doesn't match a newly selected country, the city cell is cleared. - Column City uses dynamic parameters to display values for the selected country, and uses
formatValueto add the selected city's country as a suffix. - Column Address uses the large text area editor.
"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 {
CellValueChangedEvent,
ClientSideRowModelModule,
ColDef,
ColGroupDef,
GridApi,
GridOptions,
ICellEditorParams,
LargeTextEditorModule,
ModuleRegistry,
TextEditorModule,
enableDevValidations,
} from "ag-grid-community";
import {
ColumnMenuModule,
ColumnsToolPanelModule,
ContextMenuModule,
RichSelectModule,
} from "ag-grid-enterprise";
import { IRow, getData } from "./data";
import GenderCellRenderer from "./genderCellRenderer.tsx";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
ClientSideRowModelModule,
ColumnsToolPanelModule,
ColumnMenuModule,
ContextMenuModule,
RichSelectModule,
TextEditorModule,
LargeTextEditorModule,
];
const cellCellEditorParams = (params: ICellEditorParams<IRow>) => {
const selectedCountry = params.data.country;
const allowedCities = countyToCityMap(selectedCountry);
return {
values: allowedCities,
formatValue: (value: any) => `${value} (${selectedCountry})`,
};
};
const countyToCityMap: (match: string) => string[] = (match: string) => {
const map: {
[key: string]: string[];
} = {
Ireland: ["Dublin", "Cork", "Galway"],
USA: ["New York", "Los Angeles", "Chicago", "Houston"],
};
return map[match];
};
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: "gender",
cellRenderer: GenderCellRenderer,
cellEditor: "agRichSelectCellEditor",
cellEditorParams: {
values: ["Male", "Female"],
cellRenderer: GenderCellRenderer,
},
},
{
field: "country",
cellEditor: "agRichSelectCellEditor",
cellEditorParams: {
cellHeight: 50,
values: ["Ireland", "USA"],
},
},
{
field: "city",
cellEditor: "agRichSelectCellEditor",
cellEditorParams: cellCellEditorParams,
},
{
field: "address",
cellEditor: "agLargeTextCellEditor",
cellEditorPopup: true,
minWidth: 550,
},
]);
const defaultColDef = useMemo<ColDef>(() => {
return {
flex: 1,
minWidth: 130,
editable: true,
};
}, []);
const onCellValueChanged = useCallback((params: CellValueChangedEvent) => {
const colId = params.column.getId();
if (colId === "country") {
const selectedCountry = params.data.country;
const selectedCity = params.data.city;
const allowedCities = countyToCityMap(selectedCountry) || [];
const cityMismatch = allowedCities.indexOf(selectedCity) < 0;
if (cityMismatch) {
params.node.setDataValue("city", null);
}
}
}, []);
return (
<AgGridProvider modules={modules}>
<div style={containerStyle}>
<div style={gridStyle}>
<AgGridReact
rowData={rowData}
columnDefs={columnDefs}
defaultColDef={defaultColDef}
onCellValueChanged={onCellValueChanged}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
export interface IRow {
name: string;
gender: string;
age?: number;
address: string;
city: string;
country: string;
}
export function getData(): IRow[] {
return [
{
name: 'Bob Harrison',
gender: 'Male',
address: '1197 Thunder Wagon Common, Cataract, RI, 02987-1016, US, (401) 747-0763',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Mary Wilson',
gender: 'Female',
age: 11,
address: '3685 Rocky Glade, Showtucket, NU, X1E-9I0, CA, (867) 371-4215',
city: 'New York',
country: 'USA',
},
{
name: 'Zahid Khan',
gender: 'Male',
age: 12,
address: '3235 High Forest, Glen Campbell, MS, 39035-6845, US, (601) 638-8186',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Jerry Mane',
gender: 'Male',
age: 12,
address: '2234 Sleepy Pony Mall , Drain, DC, 20078-4243, US, (202) 948-3634',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Bob Harrison',
gender: 'Male',
address: '1197 Thunder Wagon Common, Cataract, RI, 02987-1016, US, (401) 747-0763',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Mary Wilson',
gender: 'Female',
age: 11,
address: '3685 Rocky Glade, Showtucket, NU, X1E-9I0, CA, (867) 371-4215',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Zahid Khan',
gender: 'Male',
age: 12,
address: '3235 High Forest, Glen Campbell, MS, 39035-6845, US, (601) 638-8186',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Jerry Mane',
gender: 'Male',
age: 12,
address: '2234 Sleepy Pony Mall , Drain, DC, 20078-4243, US, (202) 948-3634',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Bob Harrison',
gender: 'Male',
address: '1197 Thunder Wagon Common, Cataract, RI, 02987-1016, US, (401) 747-0763',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Mary Wilson',
gender: 'Female',
age: 11,
address: '3685 Rocky Glade, Showtucket, NU, X1E-9I0, CA, (867) 371-4215',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Zahid Khan',
gender: 'Male',
age: 12,
address: '3235 High Forest, Glen Campbell, MS, 39035-6845, US, (601) 638-8186',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Jerry Mane',
gender: 'Male',
age: 12,
address: '2234 Sleepy Pony Mall , Drain, DC, 20078-4243, US, (202) 948-3634',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Bob Harrison',
gender: 'Male',
address: '1197 Thunder Wagon Common, Cataract, RI, 02987-1016, US, (401) 747-0763',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Mary Wilson',
gender: 'Female',
age: 11,
address: '3685 Rocky Glade, Showtucket, NU, X1E-9I0, CA, (867) 371-4215',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Zahid Khan',
gender: 'Male',
age: 12,
address: '3235 High Forest, Glen Campbell, MS, 39035-6845, US, (601) 638-8186',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Jerry Mane',
gender: 'Male',
age: 12,
address: '2234 Sleepy Pony Mall , Drain, DC, 20078-4243, US, (202) 948-3634',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Bob Harrison',
gender: 'Male',
address: '1197 Thunder Wagon Common, Cataract, RI, 02987-1016, US, (401) 747-0763',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Mary Wilson',
gender: 'Female',
age: 11,
address: '3685 Rocky Glade, Showtucket, NU, X1E-9I0, CA, (867) 371-4215',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Zahid Khan',
gender: 'Male',
age: 12,
address: '3235 High Forest, Glen Campbell, MS, 39035-6845, US, (601) 638-8186',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Jerry Mane',
gender: 'Male',
age: 12,
address: '2234 Sleepy Pony Mall , Drain, DC, 20078-4243, US, (202) 948-3634',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Bob Harrison',
gender: 'Male',
address: '1197 Thunder Wagon Common, Cataract, RI, 02987-1016, US, (401) 747-0763',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Mary Wilson',
gender: 'Female',
age: 11,
address: '3685 Rocky Glade, Showtucket, NU, X1E-9I0, CA, (867) 371-4215',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Zahid Khan',
gender: 'Male',
age: 12,
address: '3235 High Forest, Glen Campbell, MS, 39035-6845, US, (601) 638-8186',
city: 'Dublin',
country: 'Ireland',
},
{
name: 'Jerry Mane',
gender: 'Male',
age: 12,
address: '2234 Sleepy Pony Mall , Drain, DC, 20078-4243, US, (202) 948-3634',
city: 'Dublin',
country: 'Ireland',
},
];
}
import React from 'react';
import type { CustomCellRendererProps } from 'ag-grid-react';
export default (props: CustomCellRendererProps) => {
const icon = props.value === 'Male' ? 'fa-male' : 'fa-female';
return props.value ? (
<span>
<i className={`fa ${icon}`}></i> {props.value}
</span>
) : (
<React.Fragment></React.Fragment>
);
};
Popup Editor Copy Link
An editor can be Inline or Popup.
An Inline Editor Component will be placed inside the Grid's Cell, replacing the Cell contents when active.
A Popup Editor Component appears in a popup over the Cell. Popup Editors are not constrained to the Cells dimensions.
Configure that an Editor is in a popup by setting cellEditorPopup=true on the Column Definition.
colDefs = [
{
cellEditor: MyPopupEditor,
cellEditorPopup: true
// ...
}
]Popup Editors appear over the editing Cell. Configure the Popup Editor to appear below the Cell by setting cellEditorPopupPosition='under' on the Column Definition.
colDef = {
cellEditorPopup: true,
cellEditorPopupPosition: 'under',
// ...other props
}The following example demonstrates the same editor positioned inline, as a popup over the cell, and as a popup under the cell:
'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 MoodEditor from "./moodEditor";
import MoodRenderer from "./moodRenderer";
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: "mood",
headerName: "Inline",
cellRenderer: MoodRenderer,
cellEditor: MoodEditor,
},
{
field: "mood",
headerName: "Popup Over",
cellRenderer: MoodRenderer,
cellEditor: MoodEditor,
cellEditorPopup: true,
},
{
field: "mood",
headerName: "Popup Under",
cellRenderer: MoodRenderer,
cellEditor: MoodEditor,
cellEditorPopup: true,
cellEditorPopupPosition: "under",
},
]);
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>,
);
.mood-renderer {
width: 100%;
height: 100%;
display: flex;
align-items: center;
}
.mood {
border-radius: 5px;
border: 1px solid grey;
background-color: #e6e6e6;
padding: 2px;
height: 33px;
text-align: center;
display: inline-block;
outline: none;
}
.default {
width: 22px;
border: 1px solid transparent !important;
padding: 4px;
}
.selected {
width: 22px;
border: 1px solid blue !important;
padding: 4px;
}
export function getData(): any[] {
const moods = [
{
mood: "Happy",
},
{
mood: "Sad",
},
{
mood: "Happy",
},
{
mood: "Happy",
},
];
return [...moods, ...moods];
}
import React, { memo, useEffect, useRef, useState } from "react";
import type { CustomCellEditorProps } from "ag-grid-react";
export default memo(
({ value, onValueChange, stopEditing }: CustomCellEditorProps) => {
const isHappy = (value: string) => value === "Happy";
const [ready, setReady] = useState(false);
const refContainer = useRef<HTMLDivElement>(null);
const checkAndToggleMoodIfLeftRight = (event: any) => {
if (ready) {
if (["ArrowLeft", "ArrowRight"].indexOf(event.key) > -1) {
// left and right
const isLeft = event.key === "ArrowLeft";
onValueChange(isLeft ? "Happy" : "Sad");
event.stopPropagation();
}
}
};
useEffect(() => {
refContainer.current?.focus();
setReady(true);
}, []);
useEffect(() => {
window.addEventListener("keydown", checkAndToggleMoodIfLeftRight);
return () => {
window.removeEventListener("keydown", checkAndToggleMoodIfLeftRight);
};
}, [checkAndToggleMoodIfLeftRight, ready]);
const onClick = (happy: boolean) => {
onValueChange(happy ? "Happy" : "Sad");
stopEditing();
};
const happyClass = isHappy(value) ? "selected" : "default";
const sadClass = !isHappy(value) ? "selected" : "default";
return (
<div
ref={refContainer}
className={"mood"}
tabIndex={0} // important - without this the key presses wont be caught
>
<img
src="https://www.ag-grid.com/example-assets/smileys/happy.png"
onClick={() => onClick(true)}
className={happyClass}
/>
<img
src="https://www.ag-grid.com/example-assets/smileys/sad.png"
onClick={() => onClick(false)}
className={sadClass}
/>
</div>
);
},
);
import React, { useMemo } from "react";
import type { CustomCellRendererProps } from "ag-grid-react";
export default (props: CustomCellRendererProps) => {
const imageForMood = (mood: string) =>
"https://www.ag-grid.com/example-assets/smileys/" +
(mood === "Happy" ? "happy.png" : "sad.png");
const mood = useMemo(() => imageForMood(props.value), [props.value]);
return (
<div className="mood-renderer">
<img width="20px" src={mood} />
</div>
);
};
If a custom cell editor creates its own popup that is anchored outside of the editor component (e.g. like a third-party date picker), then the popup element needs to have the 'ag-custom-component-popup' CSS class. This allows the grid to determine correctly when to stop editing.
Keyboard Navigation Copy Link
In Custom Editors, you may wish to disable some of the Grids keyboard navigation. For example, if you are providing a simple text editor, you may wish the grid to do nothing when you press the right and left arrows (the default is the grid will move to the next / previous cell) as you may want the right and left arrows to move the cursor inside your editor. In other cell editors, you may wish the grid to behave as normal.
Because different cell editors will have different requirements on what the grid does, it is up to the cell editor to decide which event it wants the grid to handle and which it does not.
You have two options to stop the grid from doing it's default action on certain key events:
- Stop propagation of the event to the grid in the cell editor.
- Tell the grid to do nothing via the
colDef.suppressKeyboardEvent()callback.
Option 1 - Stop Propagation Copy Link
If you don't want the grid to act on an event, call event.stopPropagation(). The advantage of this method is that your cell editor takes care of everything, this is good for creating reusable cell editors.
The following code snippet is one you could include for a simple text editor, which would stop the grid from doing navigation.
const KEY_LEFT = 'ArrowLeft';
const KEY_UP = 'ArrowUp';
const KEY_RIGHT = 'ArrowRight';
const KEY_DOWN = 'ArrowDown';
const KEY_PAGE_UP = 'PageUp';
const KEY_PAGE_DOWN = 'PageDown';
const KEY_PAGE_HOME = 'Home';
const KEY_PAGE_END = 'End';
const MyCellEditor = ({ value, onValueChange }) => {
const onKeyDown = (event) => {
const key = event.key;
const isNavigationKey = key === KEY_LEFT ||
key === KEY_RIGHT ||
key === KEY_UP ||
key === KEY_DOWN ||
key === KEY_PAGE_DOWN ||
key === KEY_PAGE_UP ||
key === KEY_PAGE_HOME ||
key === KEY_PAGE_END;
if (isNavigationKey) {
// this stops the grid from receiving the event and executing keyboard navigation
event.stopPropagation();
}
}
return (
<input
value={value || ''}
onChange={({ target: { value: newValue }) => onValueChange(newValue)}
onKeyDownCapture={onKeyDown}
/>
);
}); Option 2 - Suppress Keyboard Event Copy Link
If you implement colDef.suppressKeyboardEvent(), you can tell the grid which events you want to process and which not. The advantage of this method of the previous method is it takes the responsibility out of the cell editor and into the column definition. So if you are using a reusable, or third party, cell editor, and the editor doesn't have this logic in it, you can add the logic via configuration.
Allows the user to suppress certain keyboard events in the grid cell. |
const KEY_UP = 'ArrowUp';
const KEY_DOWN = 'ArrowDown';
const GridExample = () => {
// rest of the component
const columnDefs = [
{
field: 'value',
suppressKeyboardEvent: params => {
console.log('cell is editing: ' + params.editing);
console.log('keyboard event:', params.event);
// return true (to suppress) if editing and user hit up/down keys
const key = params.event.key;
const gridShouldDoNothing = params.editing && (key === KEY_UP || key === KEY_DOWN);
return gridShouldDoNothing;
}
}
];
return (
<div
style={{
height: '100%',
width: '100%'
}}
className="test-grid">
<AgGridReact columnDefs={columnDefs} {/* ...rest of the definition... */} />
</div>
);
}; Accessing Instances Copy Link
After the grid has created an instance of an Editor Component for a Cell it is possible to access that instance. This is useful if you want to call a method that you provide on the Editor that has nothing to do with the operation of the grid. Accessing Editors is done using the grid API getCellEditorInstances(params).
Returns the list of active cell editor instances. Optionally provide parameters to restrict to certain columns / row nodes. |
If you are doing normal editing, then only one cell is editable at any given time. For this reason if you call getCellEditorInstances() with no params, it will return back the editing cell's editor if a cell is editing, or an empty list if no cell is editing.
An example of calling getCellEditorInstances() is as follows:
const instances = api.getCellEditorInstances(params);
if (instances.length > 0) {
getInstance(instances[0], instance => {
...
});
}The example below shows using getCellEditorInstances. The following can be noted:
- All cells are editable.
- First Name and Last Name use the default editor.
- All other columns use the provided
MySimpleCellEditoreditor. - The example sets an interval to print information from the active cell editor. There are three results: 1) No editing 2) Editing with default cell renderer and 3) editing with the custom cell editor. All results are printed to the developer console.
'use client';
import React, {
StrictMode,
useCallback,
useMemo,
useRef,
useState,
} from "react";
import { createRoot } from "react-dom/client";
import type { ColDef, GridReadyEvent, ICellEditor } from "ag-grid-community";
import {
ClientSideRowModelModule,
CustomEditorModule,
NumberEditorModule,
TextEditorModule,
TextFilterModule,
enableDevValidations,
} from "ag-grid-community";
import { AgGridProvider, AgGridReact, getInstance } from "ag-grid-react";
import type { MySimpleInterface } from "./mySimpleEditor";
import MySimpleEditor from "./mySimpleEditor";
import "./style.css";
if (process.env.NODE_ENV !== "production") {
// Enable extended validations only for development
enableDevValidations();
}
const modules = [
NumberEditorModule,
TextEditorModule,
TextFilterModule,
CustomEditorModule,
ClientSideRowModelModule,
];
const createRowData = () => {
const cloneObject = (obj: any) => JSON.parse(JSON.stringify(obj));
const students = [
{
first_name: "Bob",
last_name: "Harrison",
gender: "Male",
address:
"1197 Thunder Wagon Common, Cataract, RI, 02987-1016, US, (401) 747-0763",
mood: "Happy",
country: "Ireland",
},
{
first_name: "Mary",
last_name: "Wilson",
gender: "Female",
age: 11,
address: "3685 Rocky Glade, Showtucket, NU, X1E-9I0, CA, (867) 371-4215",
mood: "Sad",
country: "Ireland",
},
{
first_name: "Zahid",
last_name: "Khan",
gender: "Male",
age: 12,
address:
"3235 High Forest, Glen Campbell, MS, 39035-6845, US, (601) 638-8186",
mood: "Happy",
country: "Ireland",
},
{
first_name: "Jerry",
last_name: "Mane",
gender: "Male",
age: 12,
address:
"2234 Sleepy Pony Mall , Drain, DC, 20078-4243, US, (202) 948-3634",
mood: "Happy",
country: "Ireland",
},
];
students.forEach((item) => {
students.push(cloneObject(item));
});
students.forEach((item) => {
students.push(cloneObject(item));
});
students.forEach((item) => {
students.push(cloneObject(item));
});
return students;
};
const GridExample = () => {
const gridRef = useRef<AgGridReact>(null);
const [rowData] = useState<any[]>(createRowData());
const columnDefs = useMemo<ColDef[]>(
() => [
{
field: "first_name",
headerName: "First Name",
width: 120,
editable: true,
},
{
field: "last_name",
headerName: "Last Name",
width: 120,
editable: true,
},
{
field: "gender",
width: 100,
cellEditor: MySimpleEditor,
},
{
field: "age",
width: 80,
cellEditor: MySimpleEditor,
},
{
field: "mood",
width: 90,
cellEditor: MySimpleEditor,
},
{
field: "country",
width: 110,
cellEditor: MySimpleEditor,
},
{
field: "address",
minWidth: 502,
cellEditor: MySimpleEditor,
},
],
[],
);
const onGridReady = useCallback((params: GridReadyEvent) => {
if (gridRef.current) {
const interval = window.setInterval(() => {
const instances = params.api.getCellEditorInstances();
if (instances.length > 0) {
getInstance<ICellEditor, MySimpleInterface>(
instances[0],
(instance) => {
if (instance && instance.myCustomFunction) {
const result = instance.myCustomFunction();
console.log(
`found editing cell: row index = ${result.rowIndex}, column = ${result.colId}.`,
);
} else {
console.log(
"found editing cell, but method myCustomFunction not found, must be the default editor.",
);
}
},
);
} else {
console.log("found not editing cell.");
}
}, 1000);
return () => clearInterval(interval);
}
}, []);
const defaultColDef = useMemo(
() => ({
editable: true,
flex: 1,
minWidth: 100,
filter: true,
}),
[],
);
return (
<AgGridProvider modules={modules}>
<div style={{ width: "100%", height: "100%" }}>
<div
style={{
height: "100%",
width: "100%",
}}
>
<AgGridReact
ref={gridRef}
defaultColDef={defaultColDef}
rowData={rowData}
columnDefs={columnDefs}
onGridReady={onGridReady}
/>
</div>
</div>
</AgGridProvider>
);
};
const root = createRoot(document.getElementById("root")!);
root.render(
<StrictMode>
<GridExample />
</StrictMode>,
);
.my-simple-editor {
box-sizing: border-box;
padding-left: var(--ag-spacing);
width: 100%;
height: 100%;
}
import React, {
forwardRef,
useEffect,
useImperativeHandle,
useRef,
} from "react";
import type { ICellEditor } from "ag-grid-community";
import type { CustomCellEditorProps } from "ag-grid-react";
// backspace starts the editor on Windows
const KEY_BACKSPACE = "Backspace";
export interface MySimpleInterface extends ICellEditor {
myCustomFunction(): { rowIndex: number; colId: string };
}
export default forwardRef(
(
{ value, onValueChange, eventKey, rowIndex, column }: CustomCellEditorProps,
ref,
) => {
const updateValue = (val: string) => {
onValueChange(val === "" ? null : val);
};
useEffect(() => {
let startValue;
if (eventKey === KEY_BACKSPACE) {
startValue = "";
} else if (eventKey && eventKey.length === 1) {
startValue = eventKey;
} else {
startValue = value;
}
if (startValue == null) {
startValue = "";
}
updateValue(startValue);
refInput.current?.focus();
}, []);
const refInput = useRef<HTMLInputElement>(null);
useImperativeHandle(ref, () => {
return {
myCustomFunction() {
return {
rowIndex: rowIndex,
colId: column.getId(),
};
},
};
});
return (
<input
value={value || ""}
ref={refInput}
onChange={(event) => updateValue(event.target.value)}
className="my-simple-editor"
/>
);
},
);
API Reference Copy Link
CustomCellEditorProps Copy Link
Properties available on the CustomCellEditorProps<TData = any, TValue = any, TContext = any> interface.
The value in the cell when editing started. May be null or undefined — for example on group rows or when the field is absent from the row data; the component must handle this. |
The current value for the editor. May be null or undefined if the editor has been cleared; the component must handle this. |
Callback that should be called every time the value in the editor changes. |
Utility function to parse a value using the column's colDef.valueParser |
Utility function to format a value using the column's colDef.valueFormatter. The value argument may be null or undefined; callers should handle this. |
Key value of key that started the edit, eg 'Enter' or 'F2' - non-printable characters appear here |
Grid column |
Column definition |
Row node for the cell |
Row data |
Editing row index |
If doing full row edit, this is true if the cell is the one that started the edit (eg it is the cell the use double clicked on, or pressed a key on etc). |
callback to tell grid a key was pressed - useful to pass control key events (tab, arrows etc) back to grid - however you do |
Callback to tell grid to stop editing the current cell. Call with input parameter true to prevent focus from moving to the next cell after editing stops in case the grid property enterNavigatesVerticallyAfterEdit=true. Pass the originating keydown event when committing from a key press so that enterNavigatesVerticallyAfterEdit can move focus in the correct direction. |
A reference to the DOM element representing the grid cell that your component will live inside. Useful if you want to add event listeners or classes at this level. This is the DOM element that gets browser focus when selecting cells. |
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.
|
Runs the Editor Validation.
|
The grid api. |
Application context as set on gridOptions.context. |