Skip to main content

Overview

Cell Editors provide custom interfaces for editing cell values when users enter edit mode. Use cell editors to:
  • Create specialized input controls (dropdowns, date pickers, etc.)
  • Implement custom validation logic
  • Build multi-field editors for complex data types
  • Add rich editing experiences with autocomplete, suggestions, or formatting
  • Control when editing starts and stops
Editing is triggered by double-clicking a cell, pressing F2, or typing a printable character. You can customize this behavior using editType and singleClickEdit column properties.

ICellEditorParams Interface

The grid provides cell editors with comprehensive parameters:
interface ICellEditorParams<TData = any, TValue = any, TContext = any>
  extends ICellEditorParamsShared<TData, TValue, TContext> {
  /** Current value of the cell */
  value: TValue | null | undefined;
  
  /** Key that started the edit (e.g., 'Enter', 'F2', or printable character) */
  eventKey: string | null;
  
  /** Grid column */
  column: Column<TValue>;
  
  /** Column definition */
  colDef: ColDef<TData, TValue>;
  
  /** Row node for the cell */
  node: IRowNode<TData>;
  
  /** Row data */
  data: TData;
  
  /** Editing row index */
  rowIndex: number;
  
  /** True if this cell started the edit (user double-clicked or pressed key) */
  cellStartedEdit: boolean;
  
  /** Pass key events back to grid (tab, arrows, etc.) */
  onKeyDown: (event: KeyboardEvent) => void;
  
  /** Stop editing and optionally suppress navigation */
  stopEditing: (suppressNavigateAfterEdit?: boolean) => void;
  
  /** Reference to the grid cell DOM element */
  eGridCell: HTMLElement;
  
  /** Parse a string value using the column's valueParser */
  parseValue: (value: string) => TValue | null | undefined;
  
  /** Format a value using the column's valueFormatter */
  formatValue: (value: TValue | null | undefined) => string;
  
  /** Custom validation callback */
  getValidationErrors?: (
    params: IErrorValidationParams<TData, TValue, TContext>
  ) => string[] | null;
  
  /** Run editor validation */
  validate(): void;
}

ICellEditor Interface

Implement this interface to create a custom cell editor:
interface ICellEditor<TValue = any> extends BaseCellEditor {
  /**
   * Mandatory - Return the final value.
   * Called by the grid once after editing is complete.
   */
  getValue(): TValue | null | undefined;

  /**
   * Optional: Called when cell editor params update
   */
  refresh?(params: ICellEditorParams<any, TValue>): void;

  /**
   * Optional: Called after the GUI is attached to the DOM.
   * Use for operations that require attachment (e.g., focus).
   */
  afterGuiAttached?(): void;

  /**
   * Optional: Return true for popup editors.
   * Default is false (inline editing).
   */
  isPopup?(): boolean;

  /**
   * Optional: Return 'over' or 'under' for popup position.
   * Only called if isPopup() returns true.
   */
  getPopupPosition?(): 'over' | 'under' | undefined;
}

BaseCellEditor Interface

interface BaseCellEditor {
  /**
   * Optional: Return true to cancel editing before it starts.
   * Useful for conditional editing based on the key pressed.
   */
  isCancelBeforeStart?(): boolean;

  /**
   * Optional: Return true to cancel editing and discard the value.
   * Called after editing is complete.
   */
  isCancelAfterEnd?(): boolean;

  /**
   * Optional: Called when focus should be put into the editor
   * (full row edit mode)
   */
  focusIn?(): void;

  /**
   * Optional: Called when focus is leaving the editor
   * (full row edit mode)
   */
  focusOut?(): void;

  /**
   * Optional: Return the element for validation feedback.
   * @param tooltip - True for tooltip anchor, false for CSS styling
   */
  getValidationElement?(tooltip: boolean): HTMLElement;

  /**
   * Optional: Return validation error messages
   */
  getValidationErrors?(): string[] | null;
}

Basic Examples

class NumericCellEditor {
  init(params) {
    this.params = params;
    this.eInput = document.createElement('input');
    this.eInput.type = 'number';
    this.eInput.value = params.value ?? '';
    this.eInput.style.width = '100%';
    
    // Focus and select on attach
    this.eInput.addEventListener('focus', () => {
      this.eInput.select();
    });
  }

  getGui() {
    return this.eInput;
  }

  afterGuiAttached() {
    this.eInput.focus();
  }

  getValue() {
    const value = this.eInput.value;
    return value === '' ? null : Number(value);
  }

  destroy() {
    // Cleanup if needed
  }
}

// Column definition
const columnDefs = [
  {
    field: 'price',
    editable: true,
    cellEditor: NumericCellEditor
  }
];

Advanced Examples

