Using dash_core_components Dropdown as an AG Grid cell editor

Continuing the discussion from :mega: Dash AG Grid – Update 2.0.0rc1 Pre Release is Now Available **:

So far, I’ve been able to use React.createElement(window.dash_core_components.Dropdown) inside a custom component and use that as a renderer.

However it really should be an editor but I don’t know if this is working, when I take the NumericInput Cell editor example and try to put the dropdown in for the eInput I get an error “”“Failed to execute ‘appendChild’ on ‘Node’: parameter 1 is not of type ‘Node’.”“”

Is this a bug or am I trying to do something impossible?

Hello @alistair.welch,

I am testing it out as well.

This seems to be a limitation of how the functions work in AG Grid works.

Please note, cellRenderer has the params.node.setDataValue in which you can update straight from the renderer. For dropdowns, currently I would recommend turning edit: False and then use the setProps to get the params.value and set the value to that.

Thanks for looking into it!

Am I doing something wrong here?

dagcomponentfuncs.DropdownInput = function (props) {
    const setDataValue = props.node.setDataValue;
    function setProps(params) {
        setDataValue(params.value);
    };
    return React.createElement(
            window.dash_core_components.Dropdown,
            {
                setProps,
                value: props.value,
                options: props.options,
            }
        )
}

You need to pass the colId.

dagcomponentfuncs.DropdownInput = function (props) {
    const setDataValue = props.node.setDataValue;
    const colId = props.column.colId
    function setProps(params) {
        setDataValue(colId, params.value);
    };
    return React.createElement(
            window.dash_core_components.Dropdown,
            {
                setProps,
                value: props.value,
                options: props.options,
            }
        )
}

Try that.

No luck. I think I got it though!

dagcomponentfuncs.DropdownInput = function (props) {
    const setValue = props.setValue;
    function setProps(params) {
        if (params.value){
            setValue(params.value);
        };
    }
    return React.createElement(
            window.dash_core_components.Dropdown,
            {
                setProps,
                value: props.value,
                options: props.options,
            }
        )
}

Now I just need to clean up the format a bit and fingers crossed that’ll be it.

So far I’m using:
dashGridOptions={'suppressRowTransform': True}
and

.ag-cell {
    overflow: visible !important
}
.Select-control {
    overflow: visible !important
}

just for people’s reference.

Thank you loads @jinnyzor!

2 Likes

Hello @alistair.welch,

Check this out for the editor:

