Dash AG Grid async options

hi there, i am trying to update options for either dcc dropdown or dmc select within an ag grid cell for a column

columnDefs = [
{
“field”: “column1”,
“editable”: True,
“checkboxSelection”: True,
“cellEditor”: {“function”: “DCC_Dropdown”},
# “cellEditor”: {“function”: “DMC_Select”},
“cellEditorPopup”: True,
“filter”: True,
}]

I’ve taken a look at both DCC_Dropdown and DMC_Select in the plotly ag grid github and tried to define a async function called updateOptions() that takes in the searchvalue and gets a return json and sets this.options but the component doesnt rerender upon this.options having new data. I am unfortunately not super familiar with javascript/react and was wondering if someone could take a look.

I tried modifying cellEditorParams to be {“values”: “updateOptions(params)”) as that works in this sandbox i was testing out (Plunker - Dynamic Parameters) but within my dash app the function doesnt even seem to be called.

  • getGenders() is an async function that returns [‘Male’, ‘Female’] after a timeout. I tried to do something similar in dash ag grid’s celleditorparams and it didnt work

im using :

dash 2.13.0
dash-mantine-components 0.12.1
dash-ag-grid 2.4.0

and both DMC_Select and DCC_Dropdown im using are in
https://github.com/plotly/dash-ag-grid/blob/c12fc22e1278b09afeeb4a5ae5c4e3bbbebb35f6/docs/examples/components/assets/dashAgGridFunctions.js#L259

Hello @aKib,

Welcome to the community!

You are trying to provide options based upon an async function/server call?

You can check out this topic here for how I was able to do this:

If you want to, you can also make cellEditorParams a whole function and return the data however you want using JS functions, etc.

1 Like

Hi @jinnyzor ,

Thank you for the welcome! Yes, I am trying to provide options based on async function/server call based on search value, how would I achieve that ? Within setProps I could set this.search_value to be whats the component passes back, but how would i initiate the componenet rerender on each search value change?

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();


            }
            if (props.search_value !== '') {
                this.search_value = props.search_value;
            }
        };

//something akin to this?
dropdownRequest(this.search_value).then((data) => { ... });

For this, you could possibly use something like useEffect on the this.search_value and force it to rerender.

Give me a bit and I might be able to come up with something to allow you some options.

Do you have a small example app I can try on?

a small example app could be something like this (sorry for the bareness, scraped something togheter)

app.py


import dash
from dash import html, dcc
import pandas as pd
from dash.dependencies import Input, Output
import dash_ag_grid as dag
import dash_mantine_components as dmc

# Create a DataFrame
df = pd.DataFrame({
    'Name': ['Person' + str(i) for i in range(20)],
    'favorite_word': ["" for _ in range(20)],
})

# Initialize the Dash app
app = dash.Dash(__name__)

# Define the layout of the app
app.layout = html.Div([
    dag.AgGrid(
        id='table',
        rowData=df.to_dict('records'),
        columnDefs=[{"headerName": i, "field": i,
                     "cellEditor": {"function": "DMC_Select"},
                     "editable": True,
                     "cellEditorParams": {"creatable": True, "searchable": True},
                        "cellEditorPopup": True,
                     } for i in df.columns],
        defaultColDef={'flex': 1},
    )
])

# Run the app
if __name__ == '__main__':
    app.run_server(debug=True, port=8080)

dashAgGridFunctions.js


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

const [useRef, useState, Component, forwardRef] = [React.useRef, React.useState, React.Component, React.forwardRef]

dagfuncs.dynamicOptions = async function (search) {

    try {
        let response = await fetch(`https://random-word-form.repl.co/random/noun/${search}?count=10`, {
            method: "GET",
            credentials: "include",
            headers: {
                "Content-Type": "application/json",
            }
        });
        if (!response.ok) {
            console.log(`HTTP error: ${response.status}`);
        }
        const responseData = await response.json();
        return responseData;
    } catch (e) {
        console.log(e);
    }
}

//... DMC Select the same as before

Hey @aKib,

If you are trying to limit search results, you do know that dmc can do this natively?

I’m not exactly sure about this use-case for tying it to the searchValue… XD

ah sorry, i was trying to give an example as the endpoint im reaching is internal, the workflow is basically like theres a total number of values i can get > 200k, therefore upon search_value change, it sends the search value to another endpoint and gets back json of the filtered results for me (no filtering done on the dash app)

Ah, ok.

Wanted to make there there wasnt already a way to accomplish this without performing a call like this. :wink:

This should get you started:

dynamicOptions = async function (search) {
    try {
        let response = await fetch('./dynamicOptions', {method: 'POST', headers: {'content-type': 'application/json'}, body: JSON.stringify({'searchValue':search})})
        if (!response.ok) {
            console.log(`HTTP error: ${response.status}`);
        }
        const responseData = await response.json();
        return responseData;
    } catch (e) {
        console.log(e);
    }
}

dagfuncs.DMC_Select2 = class {

    // gets called once before the renderer is used
    init(params) {
        var options = [];

        // 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();
            }
            if (typeof props.searchValue != typeof undefined) {
                dynamicOptions(props.searchValue).then((opts) =>
                    ReactDOM.render(
                        React.createElement(window.dash_mantine_components.Select, {
                            data: opts,
                            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
                        )
                )
            }
        };
        this.eInput = document.createElement('div');

        ReactDOM.render(
        React.createElement(window.dash_mantine_components.Select, {
            data: options,
            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();
    }
};
import dash
from dash import html, dcc
import pandas as pd
from dash.dependencies import Input, Output
import dash_ag_grid as dag
import dash_mantine_components as dmc
import requests
from flask import request

# Create a DataFrame
df = pd.DataFrame({
    'Name': ['Person' + str(i) for i in range(20)],
    'favorite_word': ["" for _ in range(20)],
})

# Initialize the Dash app
app = dash.Dash(__name__)

# Define the layout of the app
app.layout = html.Div([
    dag.AgGrid(
        id='table',
        rowData=df.to_dict('records'),
        columnDefs=[{"headerName": i, "field": i,
                     "cellEditor": {"function": "DMC_Select2"},
                     "editable": True,
                     "cellEditorParams": {"creatable": True, "searchable": True},
                        "cellEditorPopup": True,
                     } for i in df.columns],
        defaultColDef={'flex': 1},
    )
])

@app.server.route('/dynamicOptions', methods=['POST'])
def dynamicOptions():
    try:
        url = f'https://random-word-form.repl.co/random/noun/{request.json.get("searchValue")}?count=10'
        resp = requests.get(url).json()
        return resp
    except:
        return []

# Run the app
if __name__ == '__main__':
    app.run_server(debug=True, port=8080)
2 Likes

ohhh so the secret sauce was to rerender it within set props. Amazing, thank you so much!

1 Like

Correct, the useEffect wasn’t working in this case. They work when using the forwardRef method, not the class.