Dash AgGrid : Dynamic Cell Dropdown Options (from server callback)

I’m trying to set the options of the Select Editor (dropdown) each time i access a cell dropdown editor.

The rowMenu seemed good to start from, but a rowMenu does not display the value of the cell.

import random, json

import dash
from dash import Dash, html, dcc, Input, Output, State, Patch, ctx, MATCH, ALL
import dash_ag_grid as dag

app = Dash(__name__)
columnDefs = [
    {"field": "A"},
    {
        "field": "dynamic_select", 
        "editable": True,
        "cellEditor": "agSelectCellEditor",
        "cellEditorParams": {
            "values": [f'set@starup{i}' for i in range(4)],
        },
    },
]
rowData = [
    {"A": "a1", "dynamic_select": "--"},
    {"A": "a2", "dynamic_select": "--"},
    {"A": "a3", "dynamic_select": "--"},
]

app.layout = html.Div(
    [
        dag.AgGrid(
            id="table",
            columnDefs=columnDefs,
            rowData=rowData,
        ),
        html.Div(id="dynamic_dropdown_option_output"),
    ],
    style={"margin": 20},
)

@app.callback(
    Output("dynamic_dropdown_option_output", "children"),
    Input("table", "cellDoubleClicked"),
    prevent_initial_call=True,
    )
def set_dynamic_dropdown_editor_options(cell_data):
    # simulate bogus ever-changing dropdown options
    # set options to the Select Editor Dropdown in somne way
    return [f'  server_query_{random.randint(0,9)}' for i in range(random.randint(1,7))]

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

So I started with a more generic MRE.

Does anyone know how to achieve this?

Thx

I also tried to do somekind of hack with the rowMenu cellRenderer in the column.

I change the menu options by updating the menu cell value via a rowTransaction (update)

But no change in the rowMenu cellRenderer dropdown…

import random, json
import dash_ag_grid as dag
import dash
from dash import Dash, html, dcc, Input, Output, State, Patch, ctx, MATCH, ALL

app = dash.Dash(__name__)

columnDefs = [
    {"headerName": "Make", "field": "make", "sortable": True},
    {"headerName": "Model", "field": "model"},
    {"headerName": "Price", "field": "price"},
    {"headerName": "Menu", "field": "menu", "cellRenderer": "rowMenu"},
]

rowData = [
    {
        "make": "Toyota",
        "model": "Celica",
        "price": 35000,
        "menu": [
            {"label": "Option 1", "value": 1},
            {"label": "Option 2", "value": 2},
            {"label": "Option 3", "value": 3},
        ],
    },
    {
        "make": "Ford",
        "model": "Mondeo",
        "price": 32000,
        "menu": [
            {"label": "Option 4", "value": 4},
            {"label": "Option 5", "value": 5},
            {"label": "Option 6", "value": 6},
        ],
    },
    {
        "make": "Porsche",
        "model": "Boxster",
        "price": 72000,
        "menu": [
            {"label": "Option 7", "value": 7},
            {"label": "Option 8", "value": 8},
            {"label": "Option 9", "value": 9},
        ],
    },
]


grid = dag.AgGrid(
    id="cellrenderer-grid",
    columnSize="sizeToFit",
    getRowId="params.data.make",
    columnDefs=columnDefs,
    rowData=rowData,
)

app.layout = html.Div(
    [
        dcc.Markdown("DOUBLECLICK IN THE MENU COLUMN !! next to the Button to UPDATE MENU Options"),
        grid,
        html.P("SET_DROPDOWN_OPTIONS"),
        html.P(id="SET_DROPDOWN_OPTIONS_output"),
        html.P(" "),
        html.P("dropdown_output"),
        html.P(id="dropdown_output"),

        html.P(id="cellrenderer-data"),
    ],
    style={"margin": 20},
)

@app.callback(
    Output("cellrenderer-grid", "rowTransaction"),
    Output("SET_DROPDOWN_OPTIONS_output", 'children'),
    Input("cellrenderer-grid", "cellDoubleClicked"), 
    State("cellrenderer-grid", "virtualRowData"),
    prevent_initial_call=True,
    )
def set_dynamic_dropdown_editor_options(cell_data, rows_data):
    row_data = rows_data[cell_data['rowIndex']]
    row_data['menu'] = [f'server_query_{random.randint(0,9)}' for i in range(random.randint(1,7))]

    print(f'>>> SET MENU OPTIONS : {row_data["menu"] }')
    return {'update' : row_data} , row_data['menu'] 


