Editing

Overview

rs-grid supports inline cell editing. Double-click a cell (or press Enter on a selected cell) to start editing. The web layer overlays a DOM input element over the canvas cell.

Editing lifecycle

StartEdit → user types → CommitEdit (Enter) or CancelEdit (Escape)

Start editing

state.apply(GridCommand::StartEdit {
    row: 5,
    col_key: "name".into(),
});

The grid stores the current cell value in an EditCell snapshot so it can be restored on cancel.

Commit

state.apply(GridCommand::CommitEdit {
    row: 5,
    col_key: "name".into(),
    value: "New Value".into(),
});

The new value is written to the data source (or the patches layer for read-only sources). The edit is recorded in the undo history.

Cancel

state.apply(GridCommand::CancelEdit);

Restores the original value. No undo entry is created.

Cell editors

The editor type is controlled by ColumnDef::editor:

Text input

When editor is Some(CellEditor::Text), a plain <input type="text"> is shown:

col.editor = Some(CellEditor::Text);

No editor (None)

When editor is None (the default for a new column), double-clicking the cell dispatches CancelEdit and shows no DOM overlay. To enable plain-text editing on a column, set editor explicitly:

col.editor = Some(CellEditor::Text);

Select dropdown

For fixed-choice columns, use CellEditor::Select:

col.editor = Some(CellEditor::Select {
    options: vec![
        SelectOption {
            value: "active".into(),
            label: "Active".into(),
            icon: None,
        },
        SelectOption {
            value: "inactive".into(),
            label: "Inactive".into(),
            icon: None,
        },
    ],
});

Each SelectOption has:

  • value — stored in the cell on commit
  • label — displayed in the dropdown
  • icon — optional icon URL shown left of the label

Undo support

Cell edits are automatically recorded in the undo history. Press Ctrl+Z to undo or Ctrl+Y to redo. See Undo & Redo for details.

Editing with read-only data sources

Even FnDataSource (which has no set_cell) supports editing — the new value is stored in GridModel::patches, which overrides the data source for that cell.

Per-cell editability

ColumnDef::editable locks an entire column. For finer control — locking individual cells based on row data — set a dynamic predicate with .editable_when(...):

use rs_grid_core::column::ColumnDef;

let notes = ColumnDef::new("notes", "Notes", 160.0).editable_when(
    |row, model| model.get_cell(row, "status").as_deref() != Some("locked"),
);

The predicate receives the row index and the full GridModel, so it can read any column's value for that row — not just its own — to decide whether a cell is editable. This mirrors AG Grid's colDef.editable callback.

Checked only when the static editable flag is true — if a column is statically read-only, the predicate is never called:

StartEdit → editable == false?  → blocked, predicate not evaluated
          → editable == true    → editable_when(row, model)?

When a cell resolves to non-editable, rs-grid-web shows a not-allowed cursor on hover and renders the cell with a themed locked-cell background and text color (Theme::locked_cell_bg / Theme::locked_cell_text, CSS variables --rs-grid-locked-cell-bg / --rs-grid-locked-cell-text — see Theming).

See EditablePredicate for the full type reference.

Per-cell decoration

Sometimes a business rule spans two columns and neither one is invalid on its own — e.g. a row is inconsistent if one of a paired file/label column is filled in and the other is blank. Neither column can express this with Validation alone, since each accepts a blank value in isolation. Attach a dynamic decorator with .decorated_when(...) to flag the cell persistently, at rest — not just while it's being edited:

use rs_grid_core::column::{CellDecoration, ColumnDef};

let doc1_file = ColumnDef::new("doc1_file", "Doc 1 file", 160.0)
    .decorated_when(|row, model| {
        let file = model.get_cell(row, "doc1_file").unwrap_or_default();
        let label = model.get_cell(row, "doc1_label").unwrap_or_default();
        (file.is_empty() != label.is_empty()).then(|| {
            CellDecoration::default().with_border_color([239, 68, 68, 255])
        })
    });

Like .editable_when(...), the closure receives the row index and the full GridModel, so it can read any other column's value for that row. Unlike editability or validation, decoration never blocks anything — it's purely cosmetic, and there's no static gate to short-circuit it.

The border color and background tint are RGBA values you supply directly in CellDecoration — they aren't read from the theme. Only the border's stroke width is themed (--rs-grid-decoration-border-width), since it's uniform across every decorated cell regardless of which color you pick.

See CellDecoration and CellDecorator for the full type reference.

Validation

CommitEdit (and the live ValidateEdit command fired on every keystroke) run through per-column validation before the value is accepted. See Validation for declarative rules, the revert-or-block policy, and live feedback UI hooks.