Using dash_core_components Dropdown as an AG Grid cell editor

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

That works for sure. You kind of lose which row you’re on but it does fix the problem. I think best case is the Select-menu-outer would switch to be above the current cell if there’s not enough room for it. Similar to React Select. That might actually be an idea. With the ReactDOM.render that we’re using here we might be able to just render a React Select

You could, but does it work within the grid the same way as it does a document?

Yeah would need looking into. And ignore about React Select, it’s not React Select that has the behaviour by default, only Mantine Select

Yes, dmc does handle that more smoothly for sure.

If you wanted to add a listener for scrolling, you could. But honestly, its unnecessary. XD

Yeah I had a look at how it’s done in here, looking at the dropdownPosition variable, it’s not a quick one to sort haha