Skip to main content

Overview

AG Grid provides comprehensive cell editing capabilities with built-in editors for common data types and support for custom editor components. Editing can be configured at the column level and controlled through various modes and APIs.

Enabling Editing

Enable editing on columns using the editable property:
const columnDefs = [
  {
    field: 'athlete',
    editable: true  // Enable editing
  },
  {
    field: 'year',
    editable: false  // Disable editing (default)
  }
];

Edit Modes

Cell Editing (Default)

Edit one cell at a time:
const gridOptions = {
  // Cell editing is the default (no editType specified)
  columnDefs: [
    { field: 'athlete', editable: true },
    { field: 'age', editable: true }
  ]
};

Full Row Editing

Edit all cells in a row simultaneously:
import { EditStrategyType, FullRowEditValidationParams } from 'ag-grid-community';

const gridOptions = {
  editType: 'fullRow' as EditStrategyType,
  
  // Validate the entire row
  getFullRowEditValidationErrors: (params: FullRowEditValidationParams) => {
    const { data } = params;
    const errors: Record<string, string> = {};
    
    if (!data.athlete) {
      errors.athlete = 'Athlete name is required';
    }
    if (data.age < 0) {
      errors.age = 'Age must be positive';
    }
    
    return errors;
  }
};

Edit Triggers

Single Click Editing

const gridOptions = {
  singleClickEdit: true  // Start editing with single click
};

// Or per column
const columnDefs = [
  {
    field: 'athlete',
    singleClickEdit: true
  }
];

Suppress Click Edit

const gridOptions = {
  suppressClickEdit: true  // Require keyboard to start editing
};

Keyboard Triggers

const gridOptions = {
  enableCellEditingOnBackspace: true,  // MacOS: backspace starts editing
  suppressStartEditOnTab: false        // Tab navigates and starts editing
};

Built-in Editors

Text Editor

const columnDefs = [
  {
    field: 'athlete',
    cellEditor: 'agTextCellEditor',
    cellEditorParams: {
      maxLength: 100,
      useFormatter: true
    }
  }
];

Large Text Editor

const columnDefs = [
  {
    field: 'description',
    cellEditor: 'agLargeTextCellEditor',
    cellEditorPopup: true,
    cellEditorParams: {
      maxLength: 500,
      rows: 10,
      cols: 50
    }
  }
];

Number Editor

const columnDefs = [
  {
    field: 'age',
    cellEditor: 'agNumberCellEditor',
    cellEditorParams: {
      min: 0,
      max: 120,
      precision: 0,
      step: 1,
      showStepperButtons: true
    }
  }
];

Date Editor

const columnDefs = [
  {
    field: 'date',
    cellEditor: 'agDateCellEditor',
    cellEditorParams: {
      min: '2020-01-01',
      max: '2025-12-31'
    },
    dateComponent: 'CustomDatePicker'  // Optional custom date component
  }
];

Select Editor

const columnDefs = [
  {
    field: 'country',
    cellEditor: 'agSelectCellEditor',
    cellEditorParams: {
      values: ['USA', 'UK', 'France', 'Germany']
    }
  }
];

Checkbox Editor

const columnDefs = [
  {
    field: 'active',
    cellEditor: 'agCheckboxCellEditor'
  }
];

Custom Cell Editors

Basic Custom Editor

import { ICellEditor, ICellEditorParams } from 'ag-grid-community';

class CustomEditor implements ICellEditor {
  private eInput: HTMLInputElement;
  private value: any;
  
  init(params: ICellEditorParams): void {
    this.eInput = document.createElement('input');
    this.eInput.type = 'text';
    this.eInput.value = params.value || '';
    this.value = params.value;
    
    // Focus and select
    setTimeout(() => {
      this.eInput.focus();
      this.eInput.select();
    });
  }
  
  getGui(): HTMLElement {
    return this.eInput;
  }
  
  getValue(): any {
    return this.eInput.value;
  }
  
  destroy(): void {
    // Cleanup if needed
  }
  
  // Optional: Cancel editing if condition not met
  isCancelBeforeStart(): boolean {
    return false;
  }
  
  // Optional: Cancel after editing
  isCancelAfterEnd(): boolean {
    return false;
  }
}

const columnDefs = [
  {
    field: 'athlete',
    cellEditor: CustomEditor
  }
];
class PopupEditor implements ICellEditor {
  private eGui: HTMLDivElement;
  