@app.callback(
    Output("dropdown_output", "children"),
    Input("cellrenderer-grid", "cellRendererData"), 
    prevent_initial_call=True,   
)
def show_click_data(data):     
    if data:
        return (
            "You selected option {} from the colId {}, rowIndex {}, rowId {}.".format(
                data["value"],
                data["colId"],
                data["rowIndex"],
                data["rowId"],
            )
        )
    return "No menu item selected."


if __name__ == "__main__":
    app.run_server(debug=False)

PS : rowMenu cellRenderer won’t do the trick anyway because one cannot see the value in the cell.
Instead, there is a big ‘+’ button…

Hello @popo,

This isnt exactly a walk in the park, it uses quite a bit of advanced knowledge of flask, JS and the grid:

app.py:

import random, json

import dash
from dash import Dash, html, dcc, Input, Output, State, Patch, ctx, MATCH, ALL
import dash_ag_grid as dag
from flask import jsonify, request
import dash_mantine_components

app = Dash(__name__)
columnDefs = [
    {"field": "A"},
    {
        "field": "dynamic_select",
        "editable": True,
        "cellEditor": {'function': "DMC_Select2"},
        "cellEditorPopup": True
    }
]
rowData = [
    {"A": "a1", "dynamic_select": "--"},
    {"A": "a2", "dynamic_select": "--"},
    {"A": "a3", "dynamic_select": "--"},
]

app.layout = html.Div(
    [
        dag.AgGrid(
            id="grid",
            columnDefs=columnDefs,
            rowData=rowData,
        ),
        html.Div(id="dynamic_dropdown_option_output"),
    ],
    style={"margin": 20},
)


@app.server.route('/dynamicOptions', methods=['POST'])
def dynamicOptions():
    print(request.json)
    return jsonify([f'  server_query_{random.randint(0,9)}' for i in range(random.randint(1,7))])

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

dashAgGridFunctions.js

var dagfuncs = window.dashAgGridFunctions = window.dashAgGridFunctions || {};

var dropdownRequest = async (params) => {
    resp = await fetch('./dynamicOptions', {method: 'POST', headers: {'content-type': 'application/json'}, 'body': JSON.stringify(params.node.data)})
    .then(resp => resp.json())
    .then((data) => {return data})
    return resp
}

dagfuncs.DMC_Select2 = class {
    // gets called once before the renderer is used
    init(params) {
        // create the cell
        this.params = params;

        // function for when Dash is trying to send props back to the component / server
        var setProps = (props) => {
            if (typeof props.value != typeof undefined) {
                // updates the value of the editor
                this.value = props.value;

                // re-enables keyboard event
                delete params.colDef.suppressKeyboardEvent;

                // tells the grid to stop editing the cell
                params.api.stopEditing();

                // sets focus back to the grid's previously active cell
                this.prevFocus.focus();
            }
        };
        this.eInput = document.createElement('div');

        // renders component into the editor element - dynamically requests data from the server
        dropdownRequest(params).then((data) => {
        ReactDOM.render(
            React.createElement(window.dash_mantine_components.Select, {
                data: data,
                value: params.value,
                setProps,
                style: {width: params.column.actualWidth-2,  ...params.style},
                className: params.className,
                clearable: params.clearable,
                searchable: params.searchable || true,
                creatable: params.creatable,
                debounce: params.debounce,
                disabled: params.disabled,
                filterDataOnExactSearchMatch:
                    params.filterDataOnExactSearchMatch,
                limit: params.limit,
                maxDropdownHeight: params.maxDropdownHeight,
                nothingFound: params.nothingFound,
                placeholder: params.placeholder,
                required: params.required,
                searchValue: params.searchValue,
                shadow: params.shadow,
                size: params.size,
                styles: params.styles,
                switchDirectionOnFlip: params.switchDirectionOnFlip,
                variant: params.variant,
            }),
            this.eInput
        );
        })

        // allows focus event
        this.eInput.tabIndex = '0';

        // sets editor value to the value from the cell
        this.value = params.value;
    }

    // gets called once when grid ready to insert the element
    getGui() {
        return this.eInput;
    }

    focusChild() {
        // needed to delay and allow the component to render
        setTimeout(() => {
            var inp = this.eInput.getElementsByClassName(
                'mantine-Select-input'
            )[0];
            inp.tabIndex = '1';

            // disables keyboard event
            this.params.colDef.suppressKeyboardEvent = (params) => {
                const gridShouldDoNothing = params.editing;
                return gridShouldDoNothing;
            };
            // shows dropdown options
            inp.focus();
        }, 100);
    }

    // focus and select can be done after the gui is attached
    afterGuiAttached() {
        // stores the active cell
        this.prevFocus = document.activeElement;

        // adds event listener to trigger event to go into dash component
        this.eInput.addEventListener('focus', this.focusChild());

        // triggers focus event
        this.eInput.focus();
    }

    // returns the new value after editing
    getValue() {
        return this.value;
    }

    // any cleanup we need to be done here
    destroy() {
        // sets focus back to the grid's previously active cell
        this.prevFocus.focus();
    }
};