class SelectCellEditor {
  init(params) {
    this.params = params;
    this.eGui = document.createElement('select');
    this.eGui.style.width = '100%';
    this.eGui.style.height = '100%';
    
    const options = params.values || [];
    options.forEach(option => {
      const optionEl = document.createElement('option');
      optionEl.value = option;
      optionEl.text = option;
      if (option === params.value) {
        optionEl.selected = true;
      }
      this.eGui.appendChild(optionEl);
    });
  }

  getGui() {
    return this.eGui;
  }

  afterGuiAttached() {
    this.eGui.focus();
  }

  getValue() {
    return this.eGui.value;
  }
}

// Usage
const columnDefs = [
  {
    field: 'country',
    editable: true,
    cellEditor: SelectCellEditor,
    cellEditorParams: {
      values: ['USA', 'UK', 'Canada', 'Australia']
    }
  }
];

Date Picker Editor

class DateCellEditor {
  init(params) {
    this.params = params;
    this.eInput = document.createElement('input');
    this.eInput.type = 'date';
    this.eInput.style.width = '100%';
    
    // Convert value to ISO date string
    if (params.value) {
      const date = new Date(params.value);
      this.eInput.value = date.toISOString().split('T')[0];
    }
  }

  getGui() {
    return this.eInput;
  }

  afterGuiAttached() {
    this.eInput.focus();
  }

  getValue() {
    if (!this.eInput.value) return null;
    return new Date(this.eInput.value);
  }
}
class PopupCellEditor {
  init(params) {
    this.params = params;
    this.eGui = document.createElement('div');
    this.eGui.style.padding = '20px';
    this.eGui.style.background = 'white';
    this.eGui.style.border = '1px solid #ccc';
    this.eGui.style.borderRadius = '4px';
    
    this.eGui.innerHTML = `
      <div>
        <label>Enter value:</label>
        <input type="text" class="editor-input" value="${params.value || ''}" />
        <div style="margin-top: 10px;">
          <button class="btn-ok">OK</button>
          <button class="btn-cancel">Cancel</button>
        </div>
      </div>
    `;
    
    this.eInput = this.eGui.querySelector('.editor-input');
    this.eGui.querySelector('.btn-ok').addEventListener('click', () => {
      params.stopEditing();
    });
    this.eGui.querySelector('.btn-cancel').addEventListener('click', () => {
      this.cancelled = true;
      params.stopEditing();
    });
  }

  getGui() {
    return this.eGui;
  }

  isPopup() {
    return true;
  }

  getPopupPosition() {
    return 'over';
  }

  afterGuiAttached() {
    this.eInput.focus();
    this.eInput.select();
  }

  getValue() {
    return this.cancelled ? this.params.value : this.eInput.value;
  }

  isCancelAfterEnd() {
    return this.cancelled;
  }
}

Conditional Editing

Control when editing starts using isCancelBeforeStart():
class NumericOnlyCellEditor {
  init(params) {
    this.params = params;
    this.eInput = document.createElement('input');
    this.eInput.type = 'number';
    this.eInput.value = params.value ?? '';
  }

  getGui() {
    return this.eInput;
  }

  getValue() {
    return Number(this.eInput.value);
  }

  isCancelBeforeStart() {
    // Only allow editing if a number key was pressed
    const key = this.params.eventKey;
    return key && !/^[0-9]$/.test(key);
  }

  afterGuiAttached() {
    // If a number was typed, append it
    if (this.params.eventKey && /^[0-9]$/.test(this.params.eventKey)) {
      this.eInput.value = this.params.eventKey;
    }
    this.eInput.focus();
  }
}

Validation

Built-in Validation

Implement getValidationErrors() to provide validation:
class ValidatedCellEditor {
  init(params) {
    this.params = params;
    this.eInput = document.createElement('input');
    this.eInput.type = 'number';
    this.eInput.value = params.value ?? '';
    
    // Validate on input
    this.eInput.addEventListener('input', () => {
      params.validate();
    });
  }

  getGui() {
    return this.eInput;
  }

  getValue() {
    const value = this.eInput.value;
    return value === '' ? null : Number(value);
  }

  getValidationErrors() {
    const value = this.getValue();
    const errors = [];
    
    if (value === null || value === undefined) {
      errors.push('Value is required');
    } else if (value < 0) {
      errors.push('Value must be positive');
    } else if (value > 1000) {
      errors.push('Value must be less than 1000');
    }
    
    return errors.length > 0 ? errors : null;
  }

  getValidationElement(tooltip) {
    return this.eInput;
  }

  afterGuiAttached() {
    this.eInput.focus();
  }
}

Custom Validation Callback

Use cellEditorParams.getValidationErrors for validation:
const columnDefs = [
  {
    field: 'age',
    editable: true,
    cellEditor: 'agNumberCellEditor',
    cellEditorParams: {
      getValidationErrors: (params) => {
        const { value } = params;
        const errors = [];
        
        if (value < 18) {
          errors.push('Must be 18 or older');
        }
        if (value > 100) {
          errors.push('Invalid age');
        }
        
        return errors.length > 0 ? errors : null;
      }
    }
  }
];

