AG Grid clientside callback

I’m trying to convert a working callback to a clientside callback for updating the columnState for an AG-Grid table.

The goal is to update a ranking column based on user input. I do this by showing/hiding the appropriate ranking column and setting the sort in the columnState.

Here’s a basic working example that dumps the columnState to the page for debugging:

import json

import dash_ag_grid as dag
from dash import Dash, Input, Output, State, callback, clientside_callback, dcc, html

app = Dash(__name__)

dat = [
    {"fruit": "apple", "bob_rank": 1, "alice_rank": 3},
    {"fruit": "orange", "bob_rank": 2, "alice_rank": 4},
    {"fruit": "banana", "bob_rank": 3, "alice_rank": 2},
    {"fruit": "pear", "bob_rank": 4, "alice_rank": 1},
]

columnDefs = [
    {"field": "bob_rank"},
    {"field": "alice_rank", "hide": True},
    {"field": "fruit"},
]

app.layout = html.Div(
    [
        html.Div(dcc.RadioItems(id="rank-select", options=["bob", "alice"], value="bob")),
        dag.AgGrid(
            id="table",
            rowData=dat,
            columnDefs=columnDefs,
        ),
        html.Div(html.Pre(id="output")),
    ]
)

@callback(
    Output("output", "children"),
    Input("table", "columnState"),
)
def output(state):
    return json.dumps(state, indent=2)

@callback(
    Output("table", "columnState"),
    Input("rank-select", "value"),
    State("table", "columnState"),
)
def update_sort_state(rank, state):
    if state is not None:
        for col in state:
            sort = None
            hide = col["hide"]
            if "rank" in col["colId"]:
                hide = True
            if col["colId"] == f"{rank}_rank":
                sort = "asc"
                hide = False
            col.update({"sort": sort, "hide": hide})
    return state

If I convert the callback to a clientside callback it no longer works, but the columnState seems to be getting updated just like it does using the regular callback.

clientside_callback(
    """
    function(rank, state) {
        if (state) {
            for (var i = 0; i < state.length; i++) {
                col = state[i]
                sort = null
                hide = col['hide']
                if (col['colId'].includes('rank')) {hide = true}
                if (col['colId'] == rank + '_rank') {
                    sort = 'asc'
                    hide = false
                }
                update = {'sort': sort, 'hide': hide}
                state[i] = {...col, ...update}
            }
        }
        return state
    }
    """,
    Output("table", "columnState"),
    Input("rank-select", "value"),
    State("table", "columnState"),
)

I can’t figure out the difference that makes the regular callback work, but the clientside callback not work. Ideas?

Hello @cheby,

Welcome to the community!

This is due to the nature at which the variable is present when on JS, the variable is being manipulated in place. When this happens, the dash renderer doesnt recognize that there were changes made to the variable.

To get around that issue, you can use this:

clientside_callback(
    """
    function(rank, state) {
        if (state) {
            newState = JSON.parse(JSON.stringify(state))
            for (var i = 0; i < state.length; i++) {
                col = newState[i]
                sort = null
                hide = col['hide']
                if (col['colId'].includes('rank')) {hide = true}
                if (col['colId'] == rank + '_rank') {
                    sort = 'asc'
                    hide = false
                }
                update = {'sort': sort, 'hide': hide}
                newState[i] = {...col, ...update}
            }
        }
        return newState
    }
    """,
    Output("table", "columnState"),
    Input("rank-select", "value"),
    State("table", "columnState"),
)

Using JSON.parse(JSON.stringify()) breaks the link and creates a new variable.