I implemented method number two with the dagcomponentfuncs.DeleteButton
and it looks like the row/record is being deleted, but when I later access the rowData
, the deleted row/record is still there, as if it hasn’t been deleted at all.
// Delete button for AG Grid
// https://community.plotly.com/t/deleting-rows-in-dash-ag-grid/78700
dagcomponentfuncs.DeleteButton = function (props) {
function onClick() {
props.api.applyTransaction({ remove: [props.node.data] });
}
// Red color? "#ff0000"
colorWanted = props.colorWanted || "";
return React.createElement(
"span", // Using span instead of button for less default styling
{
onClick,
style: {
cursor: "pointer", // Show pointer cursor on hover
color: colorWanted,
fontSize: "16px", // Larger font size
fontWeight: "bold", // Make it bold
display: "flex", // Center content
justifyContent: "center", // Center horizontally
alignItems: "center", // Center vertically
width: "100%", // Take full width of the cell
height: "100%", // Take full height of the cell
transition: "color 0.2s", // Smooth color transition on hover
},
onMouseOver: (e) => (e.currentTarget.style.color = "#cc0000"), // Darker red on hover
onMouseOut: (e) => (e.currentTarget.style.color = colorWanted), // Restore original color
title: "Delete row", // Tooltip on hover
},
"Ă—" // Using the multiplication symbol which looks nicer than "X"
);
};
columnDefs below:
{
"field": "delete",
"headerName": "",
"cellRenderer": "DeleteButton",
"lockPosition": "left",
"filter": False,
"maxWidth": 35,
}
Here’s a fully reproducible example that illustrates the problem of the rows not actually being deleted (for my purposes):
import json
from datetime import date
import dash_bootstrap_components as dbc
import dash_mantine_components as dmc
from dash import (
Dash,
Input,
Output,
State,
_dash_renderer,
callback,
callback_context,
html,
)
from dash.exceptions import PreventUpdate
from dash_ag_grid import AgGrid
from flask import Flask
from flask_sqlalchemy import SQLAlchemy
from sqlalchemy.ext.declarative import declarative_base
app = Flask(__name__)
app.config["SQLALCHEMY_DATABASE_URI"] = "sqlite:///:memory:" # In-memory database
app.config["SQLALCHEMY_TRACK_MODIFICATIONS"] = False
app.secret_key = "your-secret-keyyyyyyyyyyyyyyyyyyyy" # Required for Flask session
# Do this before creating Dash app object.
# Dash Mantine Components is based on REACT 18.
# You must set the env variable REACT_VERSION=18.2.0 before starting the app.
# https://www.dash-mantine-components.com/migration
# _available_react_versions = {"16.14.0", "18.2.0"}
_dash_renderer._set_react_version("18.2.0")
dash_app = Dash(__name__, external_stylesheets=[dbc.themes.BOOTSTRAP], server=app)
db = SQLAlchemy(app=app)
Base = declarative_base()
def get_id_triggered() -> str:
"""Get the ID that triggered the callback"""
if not callback_context.triggered:
return ""
# This changed a bit when I updated the Dash version... Be careful
return callback_context.triggered[0]["prop_id"]
def get_ag_grid(
id: str = "",
rowData: list = [],
getRowId: str = None,
defaultColDef: dict = {},
dashGridOptions: dict = {},
columnDefs: list = [],
columnSize: str = "autoSize",
column_filter: bool = True,
column_sortable: bool = True,
pagination: bool = True,
paginationPageSize: int = 10,
skipHeaderOnAutoSize: bool = False,
columnHoverHighlight: bool = True,
# style: dict = None,
csv_filename: str = f"IJACK Data {date.today().strftime('%Y-%m-%d')}.csv",
autoHeight: bool = True,
# If autoHeight is True, height is forced to None so the below has no effect
height: str = "70vh",
className: str = "ag-theme-quartz ag-theme-ijack",
style: dict | None = None,
) -> AgGrid:
"""Get the default AgGrid component"""
if autoHeight:
# height must be None for autoHeight to work
height = None
domLayout = "autoHeight"
paginationAutoPageSize = False
page_sizes: set = {5, 10, 15, 20, 25, 50, 100}
page_sizes.add(paginationPageSize)
paginationPageSizeSelector = list(page_sizes)
paginationPageSizeSelector.sort()
else:
height = height
domLayout = "normal"
paginationAutoPageSize = True
paginationPageSizeSelector = False
# Inside a div with max-height for the viewport
return AgGrid(
id=id,
rowData=rowData,
columnDefs=columnDefs,
getRowId=getRowId,
# Default theme is "ag-theme-quartz"
className=className,
# className="ag-theme-alpine dbc-ag-grid",
style=style or {"height": height, "width": "100%"},
dashGridOptions=dashGridOptions
or {
"animateRows": True,
# allow the grid to auto-size its height to fit rows (this is nice sometimes)
"domLayout": domLayout,
"rowHeight": 30,
"skipHeaderOnAutoSize": skipHeaderOnAutoSize,
# Default tooltip show delay is 2000 ms (2 seconds)
"tooltipShowDelay": 100,
"pagination": pagination,
"paginationPageSize": paginationPageSize,
"paginationAutoPageSize": paginationAutoPageSize,
"paginationPageSizeSelector": paginationPageSizeSelector,
# Highlight the column on hover, not just the row
"columnHoverHighlight": columnHoverHighlight,
# Render all popups (including dropdown menus) in the document body
# instead of inside the grid container, so they will not be cut off by the grid container.
"popupParent": {"function": "setBody()"},
# Make filter buttons more touchable
"suppressMenuHide": True, # Always show filter buttons
# Enable better touch support
"suppressTouch": False, # Allow touch interactions
"rowSelection": "multiple",
"suppressRowClickSelection": True,
},
# https://dash.plotly.com/dash-ag-grid/column-sizing
# "autoSize" = the grid will fill the width of the container
# "sizeToFit" = the grid will fill the width of the container, but each column will be auto-sized to fit the data
# "responsiveSizeToFit" = the grid will fill the width of the container, but each column will be auto-sized to fit the data,
# and the grid will resize as the container resizes
columnSize=columnSize,
defaultColDef=defaultColDef
or {
"resizable": True,
"editable": False,
"sortable": column_sortable,
"filter": column_filter,
"flex": 1,
"initialWidth": 100,
"minWidth": 100,
"maxWidth": 150,
"wrapHeaderText": True,
"autoHeaderHeight": True,
"wrapText": True,
"autoHeight": True,
"cellStyle": {
# Breaks words according to the default line break rules,
# not 'break-all' which breaks all words on any character.
"wordBreak": "normal",
"whiteSpace": "normal",
"line-height": "1.5",
},
"filterParams": {
"buttons": ["apply", "clear"], # Add apply/clear buttons
"closeOnApply": True, # Close filter popup after applying
},
"sortingOrder": ["desc", "asc", None],
},
csvExportParams={
"allColumns": True,
"columnSeparator": ",",
"fileName": csv_filename,
"skipColumnGroupHeaders": True,
"skipColumnHeaders": False,
"skipPinnedBottom": True,
"skipPinnedTop": True,
"skipRowGroups": True,
"suppressQuotes": False,
},
)
# Sample data for warehouses - used for the dropdown options
warehouse_options = [
{"value": "wh1", "label": "Warehouse 1 - NYC"},
{"value": "wh2", "label": "Warehouse 2 - LA"},
{"value": "wh3", "label": "Warehouse 3 - Chicago"},
{"value": "wh4", "label": "Warehouse 4 - Houston"},
{"value": "wh5", "label": "Warehouse 5 - Miami"},
]
# Initial row data - sample records that will be displayed in the grid
initial_row_data = [
{
"delete": "",
"id": 1,
"product": "Laptop",
"quantity": 25,
"warehouse_id": "wh1",
"price": 999.99,
},
{
"delete": "",
"id": 2,
"product": "Smartphone",
"quantity": 50,
"warehouse_id": "wh2",
"price": 499.99,
},
{
"delete": "",
"id": 3,
"product": "Tablet",
"quantity": 15,
"warehouse_id": "wh3",
"price": 349.99,
},
{
"delete": "",
"id": 4,
"product": "Monitor",
"quantity": 30,
"warehouse_id": "wh4",
"price": 199.99,
},
{
"delete": "",
"id": 5,
"product": "Keyboard",
"quantity": 100,
"warehouse_id": "wh5",
"price": 49.99,
},
]
# Initial column definitions - this defines the structure of our grid
def get_column_defs():
return [
# Delete column with DeleteButton renderer
{
"field": "delete",
"headerName": "",
"cellRenderer": "DeleteButton",
"lockPosition": "left",
"filter": False,
"maxWidth": 35,
"cellStyle": {"paddingRight": 0, "paddingLeft": 0},
},
# Warehouse selection column with SelectDMC editor
{
"field": "warehouse_id",
"headerName": "From Warehouse",
"initialWidth": 200,
"maxWidth": 200,
"editable": True,
"cellEditorPopup": True,
"singleClickEdit": True,
"cellEditor": {"function": "SelectDMC"},
"cellEditorParams": {
"options": warehouse_options,
"placeholder": "Select a warehouse",
"maxDropdownHeight": 280,
"creatable": False,
"clearable": True,
},
"valueFormatter": {"function": "getDisplayLabel(params)"},
},
# Other standard columns
{
"field": "id",
"headerName": "ID",
"filter": True,
},
{"field": "product", "headerName": "Product", "filter": True, "editable": True},
{
"field": "quantity",
"headerName": "Quantity",
"filter": True,
"editable": True,
},
{"field": "price", "headerName": "Price ($)", "filter": True, "editable": True},
]
# Define app layout
dash_app.layout = dmc.MantineProvider(
dbc.Container(
class_name="m-3",
children=dbc.Row(
dbc.Col(
dbc.Card(
[
dbc.CardHeader(
html.H1("AG Grid Demo with Dash and Flask"),
),
dbc.CardBody(
[
dbc.Row(
dbc.Col(
dbc.Alert(
"Click the button below to update the data and toggle the price column",
color="info",
),
),
class_name="m-3",
),
dbc.Row(
dbc.Col(
dbc.Button(
"Update Data & Toggle Column",
id="update-button",
color="success",
className="ms-auto",
),
),
class_name="m-3",
),
dbc.Row(
dbc.Col(
get_ag_grid(
id="grid",
rowData=initial_row_data,
columnDefs=get_column_defs(),
dashGridOptions={
"rowSelection": "multiple",
"suppressRowClickSelection": True,
"animateRows": True,
"popupParent": {
"function": "setBody()"
},
},
className="ag-theme-alpine",
style={
"height": "400px",
"width": "100%",
},
),
),
class_name="m-3",
),
dbc.Row(
dbc.Col(
class_name="m-3",
children=[
html.H3("Current Row Data:"),
html.Pre(
id="row-data-display",
style={
"backgroundColor": "#f5f5f5",
"padding": "10px",
"border": "1px solid #ddd",
"borderRadius": "5px",
"overflowX": "auto",
"whiteSpace": "pre-wrap",
},
),
],
),
class_name="m-3",
),
# # Store components for state management
# dcc.Store(
# id="row-data-store",
# data=initial_row_data,
# ),
# dcc.Store(
# id="column-defs-store",
# data=get_column_defs(),
# ),
]
),
]
),
)
),
)
)
@callback(Output("row-data-display", "children"), Input("grid", "rowData"))
def display_row_data(row_data):
"""
This callback formats and displays the current rowData as JSON
in the dedicated HTML div element.
"""
if row_data is None:
return "No data available"
return json.dumps(row_data, indent=2)
@callback(
Output("grid", "columnDefs"),
Input("update-button", "n_clicks"),
State("grid", "columnDefs"),
# State("column-defs-store", "data"),
prevent_initial_call=True,
)
def update_column_defs(n_clicks, current_column_defs):
"""
This callback toggles the visibility of the price column
when the update button is clicked.
"""
if n_clicks is None:
raise PreventUpdate
# updated_column_defs = get_column_defs()
updated_column_defs = current_column_defs.copy()
# Find the price column
for i, col in enumerate(updated_column_defs):
if col["field"] == "price":
# Toggle hide property (or set it if it doesn't exist)
updated_column_defs[i]["hide"] = not col.get("hide", False)
break
return updated_column_defs
@callback(
Output("grid", "rowData"),
# Output("row-data-store", "data"),
Input("update-button", "n_clicks"),
# State("row-data-store", "data"),
State("grid", "rowData"),
prevent_initial_call=True,
)
def update_row_data(n_clicks, current_data):
"""
This callback generates new row data when the update button is clicked.
It alternates between adding new products and updating quantities.
"""
if n_clicks is None:
return current_data
# Determine what type of update to perform based on click count
if n_clicks % 2 == 1: # Odd clicks: Add new products
new_products = [
# {
# "delete": "",
# "id": len(current_data) + 1,
# "product": "Headphones",
# "quantity": 35,
# "warehouse_id": "wh1",
# "price": 149.99,
# },
# {
# "delete": "",
# "id": len(current_data) + 2,
# "product": "Printer",
# "quantity": 10,
# "warehouse_id": "wh3",
# "price": 299.99,
# },
]
return current_data + new_products
else: # Even clicks: Update quantities
updated_data = current_data.copy()
for item in updated_data:
# Increase quantity by a random amount (10-50)
import random
item["quantity"] += random.randint(10, 50)
return updated_data
# @callback(Output("grid", "rowData"), Input("row-data-store", "data"))
# def update_grid_data(row_data):
# """
# This callback updates the grid with the current rowData from the store.
# This is necessary because we need to update the grid when the store changes.
# """
# return row_data
if __name__ == "__main__":
app.run(debug=True, host="0.0.0.0", port=4999)