# Editing > Inline editing, copy/paste, fill handle, undo/redo, and validation. --- .. llms_copy::references/editing .. toc:: ### Overview Dash Glide Grid supports inline cell editing with **copy/paste**, **fill handle**, **undo/redo**, **row appending**, and **client-side validation**. ### Basic Usage Double-click a cell to edit. Track changes using the `cellEdited` callback prop. Use `readonly` to disable editing. To also prevent the popup overlay from appearing, set `allowOverlay: False` in cell data (requires `kind` to be specified). .. exec::examples.reference.editing.basic :code: false ```python # File: examples/reference/editing/basic.py import dash_glide_grid as dgg import dash_mantine_components as dmc from dash import callback, Input, Output columns = [ {"title": "Name", "id": "name", "width": 150}, {"title": "Email", "id": "email", "width": 200}, {"title": "Role", "id": "role", "width": 120}, ] data = [ {"name": "Alice Johnson", "email": "alice@example.com", "role": "Engineer"}, {"name": "Bob Smith", "email": "bob@example.com", "role": "Designer"}, {"name": "Carol White", "email": "carol@example.com", "role": "Manager"}, {"name": "David Brown", "email": "david@example.com", "role": "Developer"}, ] component = dmc.Stack([ dmc.Group([ dmc.Switch( id="editing-basic-readonly", label="Read Only", checked=False, ), dmc.Switch( id="editing-basic-overlay", label="Allow Overlay", checked=True, ), ]), dgg.GlideGrid( id={"type": "glide-grid", "index": "editing-basic"}, columns=columns, data=data, height=200, ), dmc.Text(id="editing-basic-output", size="sm", c="dimmed"), ]) @callback( Output({"type": "glide-grid", "index": "editing-basic"}, "readonly"), Input("editing-basic-readonly", "checked"), ) def toggle_readonly(checked): return checked @callback( Output({"type": "glide-grid", "index": "editing-basic"}, "data"), Input("editing-basic-overlay", "checked"), ) def toggle_overlay(allow_overlay): """Toggle allowOverlay on all cells. Requires readonly=True to fully disable overlay.""" return [ { "name": {"kind": "text", "data": row["name"], "allowOverlay": allow_overlay}, "email": {"kind": "text", "data": row["email"], "allowOverlay": allow_overlay}, "role": {"kind": "text", "data": row["role"], "allowOverlay": allow_overlay}, } for row in [ {"name": "Alice Johnson", "email": "alice@example.com", "role": "Engineer"}, {"name": "Bob Smith", "email": "bob@example.com", "role": "Designer"}, {"name": "Carol White", "email": "carol@example.com", "role": "Manager"}, {"name": "David Brown", "email": "david@example.com", "role": "Developer"}, ] ] @callback( Output("editing-basic-output", "children"), Input({"type": "glide-grid", "index": "editing-basic"}, "cellEdited"), ) def show_edit(edited): if edited: return f"Edited: column {edited['col']}, row {edited['row']} → {edited['value']}" return "Double-click a cell to edit. Disable overlay + enable readonly to prevent popups." ``` :defaultExpanded: false :withExpandedButton: true ### Copy & Paste Enable clipboard operations with `enableCopyPaste`. Include headers when copying with `copyHeaders`. Use `coercePasteValue` to transform pasted values. .. exec::examples.reference.editing.copypaste :code: false :defaultExpanded: false :withExpandedButton: true ### Fill Handle Enable Excel-like drag-to-fill with `fillHandle`. Control fill direction with `allowedFillDirections`. .. exec::examples.reference.editing.fillhandle :code: false ```python # File: examples/reference/editing/fillhandle.py import dash_glide_grid as dgg import dash_mantine_components as dmc from dash import callback, Input, Output columns = [ {"title": "Item", "id": "item", "width": 120}, {"title": "Q1", "id": "q1", "width": 80}, {"title": "Q2", "id": "q2", "width": 80}, {"title": "Q3", "id": "q3", "width": 80}, {"title": "Q4", "id": "q4", "width": 80}, ] data = [ {"item": "Product A", "q1": 100, "q2": 0, "q3": 0, "q4": 0}, {"item": "Product B", "q1": 200, "q2": 0, "q3": 0, "q4": 0}, {"item": "Product C", "q1": 150, "q2": 0, "q3": 0, "q4": 0}, {"item": "Product D", "q1": 300, "q2": 0, "q3": 0, "q4": 0}, ] component = dmc.Stack([ dmc.Select( id="editing-fill-direction", label="Fill Direction", data=[ {"value": "horizontal", "label": "Horizontal only"}, {"value": "vertical", "label": "Vertical only"}, {"value": "orthogonal", "label": "Orthogonal (default)"}, {"value": "any", "label": "Any direction"}, ], value="orthogonal", w=200, ), dgg.GlideGrid( id={"type": "glide-grid", "index": "editing-fillhandle"}, columns=columns, data=data, height=200, fillHandle=True, allowedFillDirections="orthogonal", rangeSelect="rect", ), dmc.Text( "Select a cell, then drag the small square at the bottom-right corner to fill adjacent cells.", size="sm", c="dimmed", ), ]) @callback( Output({"type": "glide-grid", "index": "editing-fillhandle"}, "allowedFillDirections"), Input("editing-fill-direction", "value"), ) def update_direction(value): return value ``` :defaultExpanded: false :withExpandedButton: true ### Appending Rows Add new rows using `trailingRowOptions`. Handle the `rowAppended` callback to add data. .. exec::examples.reference.editing.append :code: false ```python # File: examples/reference/editing/append.py import dash_glide_grid as dgg import dash_mantine_components as dmc from dash import callback, Input, Output, State columns = [ {"title": "Name", "id": "name", "width": 150}, {"title": "Email", "id": "email", "width": 200}, {"title": "Department", "id": "dept", "width": 120}, ] initial_data = [ {"name": "Alice Johnson", "email": "alice@example.com", "dept": "Engineering"}, {"name": "Bob Smith", "email": "bob@example.com", "dept": "Marketing"}, ] component = dmc.Stack([ dgg.GlideGrid( id={"type": "glide-grid", "index": "editing-append"}, columns=columns, data=initial_data, height=250, trailingRowOptions={ "hint": "Add new row...", "sticky": True, "tint": True, }, ), dmc.Text(id="editing-append-output", size="sm", c="dimmed"), ]) @callback( Output({"type": "glide-grid", "index": "editing-append"}, "data"), Output("editing-append-output", "children"), Input({"type": "glide-grid", "index": "editing-append"}, "rowAppended"), State({"type": "glide-grid", "index": "editing-append"}, "data"), prevent_initial_call=True, ) def append_row(appended, current_data): if appended: new_row = {"name": "", "email": "", "dept": ""} new_data = current_data + [new_row] return new_data, f"Added row {len(new_data)}" return current_data, "Click the trailing row to add a new row" ``` :defaultExpanded: false :withExpandedButton: true ### Cell Validation Validate edits client-side using `validateCell`. The JavaScript function returns `false` to reject edits. .. exec::examples.reference.editing.validation :code: false :defaultExpanded: false :withExpandedButton: true ### Cell Activation Behavior Control when cells open for editing with `cellActivationBehavior`. Choose between **single-click** for rapid data entry, **second-click (default)** for balanced selection/editing, or **double-click** to prevent accidental edits. .. exec::examples.reference.editing.cell_activation :code: false ```python # File: examples/reference/editing/cell_activation.py import dash_glide_grid as dgg import dash_mantine_components as dmc from dash import callback, Input, Output columns = [ {"title": "Name", "id": "name", "width": 150}, {"title": "Value", "id": "value", "width": 100}, {"title": "Notes", "id": "notes", "width": 220}, ] data = [ {"name": "Item A", "value": 100, "notes": "Click to edit this cell"}, {"name": "Item B", "value": 200, "notes": "Try different activation modes"}, {"name": "Item C", "value": 300, "notes": "Single-click is fastest"}, {"name": "Item D", "value": 400, "notes": "Double-click is most deliberate"}, ] component = dmc.Stack([ dmc.SegmentedControl( id="editing-activation-mode", data=[ {"value": "single-click", "label": "Single Click"}, {"value": "second-click", "label": "Second Click"}, {"value": "double-click", "label": "Double Click"}, ], value="second-click", ), dgg.GlideGrid( id={"type": "glide-grid", "index": "editing-activation"}, columns=columns, data=data, height=200, cellActivationBehavior="second-click", ), dmc.Text(id="editing-activation-output", size="sm", c="dimmed"), ]) @callback( Output({"type": "glide-grid", "index": "editing-activation"}, "cellActivationBehavior"), Input("editing-activation-mode", "value"), ) def update_activation_mode(value): return value @callback( Output("editing-activation-output", "children"), Input("editing-activation-mode", "value"), ) def show_mode_description(mode): descriptions = { "single-click": "Click once to start editing immediately", "second-click": "Click to select, click again to edit (default)", "double-click": "Double-click to start editing", } return descriptions.get(mode, "") ``` :defaultExpanded: false :withExpandedButton: true ### Edit On Type Enable or disable immediate editing when typing on a selected cell with `editOnType`. When enabled (default), selecting a cell and typing starts editing. When disabled, you must activate the cell first via double-click or Enter. .. exec::examples.reference.editing.edit_on_type :code: false ```python # File: examples/reference/editing/edit_on_type.py import dash_glide_grid as dgg import dash_mantine_components as dmc from dash import callback, Input, Output columns = [ {"title": "Product", "id": "product", "width": 140}, {"title": "Price", "id": "price", "width": 100}, {"title": "Quantity", "id": "quantity", "width": 100}, {"title": "Notes", "id": "notes", "width": 200}, ] data = [ {"product": "Laptop", "price": 999.99, "quantity": 5, "notes": "Select and type"}, {"product": "Mouse", "price": 29.99, "quantity": 50, "notes": "Try with toggle on/off"}, {"product": "Keyboard", "price": 79.99, "quantity": 30, "notes": "When on, typing edits"}, {"product": "Monitor", "price": 299.99, "quantity": 10, "notes": "When off, must click"}, ] component = dmc.Stack([ dmc.Switch( id="editing-type-toggle", label="Edit on Type", checked=True, ), dgg.GlideGrid( id={"type": "glide-grid", "index": "editing-type"}, columns=columns, data=data, height=200, editOnType=True, ), dmc.Text(id="editing-type-output", size="sm", c="dimmed"), ]) @callback( Output({"type": "glide-grid", "index": "editing-type"}, "editOnType"), Input("editing-type-toggle", "checked"), ) def update_edit_on_type(checked): return checked @callback( Output("editing-type-output", "children"), Input("editing-type-toggle", "checked"), ) def show_mode_description(enabled): if enabled: return "Select a cell and start typing to edit immediately" return "Select a cell, then double-click or press Enter to edit" ``` :defaultExpanded: false :withExpandedButton: true ### Undo / Redo Enable undo/redo with `enableUndoRedo`. Users can press Cmd+Z (undo) or Cmd+Shift+Z (redo). Control history depth with `maxUndoSteps`. Trigger actions programmatically with `undoRedoAction`. .. exec::examples.reference.editing.undo_redo :code: false ```python # File: examples/reference/editing/undo_redo.py import time import dash_glide_grid as dgg import dash_mantine_components as dmc from dash import callback, Input, Output, no_update from dash_iconify import DashIconify columns = [ {"title": "Name", "id": "name", "width": 150}, {"title": "Department", "id": "department", "width": 150}, {"title": "Salary", "id": "salary", "width": 120}, ] data = [ {"name": "Alice Johnson", "department": "Engineering", "salary": 95000}, {"name": "Bob Smith", "department": "Marketing", "salary": 75000}, {"name": "Carol White", "department": "Sales", "salary": 82000}, {"name": "David Brown", "department": "Engineering", "salary": 105000}, ] component = dmc.Stack([ dmc.Group([ dmc.ActionIcon( DashIconify(icon="mdi:undo", width=20), id="editing-undo-btn", variant="default", disabled=True, ), dmc.ActionIcon( DashIconify(icon="mdi:redo", width=20), id="editing-redo-btn", variant="default", disabled=True, ), dmc.Text(id="editing-undo-status", size="sm", c="dimmed"), ]), dgg.GlideGrid( id={"type": "glide-grid", "index": "editing-undo"}, columns=columns, data=data, height=200, enableUndoRedo=True, maxUndoSteps=20, ), dmc.Text(id="editing-undo-output", size="sm", c="dimmed"), ]) @callback( Output("editing-undo-btn", "disabled"), Output("editing-redo-btn", "disabled"), Output("editing-undo-status", "children"), Input({"type": "glide-grid", "index": "editing-undo"}, "canUndo"), Input({"type": "glide-grid", "index": "editing-undo"}, "canRedo"), ) def update_button_states(can_undo, can_redo): status = [] if can_undo: status.append("Can undo") if can_redo: status.append("Can redo") return not can_undo, not can_redo, " | ".join(status) if status else "Edit cells to build history" @callback( Output({"type": "glide-grid", "index": "editing-undo"}, "undoRedoAction", allow_duplicate=True), Input("editing-undo-btn", "n_clicks"), prevent_initial_call=True, ) def handle_undo(n_clicks): if n_clicks: return {"action": "undo", "timestamp": int(time.time() * 1000)} return no_update @callback( Output({"type": "glide-grid", "index": "editing-undo"}, "undoRedoAction", allow_duplicate=True), Input("editing-redo-btn", "n_clicks"), prevent_initial_call=True, ) def handle_redo(n_clicks): if n_clicks: return {"action": "redo", "timestamp": int(time.time() * 1000)} return no_update @callback( Output("editing-undo-output", "children"), Input({"type": "glide-grid", "index": "editing-undo"}, "undoRedoPerformed"), ) def show_undo_redo(event): if event: return f"Action performed: {event['action'].upper()}" return "Try Cmd+Z to undo, Cmd+Shift+Z to redo" ``` :defaultExpanded: false :withExpandedButton: true ### Props Reference #### Editing Mode Props | Property | Type | Default | Description | |----------|------|---------|-------------| | `readonly` | boolean | False | Make entire grid read-only | | `cellEdited` | dict | - | Info about last edit: `{col, row, value, timestamp}` | | `cellActivationBehavior` | string | 'second-click' | When cells open for editing: 'single-click', 'second-click', 'double-click' | | `editOnType` | boolean | True | Start editing when typing on selected cell | #### Copy & Paste Props | Property | Type | Default | Description | |----------|------|---------|-------------| | `enableCopyPaste` | boolean | True | Enable Ctrl+C/V clipboard operations | | `copyHeaders` | boolean | False | Include column headers when copying | | `coercePasteValue` | dict | - | JS function to transform pasted values: `{"function": "name(val, cell)"}` | #### Fill Handle Props | Property | Type | Default | Description | |----------|------|---------|-------------| | `fillHandle` | boolean | False | Enable drag-to-fill handle | | `allowedFillDirections` | string | 'orthogonal' | Fill directions: 'horizontal', 'vertical', 'orthogonal', 'any' | #### Row Append Props | Property | Type | Default | Description | |----------|------|---------|-------------| | `trailingRowOptions` | dict | - | Config for trailing row: `{hint, sticky, tint, targetColumn}` | | `rowAppended` | dict | - | Fired when user clicks trailing row: `{timestamp}` | #### Validation Props | Property | Type | Default | Description | |----------|------|---------|-------------| | `validateCell` | dict | - | JS function for validation: `{"function": "name(cell, newValue)"}` | #### Keyboard Props | Property | Type | Default | Description | |----------|------|---------|-------------| | `keybindings` | dict | - | Customize keyboard shortcuts (copy, paste, delete, etc.) | #### Undo / Redo Props | Property | Type | Default | Description | |----------|------|---------|-------------| | `enableUndoRedo` | boolean | False | Enable undo/redo for cell edits | | `maxUndoSteps` | number | 50 | Maximum undo steps to track | | `undoRedoAction` | dict | - | Trigger undo/redo: `{action: 'undo'\|'redo', timestamp}` | | `canUndo` | boolean | - | Output: true when undo is available | | `canRedo` | boolean | - | Output: true when redo is available | | `undoRedoPerformed` | dict | - | Output: fired when undo/redo executes | --- *Source: /reference/editing* *Generated with dash-improve-my-llms*