Dash AG Grid: inline dropdown that allows new value

I have an AG Grid with a dropdown control.

I’m following the pattern of using dashAgGridFunctions.js (like below) and the cellEditorParams property of the column.

This basically works, but I want the user to be able to enter a value that is not in the dropdown as well.

So the idea is, the dropdown will show the most common values for the given column from database results, but the user can also enter any text they want.

Is this possible/straightforward?

I haven’t written any javascript in quite a while, so I’m hoping this is a common pattern that already exists.

Thanks in advance.>

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

dagfuncs.dynamicTrueFalseOptions = function(params) {
return {
values: [‘True’, ‘False’],
};
}

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

Hi @zack_nc

It’s not possible for users to add new options with the dcc.Dropdown component. However, this feature is available in dmc.Select - see the"createable" example on this page:

You can see some examples of using the dmc.Select component in the preliminary Dash AG Grid docs (this has not yet made it’s way into the official Dash docs yet)

dag-docs

2 Likes

Thanks!

I have a head’s up and a question:

The head’s up is that I wasn’t able to make this work until I tweaked the javascript for the DCC_Dropdown like this:

ReactDOM.render(
React.createElement(window.dash_mantine_components.Select, {
data: params[0].options,
value: params.value,

Notice that I added the ‘[0]’ because when I look in the debugger, the options aren’t on the params, but params[0].

Once I made that change, it mostly works, but, the “add new” functionality isn’t working for me like in the demo.

If I enter a value that isn’t in the list, I don’t get the option to add it.

I’m setting up the column like this:

elif i == ‘Category’:
# entry[‘editable’] = True
# entry[‘cellRenderer’] = ‘Dropdown’
entry[‘cellEditor’] = {“function”: “DMC_Select”}
entry[‘cellEditorParams’] = {
“options”: dfCategoryOptions.category.unique(),
“clearable”: False,
“creatable”: True,
“shadow”: “xl”,
},
entry[‘cellEditorPopup’] = True,
entry[‘singleClickEdit’] = True

I’m wondering if this could be a matter of versions or something?

I’m using dash_ag_grid 2.3.0 and dash-mantine-components 0.12.1

Hello @zack_nc,

Could you please create a full MRE for this?

I use the select all the time, and I dont see this issue. The params should not be a list.

Sure.

The python code is below.

My dashAgGridFunctions.js is copied straight from the example at Dash

But like I said, it didn’t work until I fixed the params.options reference.

import pandas as pd

import dash
from dash import Input, Output, html
import dash_bootstrap_components as dbc
import dash_mantine_components as dmc

import dash_ag_grid as dag

dash.register_page(__name__, name='dashboardDemo', group='MISO')

@dash.callback(
    Output("div-topology-projects", "children"),
    Input("div-topology-projects", "id")
    
)
def populateTopologyProjects(id):

    df = pd.read_csv(
        "https://raw.githubusercontent.com/plotly/datasets/master/ag-grid/olympic-winners.csv"
    )

    return generateTable(df, 'table-topology-projects', tableHeight=300)

def generateTable(table, tableid, fixedColumnIndex=0, tableHeight=None, doFilter=False):
    
    columnTypes = {
        "numberColumn": {"width": 130, "filter": "agNumberColumnFilter"},
        "medalColumn": {"width": 100, "columnGroupShow": "open", "filter": False},
        "nonEditableColumn": {"editable": False},
        "editableColumn": {"editable": True}
    }
    
    columns = []
    columnIndex = 1
    for i in table.columns:
        # if table[i].dtype == 'float64':
        entry = {'headerName': i, 'field': i}
        entry['minWidth'] = 110
        entry['type'] = 'editableColumn'

        if i == 'country':
            # entry['editable'] = True
            # entry['cellRenderer'] = 'Dropdown'
            entry['cellEditor'] = {"function": "DMC_Select"}
            entry['cellEditorParams'] =  {
                "options": table.country.unique(),
                "clearable": False,
                "creatable": True,
                "shadow": "xl"
            },
            entry['cellEditorPopup'] = True
            entry['singleClickEdit'] = True
        
        if columnIndex <= fixedColumnIndex:
            entry['pinned'] = 'left'
        columnIndex += 1

        columns.append(entry)
    table = table.to_dict('records')

    defaultColDef = {
        "resizable": True,
        "sortable": True,
        "editable": False,
        "tooltipComponent": "CustomTooltip"
        }
    
    dashGridOptions = {
        "undoRedoCellEditing": True, 
        "rowSelection": "single", 
        "columnTypes": columnTypes,
        "tooltipShowDelay": 400
        }
    
    if tableHeight is None:
        style={'height': '100%', 'width': '100%'}
        dashGridOptions['domLayout'] = 'autoHeight'
    else:
        style={'height': int(f'{tableHeight}'), 'width': '100%'}
        if (len(table) * 30) < int(tableHeight):
            dashGridOptions['domLayout'] = 'autoHeight'

    # Dash tables are very slow when using fixed rows or columns, so only do that when necessary
    return dag.AgGrid(
        id=tableid,
        columnDefs=columns,
        rowData=table,
        columnSize="sizeToFit",
        defaultColDef=defaultColDef,
        dashGridOptions=dashGridOptions,
        className="ag-theme-balham",
        style=style
    )
    
layout =  dbc.Container(fluid=True, children=[
    # Toolbar
    html.Br(),
    dbc.Row([
        dbc.Col([
            html.H4('', id='div-topology-projects-Title'),
            html.Div(
                    id='div-topology-projects'
                )
            ], width=12)
    ]),
])

debug1
debug2
debug3

You have an hanging comma here, thus creating a tuple. :wink:

1 Like

Oh my goodness…

That is diabolical.

Thank you so much for humoring me and pointing that out.

2 Likes

Quick followup (since you’ve been SO helpful):

If I have a very small table/grid, the options get cutoff, like in this screenshot.

Do you know of a property that can be set to make this a little cleaner?

You’d probably have to go back to the default select that the grid offers.

You can give it a shot and see what happens. XD

2 Likes

Maybe have a minimum height for the grid - to make room for the options?

2 Likes

Thanks.

Yet another followup here:

I need a way for the user to select a row, so that I can take data from that row and populate other objects on the screen.

The code below accomplishes this. However, it hijacks the behavior of the inline DMC_select controls.

I was hoping that if I raise PreventUpdate when the selection is coming from a column with a select control, I could prevent this, but that isn’t sufficient. I always get an empty list of options.

Do you know of any way to make it possible to select a row - or a column on a row - without breaking the select control behavior?

@dash.callback(
[
(some outputs),
[Input(‘td-table-topology-projects’, ‘cellClicked’),
Input(‘td-table-topology-projects’, ‘selectedRows’)],
prevent_initial_call=True
)
def projectRowSelected(activeCell, selectedRows):
if activeCell is None:
raise PreventUpdate

if activeCell['colId'] == 'State' or activeCell['colId'] == 'Category':
    raise PreventUpdate

# else: do some stuff

Id make it so that clicking a row doesn’t make the selection, but only when they click a checkbox.

Check the docs for the selection checkbox in the first column, and suppressing row click selection.

@zack_nc Would you mind taking a look at below? I’m trying to replicate what you’ve done and have this in my dangfuncs js file:

dagfuncs.DCC_Select = 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
    React.createElement(window.dash_mantine_components.Select, {
      data: params[0].options,
      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();
  }
}

Does this look correct to you?

Hello @eldeirdro,

Did you setup the namespace for dagfuncs?

HI @jinnyzor,

Thanks for responding. This is the top of my file:

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

My other functions in the file work fine, so maybe that’s not the issue.

This is how I’m defining the column:

{
    "field": "condition",
    "headerName": "Condition",
    "cellRenderer": "Dropdown",
    "cellEditor": {
        "function": "DMC_Select"
    },
    "cellEditorParams": {
        "options": [
            "x",
            "y",
            "z"
        ],
        "clearable": true,
        "creatable": true,
        "shadow": "xl"
    },
    "cellEditorPopup": true,
    "singleClickEdit": true,
    "valueFormatter": {
        "function": "params.value ? params.value : 'Select..'"
    }
}

I get the error: Cannot read properties of undefined (reading ‘options’)
I also tried to define the options param in the React create element as “data: params.options,” instead of “data: params[0].options,”. That doesn’t throw an error, but nothing happens when I click on the cell.

It should be params.options, try to log to the console the params inside of the functions.

It might also need to be params.colDef.params.options or something similar. options I think is reserved.

Hi @eldeirdro

If you are using a dmc select, you need to use dmc props. There is no prop in dmc.Select called options. It’s called data. When you defined your component, did you map the cellEditorParams.Options to the data prop in dmc.Select?

You can find a complete example here:

1 Like

Thanks both, I’m now past that error and returning the options list as a dropdown using DMC Select.

My class is now:

dagfuncs.DMC_Select = 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
    ReactDOM.render(
      React.createElement(window.dash_mantine_components.Select, {
        data: params.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();
  }
}

dagfuncs.setBody = () => {
  return document.querySelector('body')
}

My original need was to give the user an option to type a new value if none of the options are suitable. As you can see from above, I added ‘“creatable”: true,’ to the column definition, but it didn’t change anything. I couldn’t see references to “creatable” in the docs anymore, but have just found out from https://www.dash-mantine-components.com/migration they have depreciated it and suggest TagsInput https://www.dash-mantine-components.com/components/tagsinput instead to provide similar functionality.

From the conversation https://community.plotly.com/t/inserting-tagsinput-from-dash-mantine-components-into-an-ag-grid-as-cell-editor/86131/5 it sounds quite tricky. I might be better off just using DMC version 0.12.

2 Likes

This is my attempt at getting TagsInput as cellRenderer from looking at other posts on here. I’m a bit clueless though:

dagcomponentfuncs.DMC_TagsInput = function (props) {
  const { setData, value } = props;

  return React.createElement(
    window.dash_mantine_components.TagsInput,
    {
      value: value,
      size: props.size || 'xs',
      radius: props.radius || 'sm',
      onChange: (value) => setData(value),
      styles: {
        pill: { backgroundColor: "gray", color: "black" },
        input: { backgroundColor: "transparent" },
      }
    }
  );
};

Do you have any advice of what to add/change? I’m using version 14, does the react element need to be wrapped in mantine provider?