dagfuncs.DCC_Dropdown = class {

    // gets called once before the renderer is used
  init(params) {
    // create the cell
    this.params = params;
    this.ref = React.createRef();

    // function for when Dash is trying to send props back to the component / server
    var setProps = (props) => {
        if (props.value) {
            // 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
    ReactDOM.render(React.createElement(window.dash_core_components.Dropdown, {
        options: params.values, value: params.value, ref: this.ref, setProps, style: {width: params.column.actualWidth},
    }), 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() {
    // enter keyboard event
    const keyboardEvent = new KeyboardEvent('keydown', {
        code: 'Enter',
        key: 'Enter',
        charCode: 13,
        keyCode: 13,
        view: window,
        bubbles: true
    });
    
    // needed to delay and allow the component to render
    setTimeout(() => {
        var inp = this.eInput.getElementsByClassName('Select-control')[0]
        inp.tabIndex = '1'
        inp.focus()

        // disables keyboard event
        this.params.colDef.suppressKeyboardEvent = (params) => {
               const gridShouldDoNothing = params.editing
               return gridShouldDoNothing;
           }
        // shows dropdown options
        inp.dispatchEvent(keyboardEvent)
    }, 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();
  }
}

It will automatically open up to show all the available options. I am sure there is some customization to how you want to use it.


app.py:

"""
Conditional Select options
"""

import dash_ag_grid as dag
from dash import Dash, html, dcc

app = Dash(__name__)


columnDefs = [
    {
        "field": "country",
        "editable": False,
    },
    {
        "headerName": "Select Editor",
        "field": "city",
        "cellEditor": {"function": "DCC_Dropdown"},
        "cellEditorParams": {"function": "dynamicOptions(params)"},
        "cellEditorPopup": True,
        "cellEditorPopupPosition": 'under',
    },

]

rowData = [
    {"country": "United States", "city": "Boston"},
    {"country": "Canada", "city": "Montreal"},
    {"country": "Canada", "city": "Vancouver"},
]


app.layout = html.Div(
    [
        dcc.Markdown(
            "This grid has dynamic options for city based on the country.  Try editing the cities."
        ),
        dag.AgGrid(
            id="cell-editor-grid",
            columnDefs=columnDefs,
            rowData=rowData,
            columnSize="sizeToFit",
            defaultColDef={"editable": True},
            dashGridOptions={'suppressRowTransform': True}
        ),
    ],
    style={"margin": 20},
)


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

functions:

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

dagfuncs.dynamicOptions = function(params) {
    const selectedCountry = params.data.country;
    if (selectedCountry === 'United States') {
        return {
            values: ['Boston', 'Chicago', 'San Francisco'],
        };
    } else {
        return {
            values: ['Montreal', 'Vancouver', 'Calgary']
        };
    }
}

dagfuncs.DCC_Dropdown = class {

    // gets called once before the renderer is used
  init(params) {
    // create the cell
    this.params = params;
    this.ref = React.createRef();

    // function for when Dash is trying to send props back to the component / server
    var setProps = (props) => {
        if (props.value) {
            // 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
    ReactDOM.render(React.createElement(window.dash_core_components.Dropdown, {
        options: params.values, value: params.value, ref: this.ref, setProps, style: {width: params.column.actualWidth},
    }), 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() {
    // enter keyboard event
    const keyboardEvent = new KeyboardEvent('keydown', {
        code: 'Enter',
        key: 'Enter',
        charCode: 13,
        keyCode: 13,
        view: window,
        bubbles: true
    });
    
    // needed to delay and allow the component to render
    setTimeout(() => {
        var inp = this.eInput.getElementsByClassName('Select-control')[0]
        inp.tabIndex = '1'
        inp.focus()

        // disables keyboard event
        this.params.colDef.suppressKeyboardEvent = (params) => {
               const gridShouldDoNothing = params.editing
               return gridShouldDoNothing;
           }
        // shows dropdown options
        inp.dispatchEvent(keyboardEvent)
    }, 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();
  }
}
3 Likes

Wow, yeah that seems even cleaner. Thanks! I was going to work on making any search value add to the options, so it acts like a suggestion rather than forced dropdown options. I’ve got a callback for this already so going to try that next and that should help me familiarise with how to send data to a prop and therefore a callback next

1 Like

Here, made it better:

dagfuncs.DCC_Dropdown = class {

    // gets called once before the renderer is used
  init(params) {
    // create the cell
    this.params = params;
    this.ref = React.createRef();

    // function for when Dash is trying to send props back to the component / server
    var setProps = (props) => {
        if (props.value) {
            // 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
    ReactDOM.render(React.createElement(window.dash_core_components.Dropdown, {
        options: params.values, value: params.value, ref: this.ref, setProps, style: {width: params.column.actualWidth},
    }), 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() {
    // mousedown event
    const clickEvent = new MouseEvent('mousedown', {
        view: window,
        bubbles: true
    });

    // needed to delay and allow the component to render
    setTimeout(() => {
        var inp = this.eInput.getElementsByClassName('Select-control')[0]

        // disables keyboard event
        this.params.colDef.suppressKeyboardEvent = (params) => {
               const gridShouldDoNothing = params.editing
               return gridShouldDoNothing;
           }
        // shows dropdown options
        inp.dispatchEvent(clickEvent)
    }, 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();
  }
}

Now it works just like you selected it in a regular dropdown fashion. :slight_smile:

4 Likes

That’s awesome, I’d forgotten what it was like to get helpful responses on a dev forum so thank you! I notice this removes the need for the styling and the suppressRowTransform=True

I tweaked it a bit and made the clear button work:

dagfuncs.DCC_Dropdown = class {

    // gets called once before the renderer is used
  init(params) {
    // create the cell
    this.params = params;
    this.ref = React.createRef();

    // 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
    ReactDOM.render(React.createElement(window.dash_core_components.Dropdown, {
        options: params.values, value: params.value, ref: this.ref, setProps, style: {width: params.column.actualWidth},
    }), 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() {
    // mousedown event
    const clickEvent = new MouseEvent('mousedown', {
        view: window,
        bubbles: true
    });

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

        // disables keyboard event
        this.params.colDef.suppressKeyboardEvent = (params) => {
               const gridShouldDoNothing = params.editing
               return gridShouldDoNothing;
           }
        // shows dropdown options
        inp.dispatchEvent(clickEvent)
    }, 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();
  }
}

I haven’t managed to get the search_value from the dropdown out but I think connecting these dropdowns to a pattern matching callback might not be the way to go. That’s probably a different requirement and more easily done with an autocomplete cell editor

1 Like

You can use the cellRendererData possibly to trigger the callback.

The search value is its own prop from the drop-down, so you’d have to listen to that in the setProps function as well.

Please note, need to change this line:

var inp = this.eInput.getElementsByClassName('Select-value')[0]

to this:

var inp = this.eInput.getElementsByClassName('Select-control')[0]

I’m not sure I can get to the setData function to update the cellRendererData, because we’re in the cellEditor now and setData is a prop on the cellRenderer. I tried things like params.colDef.cellEditor.setData to no avail. Although maybe I just need to specifically define a cellRenderer.

And I already changed mine to use ‘Select-arrow’, that was the only thing I could see that would open it after you hit the clear button

If you dont care about any validation, you could use the cellValueChanged and compare the newValue to the array and add it from there.

If you do care about validation, I’d recommend putting it into the setProps flow on the JS side. :slight_smile: Then once done, use the cellValueChanged like before.

I’ll get there :slight_smile: thanks again!

1 Like

A post was split to a new topic: Using Dash Mantine Components dmc.Select as an AG Grid cell editor

One thing to watch out for are component updates, because this can break the functionality. :smiley:

1 Like

Hi @alistair.welch This is awesome thanks for sharing :rocket:

If you make any other components, I hope you’ll continue to post them on the forum - and I’ll add them to the docs :slight_smile:

2 Likes

Yeah I think it’s handy! I certainly will if I work on anything else in free time. I do have some other topics that might be worth discussion, including arbitrary event handling and creating column defs from pandas Dataframes. Definitely looking forward to being more active in the community

Very true, probably the dropdown is the more reliable example. Select just more appropriate in my particular use case

If you want the editor to take up the whole column for some reason, you can do so by adding this in the setTimeout:

// sets editor in view
        var wrapper = this.eInput.closest('div.ag-popup-editor')
        wrapper.style.bottom = '0px'
        wrapper.style.top = '0px'

Resulting in this:

image

1 Like