  init(params: ICellEditorParams): void {
    this.eGui = document.createElement('div');
    this.eGui.innerHTML = `
      <div style="padding: 20px; background: white; border: 1px solid #ccc;">
        <input type="text" id="editor-input" />
        <button id="save-btn">Save</button>
      </div>
    `;
  }
  
  getGui(): HTMLElement {
    return this.eGui;
  }
  
  getValue(): any {
    const input = this.eGui.querySelector('#editor-input') as HTMLInputElement;
    return input.value;
  }
  
  // Editor appears in popup
  isPopup(): boolean {
    return true;
  }
  
  // Position popup under the cell
  getPopupPosition(): 'over' | 'under' {
    return 'under';
  }
}

const columnDefs = [
  {
    field: 'athlete',
    cellEditor: PopupEditor,
    cellEditorPopup: true,
    cellEditorPopupPosition: 'under'
  }
];

Editor with Validation

import { ICellEditorValidationError } from 'ag-grid-community';

class ValidatingEditor implements ICellEditor {
  private eInput: HTMLInputElement;
  private errors: string[] = [];
  
  init(params: ICellEditorParams): void {
    this.eInput = document.createElement('input');
    this.eInput.value = params.value || '';
    
    this.eInput.addEventListener('input', () => {
      this.validate();
    });
  }
  
  getGui(): HTMLElement {
    return this.eInput;
  }
  
  getValue(): any {
    return this.eInput.value;
  }
  
  validate(): void {
    this.errors = [];
    const value = this.eInput.value;
    
    if (!value) {
      this.errors.push('Value is required');
    }
    if (value.length < 3) {
      this.errors.push('Must be at least 3 characters');
    }
  }
  
  getValidationErrors(): string[] | null {
    return this.errors.length > 0 ? this.errors : null;
  }
  
  getValidationElement(tooltip: boolean): HTMLElement {
    return this.eInput;
  }
}

Value Handling

Value Getter and Setter

import { ValueGetterFunc, ValueSetterFunc } from 'ag-grid-community';

const valueGetter: ValueGetterFunc = (params) => {
  return params.data.firstName + ' ' + params.data.lastName;
};

const valueSetter: ValueSetterFunc = (params) => {
  const [firstName, lastName] = params.newValue.split(' ');
  params.data.firstName = firstName;
  params.data.lastName = lastName;
  return true;  // Return true if data changed
};

const columnDefs = [
  {
    headerName: 'Full Name',
    valueGetter: valueGetter,
    valueSetter: valueSetter,
    editable: true
  }
];

Value Parser

import { ValueParserFunc } from 'ag-grid-community';

const numberParser: ValueParserFunc = (params) => {
  return Number(params.newValue);
};

const columnDefs = [
  {
    field: 'age',
    valueParser: numberParser,
    editable: true
  }
];

Edit Validation

Invalid Edit Mode

import { EditValidationCommitType } from 'ag-grid-community';

const gridOptions = {
  invalidEditValueMode: 'block' as EditValidationCommitType  // Block invalid edits
};

Validate Edit API

import { validateEdit, ICellEditorValidationError } from 'ag-grid-community';

// Get validation errors for current edit
const errors: ICellEditorValidationError[] | null = validateEdit(beans);

if (errors && errors.length > 0) {
  console.log('Validation errors:', errors);
}

Edit API Methods

Start and Stop Editing

import { 
  startEditingCell, 
  stopEditing,
  isEditing,
  StartEditingCellParams 
} from 'ag-grid-community';

// Start editing a cell
startEditingCell(beans, {
  rowIndex: 0,
  colKey: 'athlete',
  key: 'Enter'  // Optional: simulate key press
} as StartEditingCellParams);

// Check if cell is being edited
const editing = isEditing(beans, { 
  rowIndex: 0, 
  column: api.getColumn('athlete')! 
});

// Stop editing
stopEditing(beans, false);  // false = save changes
stopEditing(beans, true);   // true = cancel changes

Get Editing Cells

import { getEditingCells, EditingCellPosition } from 'ag-grid-community';

// Get all cells currently being edited
const editingCells: EditingCellPosition[] = getEditingCells(beans);

editingCells.forEach(cell => {
  console.log('Editing:', {
    column: cell.colId,
    rowIndex: cell.rowIndex,
    oldValue: cell.oldValue,
    newValue: cell.newValue,
    state: cell.state  // 'editing' | 'changed'
  });
});

Get Edit Row Values

import { getEditRowValues } from 'ag-grid-community';