image

2 Likes

Wow!! That is pretty advanced stuff there @jinnyzor (greatness!)
Seems to go deep into the Dash/JS nuts and bolts.

I have no experience with Flask and my head is still spinning, but I kinda half-get it.
Correct me if I’m wrong…

  • dashmantine is imported on the server (python) so that dashmantine-components are ‘known’ in JSScripts
  • the cellEditor calls the JS DMC_Select2() on edit cell
  • the DMC_Select2 inits the dropdownRequest that communicates with the server via python def dynamicOptions (in some ‘magical’ way)
  • JS setProps() is triggered (onChange?) sets the final value of the cell to the one chosen in the dropdown.

I added a callback on cellValueChanged callback for completeness…

### ADD @jinnyzor dashAgGridFunctions.js  IN THE ASSETS FOLDER
import random, json

import dash
from dash import Dash, html, dcc, Input, Output, State, Patch, ctx, MATCH, ALL
import dash_ag_grid as dag
import dash_mantine_components

from flask import jsonify, request


app = Dash(__name__)
columnDefs = [
    {"field": "A"},
    {
        "field": "dynamic_select",
        "editable": True,
        "cellEditor": {'function': "DMC_Select2"},
        "cellEditorPopup": True
    }
]
rowData = [
    {"A": "a1", "dynamic_select": "--"},
    {"A": "a2", "dynamic_select": "--"},
    {"A": "a3", "dynamic_select": "--"},
]

app.layout = html.Div(
    [
        dag.AgGrid(
            id="grid",
            columnDefs=columnDefs,
            rowData=rowData,
        ),
        html.Div(id="dynamic_dropdown_option_output"),
    ],
    style={"margin": 20},
)


@app.server.route('/dynamicOptions', methods=['POST'])
def dynamicOptions():
    print(request.json)
    return jsonify([f'  server_query_{random.randint(0,9)}' for i in range(random.randint(1,7))])



@app.callback(
    Output("dynamic_dropdown_option_output", "children"),
    Input("grid", "cellValueChanged"), 
    prevent_initial_call=True,   
)
def after_dropdown_selection(data):     
    if data:
        return (
            "You selected option {} from the colId {}, rowIndex {}, rowId {}.".format(
                data["value"],
                data["colId"],
                data["rowIndex"],
                data["rowId"],
            )
        )
    return "No menu item selected."


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

What I do not understand is

  • where the JS params comes from?
    What if I want to pass other params than data to the control
    Eg. searchable/clearable/creatable/placeholder/error/… etc?

PS1 : Does this dynamicOptions/dropdownRequest thingy also work with dcc or bootstrap dropdowns? or other Dash components / React Components? To get server access ‘before’ anything gets handled?
I’ve had issues switching Mantine with Bootstrap dropdowns. They don’t seem to trigger/behave the same way (something with setProps/onChange/…)

PS2 : Is this something we may see implemented in future versions of Dash Aggrid? (cfr rowMenu’s)

Haha, yes @popo, most of those statements are correct.

yes

yes

It sends a fetch requests which is then used to populate the data argument of the dmc Select

setProps is a standard for all Dash components, its how you can update the properties in the component and also trigger callbacks from it.

The init(params) comes from the cellEditor in the grid, you can find more info here:

I then passed these params down to the request so that you can get row level info in the request to the server.

You can customize this however you want, if you want the info from the server for the component, then you can just adjust the response from the server where you return the jsonify.

PS1 - Yes, you can use any component, it doesnt matter, I just had the DMC_Select already made (without your modification). You can even create a generic component (browser select, etc) The main thing is to make sure you dont render the component until you have received a response from the server.

PS2 - No, I dont think this is really in the scope of a wrapper of the underlying Grid. And not really sure of the use-case for this scenario.

1 Like