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

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