Built-in Cell Editors

AG Grid provides several built-in editors:

Text Editor

columnDefs = [
  {
    field: 'name',
    editable: true,
    cellEditor: 'agTextCellEditor',
    cellEditorParams: {
      maxLength: 50,
      useFormatter: false
    }
  }
];

Number Editor

columnDefs = [
  {
    field: 'price',
    editable: true,
    cellEditor: 'agNumberCellEditor',
    cellEditorParams: {
      min: 0,
      max: 10000,
      precision: 2
    }
  }
];

Date Editor

columnDefs = [
  {
    field: 'birthDate',
    editable: true,
    cellEditor: 'agDateCellEditor',
    cellEditorParams: {
      min: '1900-01-01',
      max: '2100-12-31'
    }
  }
];

Select Editor

columnDefs = [
  {
    field: 'country',
    editable: true,
    cellEditor: 'agSelectCellEditor',
    cellEditorParams: {
      values: ['USA', 'UK', 'Canada', 'Australia']
    }
  }
];

Large Text Editor

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

Keyboard Navigation

Handle keyboard events to improve user experience:
class KeyboardAwareCellEditor {
  init(params) {
    this.params = params;
    this.eInput = document.createElement('input');
    this.eInput.value = params.value ?? '';
    
    this.eInput.addEventListener('keydown', (event) => {
      // Stop editing on Enter
      if (event.key === 'Enter') {
        params.stopEditing();
      }
      // Cancel on Escape
      else if (event.key === 'Escape') {
        this.cancelled = true;
        params.stopEditing();
      }
      // Pass Tab to grid for navigation
      else if (event.key === 'Tab') {
        params.onKeyDown(event);
      }
    });
  }

  getGui() {
    return this.eInput;
  }

  getValue() {
    return this.cancelled ? this.params.value : this.eInput.value;
  }

  isCancelAfterEnd() {
    return this.cancelled;
  }

  afterGuiAttached() {
    this.eInput.focus();
    this.eInput.select();
  }
}

Full Row Editing

Implement focusIn() and focusOut() for full row edit mode:
class FullRowCellEditor {
  init(params) {
    this.params = params;
    this.eInput = document.createElement('input');
    this.eInput.value = params.value ?? '';
  }

  getGui() {
    return this.eInput;
  }

  getValue() {
    return this.eInput.value;
  }

  focusIn() {
    this.eInput.focus();
    this.eInput.select();
  }

  focusOut() {
    // Optional cleanup when focus leaves
  }
}

// Enable full row editing
const gridOptions = {
  editType: 'fullRow',
  columnDefs: [
    { field: 'name', editable: true, cellEditor: FullRowCellEditor },
    { field: 'age', editable: true, cellEditor: FullRowCellEditor }
  ]
};

Best Practices

Implement afterGuiAttached() to focus your input element:
afterGuiAttached() {
  this.eInput.focus();
  this.eInput.select(); // Select text for easy replacement
}
If the user starts editing by typing a character, use it as the initial value:
init(params) {
  this.eInput = document.createElement('input');
  // If a printable character was typed, use it as initial value
  if (params.eventKey && params.eventKey.length === 1) {
    this.eInput.value = params.eventKey;
  } else {
    this.eInput.value = params.value ?? '';
  }
}
Leverage the provided utility functions:
init(params) {
  this.params = params;
  this.eInput = document.createElement('input');
  // Use formatValue to display the initial value
  this.eInput.value = params.formatValue(params.value);
}

getValue() {
  // Use parseValue to convert back to the correct type
  return this.params.parseValue(this.eInput.value);
}
Provide immediate feedback with validation:
getValidationErrors() {
  const value = this.getValue();
  if (!value) return ['Value is required'];
  return null;
}

TypeScript Support

Use generics for type-safe cell editors:
interface Product {
  name: string;
  price: number;
  category: string;
}

class TypedCellEditor implements ICellEditorComp<Product, number> {
  private eInput: HTMLInputElement;
  private params: ICellEditorParams<Product, number>;

  init(params: ICellEditorParams<Product, number>): void {
    this.params = params;
    this.eInput = document.createElement('input');
    this.eInput.type = 'number';
    
    // TypeScript knows params.data is Product and params.value is number
    this.eInput.value = String(params.value ?? 0);
  }

  getGui(): HTMLElement {
    return this.eInput;
  }

  getValue(): number | null {
    const value = this.eInput.value;
    return value === '' ? null : Number(value);
  }

  afterGuiAttached(): void {
    this.eInput.focus();
  }
}

Next Steps

Cell Renderers

Learn about custom cell renderers for display

Filters

Create custom filter components

Build docs developers (and LLMs) love