Deleting Rows in Dash AG Grid

Deleting Rows in Dash AG Grid

This question came up in another topic, so I decided to make it a separate post.

Here are several different ways to delete rows in Dash AG Grid. This post will demo:

  1. Using the deleteSelectedRows prop in a callback
  2. Creating a column of delete buttons ( like in DataTable)
  3. Using rowTransactions prop in a callback.

1. Delete selected rows button

This method uses a button to delete selected rows in a callback, It makes it easy for the user to delete multiple rows. See the the post on all the different ways you can select rows in Dash AG Grid.

You simply create a grid with selectable rows, then have a button that triggers a callback to sent to the deleteSelectedRows prop to True.

ag-grid-delete-selected-rows




import dash_ag_grid as dag
from dash import Dash, html,  Input, Output,  callback
import pandas as pd

app = Dash(__name__)


df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)


columnDefs = [
    {"field": "athlete", "checkboxSelection": True, "headerCheckboxSelection": True},
    {"field": "age", "maxWidth": 100},
    {"field": "country"},
    {"field": "year", "maxWidth": 120},
    {"field": "date"},
    {"field": "sport"},
    {"field": "total"},
]

defaultColDef = { "flex": 1, "minWidth": 150, "sortable": True, "resizable": True, "filter": True}


app.layout = html.Div(
    [
        html.Button("Delete Selected", id="button"),
        dag.AgGrid(
            id="grid",
            columnDefs=columnDefs,
            rowData=df.to_dict("records"),
            defaultColDef=defaultColDef,
            dashGridOptions={"rowSelection":"multiple"},
        ),
    ],
    style={"margin": 20},
)


@callback(
    Output("grid", "deleteSelectedRows"),
    Input("button", "n_clicks"),
    prevent_initial_call=True
)
def selected(_):
    return True


2. Delete button component in a cell (like Dash DataTable)

See more information on using components in cells in the Dash AG Grid docs

ag-grid-delete-row-like-datatable


import dash_ag_grid as dag
from dash import Dash, html
import pandas as pd

app = Dash(__name__)

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)
df=df.head(10)

columnDefs = [
    {
        "headerName": "",
        "cellRenderer": "DeleteButton",
        "lockPosition":'left',
        "maxWidth":35,
        "filter": False,
        'cellStyle': {'paddingRight': 0, 'paddingLeft': 0},
    },
    {"field": "athlete", "checkboxSelection": True, "headerCheckboxSelection": True},
    {"field": "age", "maxWidth": 100},
    {"field": "country"},
    {"field": "year", "maxWidth": 120},
    {"field": "date"},
    {"field": "sport"},
    {"field": "total"},
]

defaultColDef = { "sortable": True, "resizable": True, "filter": True}

app.layout = html.Div(
    [
        dag.AgGrid(
            id="grid",
            columnDefs=columnDefs,
            rowData=df.to_dict("records"),
            defaultColDef=defaultColDef,
            dashGridOptions={"rowSelection": "multiple", "suppressRowClickSelection": True},
        ),
    ],
    style={"margin": 20},
)

if __name__ == "__main__":
    app.run(debug=True)

"""

Place the following in the dashAgGridComponentFunctions.js file in the assets folder

---------------

var dagcomponentfuncs = window.dashAgGridComponentFunctions = window.dashAgGridComponentFunctions || {};


dagcomponentfuncs.DeleteButton = function (props) {
    function onClick() {
          props.api.applyTransaction({ remove: [props.node.data] })
    }
    return React.createElement('button', {onClick}, "X");
};


"""

3. Delete Rows in a callback using rowTransaction

An efficient way to update data in the grid (including deleting rows) is to use rowTransaction

The rows must have row ids and It’s only necessary to specify the ids to delete.

This example shows how to delete rows when the grid does not have selectable rows. To illustrate, it uses a button to delete all rows where the year is 2012.


import dash_ag_grid as dag
from dash import Dash, html, Input, Output, callback
import pandas as pd

app = Dash(__name__)

df = pd.read_csv(
    "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
)
df["id"] = df.index

defaultColDef = {"flex": 1, "minWidth": 150, "sortable": True, "resizable": True,  "filter": True}

app.layout = html.Div(
    [
        html.Button("Delete 2012", id="year"),
        html.Button("Undo", id="undo"),
        dag.AgGrid(
            id="grid",
            columnDefs=[{"field": i} for i in df.columns],
            rowData=df.to_dict("records"),
            defaultColDef=defaultColDef,
            getRowId="params.data.id",
        ),
    ],
    style={"margin": 20},
)

@callback(
    Output("grid", "rowTransaction"),
    Input("year", "n_clicks"),
    prevent_intial_call=True,
)
def delete_rows(_):
    dff = df[df['year'] == 2012]

    # it's only necessary to include the row ids
    ids = dff[["id"]]
    return {"remove": ids.to_dict("records")}


@callback(
    Output("grid", "rowData"),
    Input("undo", "n_clicks"),
    prevent_intial_call=True,
)
def undo(_):
   return df.to_dict("records")


if __name__ == "__main__":
    app.run(debug=True)

7 Likes

Hey @AnnMarieW , love the examples here :slight_smile:

I implemented the one with the button on the row and it works great for updating the table, however it doesn’t seem to be updating any Dash properties to trigger a callback when the row is removed. Do you have an idea of how to go about it?

1 Like

Hello @RenaudLN,

What happens if you pass “async”: False in the rowTransaction? Does the rowData update in dash?

I tried the following but no update to rowData

props.api.applyTransaction({ remove: [props.node.data], async: false })

Using the underlying api with that is going to be sync, to be async it would be applyTransactionAsync, we do this in the rowTransaction by using the keyword async which defaults to True.

The other thing that will update is virtualRowData. Issue here is that it updates for other reasons as well.

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)

@seanrez
Use the virtualRowData prop rather than rowData to track the changes.

1 Like

What happens if you instead use rowTransaction? And use the clientside set_props?

I’ve found that I am able to delete rows successfully only when using applyTransactionAsync.

The rowTransaction prop and applyTransaction both fail to update the state correctly when using row ids (my specific use case is with tree data).

It needs to be used in a clientside callback. e.g.


clientside_callback(
      """
      (_, filter_rows, filterTableId) => {
        const api = dash_ag_grid.getApi(filterTableId);
        api.applyTransactionAsync({
          add: [ ... ],
        });
        return dash_clientside.no_update;
      };
      """,
      Output(ids.filter_table(MATCH), "id", allow_duplicate=True),
      Input(ids.add_filter(MATCH), "n_clicks"),
      State(ids.filter_table(MATCH), "rowData"),
      State(ids.filter_table(MATCH), "id"),
      prevent_initial_call=True,
)
1 Like

rowTransaction is set to apply the same mechanism as applyTransactionAsync, this should be defaulting to use applyTransactionAsync by default and you need to pass async: False in order to not apply this.