const rowNode = api.getRowNode('1');
const editValues = getEditRowValues(beans, rowNode);

console.log('Current edit values:', editValues);

Read-Only Edit Mode

Prevent grid from updating data automatically:
const gridOptions = {
  readOnlyEdit: true,  // Grid doesn't update data
  
  onCellEditRequest: (event) => {
    // Handle edit in external state management
    const { data, column, newValue } = event;
    
    // Update your external store
    updateExternalStore(data.id, column.getColId(), newValue);
  }
};

Undo/Redo

Enable undo/redo for cell edits:
import { 
  undoCellEditing, 
  redoCellEditing,
  getCurrentUndoSize,
  getCurrentRedoSize 
} from 'ag-grid-community';

const gridOptions = {
  undoRedoCellEditing: true,
  undoRedoCellEditingLimit: 10  // Max undo stack size
};

// Undo last edit
undoCellEditing(beans);

// Redo last undone edit
redoCellEditing(beans);

// Check stack sizes
const undoSize = getCurrentUndoSize(beans);
const redoSize = getCurrentRedoSize(beans);
const gridOptions = {
  enterNavigatesVertically: true,          // Enter moves down
  enterNavigatesVerticallyAfterEdit: true, // Enter moves down after edit
  stopEditingWhenCellsLoseFocus: true     // Stop editing on focus loss
};

Events

import {
  CellEditingStartedEvent,
  CellEditingStoppedEvent,
  CellValueChangedEvent,
  CellEditRequestEvent
} from 'ag-grid-community';

const gridOptions = {
  onCellEditingStarted: (event: CellEditingStartedEvent) => {
    console.log('Started editing:', event.colDef.field);
  },
  
  onCellEditingStopped: (event: CellEditingStoppedEvent) => {
    console.log('Stopped editing:', event.colDef.field);
  },
  
  onCellValueChanged: (event: CellValueChangedEvent) => {
    console.log('Value changed:', {
      column: event.colDef.field,
      oldValue: event.oldValue,
      newValue: event.newValue
    });
  },
  
  // Only fires when readOnlyEdit: true
  onCellEditRequest: (event: CellEditRequestEvent) => {
    console.log('Edit request:', event);
  }
};

Common Use Cases

import { NewValueParams } from 'ag-grid-community';

const columnDefs = [
  {
    field: 'price',
    editable: true,
    onCellValueChanged: (event: NewValueParams) => {
      // Update total when price changes
      event.data.total = event.data.price * event.data.quantity;
      event.api.refreshCells({ rowNodes: [event.node], force: true });
    }
  }
];
class AsyncValidatingEditor implements ICellEditor {
  async init(params: ICellEditorParams): Promise<void> {
    // Setup editor
  }
  
  async getValue(): Promise<any> {
    const value = this.eInput.value;
    
    // Validate asynchronously
    const isValid = await validateOnServer(value);
    
    if (!isValid) {
      throw new Error('Validation failed');
    }
    
    return value;
  }
}

TypeScript Interfaces

// From iCellEditor.ts
export interface ICellEditor<TValue = any> {
  getValue(): TValue | null | undefined;
  init(params: ICellEditorParams): void;
  getGui(): HTMLElement;
  
  // Optional methods
  destroy?(): void;
  isPopup?(): boolean;
  getPopupPosition?(): 'over' | 'under';
  isCancelBeforeStart?(): boolean;
  isCancelAfterEnd?(): boolean;
  refresh?(params: ICellEditorParams): void;
  afterGuiAttached?(): void;
  getValidationErrors?(): string[] | null;
  getValidationElement?(tooltip: boolean): HTMLElement;
}

// Edit API
export function startEditingCell(beans: BeanCollection, params: StartEditingCellParams): void;
export function stopEditing(beans: BeanCollection, cancel?: boolean): void;
export function isEditing(beans: BeanCollection, cellPosition: CellPosition): boolean;
export function getEditingCells(beans: BeanCollection): EditingCellPosition[];

Best Practices

1

Choose the right edit mode

Use full row editing for forms where all fields must be valid together. Use cell editing for independent field updates.
2

Provide immediate validation feedback

Implement getValidationErrors() to show validation errors as users type.
3

Handle null values gracefully

Always check for null/undefined in value getters, setters, and parsers.
4

Use appropriate editors for data types

Use number editor for numbers, date editor for dates, etc. This provides better UX and validation.
5

Consider read-only mode for complex state

Use readOnlyEdit: true with onCellEditRequest when integrating with external state management like Redux.