Using Dash Mantine Components dmc.Select as an AG Grid cell editor

I did it. Switched it to the new dash_mantine_components.Select and used creatable=True, works a treat now.

It’s very extensible this whole Dash AG Grid thing, love it!

dagfuncs.DMC_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 (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, ref: this.ref, setProps, style: {width: params.column.actualWidth},
        className: 'dbc',
        clearable: true,
        searchable: true,
        creatable: true
    }), 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();
  }
}
3 Likes

Hi @alistair.welch

Thanks for sharing this great custom cell editor component! I just added it to the Dash AG Grid docs.

I made a couple minor edit to make it even easier to use from Dash. You can now copy and paste the component into the dashAgGridFunctions.js file in the assets folder, then pass props to the dmc.Select component using the cellEditorParams prop. No need to write any JavaScript!

See the code for these examples in the docs:
ag_grod_dmc_select1


ag_grid_dmc_select

2 Likes

That’s really awesome, definitely the thanks have to go to @jinnyzor for making it work!

My personal preference is to use it with clearable=True and set up a callback from cellValueChanged to columnDefs so that any new entries added using creatable are added to the options but with the way you’ve set it up with all the params going in I’m sure people can figure that out for themselves. And I imagine there’ll be other cellEditors people will be able to make using the ReactDOM.render system that Bryan wrote for this.

Exciting stuff, thanks for putting so much effort into this :slight_smile:

2 Likes

@AnnMarieW @How to set the length and width of dmc.select to be the same as the length and width of the cell?

Hi Sylvos, you’ll need to include style: {width: params.column.actualWidth} in the JavaScript file. If you look at the post at the top of this thread it’s in there

1 Like

Hello All,

So, making an adjustment to the disabling of keyboard events, this helps the drop down be a little more intuitive, also allows for better keyboard navigation:

// disables keyboard event
            this.params.colDef.suppressKeyboardEvent = (params) => {
                const gridShouldDoNothing = params.editing && !(window.event.key == 'Tab' ||
                  window.event.key == 'Escape');
                return gridShouldDoNothing;
            };

One other slight modification for the style, I do this:

style: {width: params.column.actualWidth-2, ...params.style},

This helps when navigating the drop downs in edit mode, if the dropdown is the exact size of the column and you moving to the column caused the grid to scroll to a previously undisplayed column, the dropdown was rendered behind the column instead of in front. Dropping it 2, maybe even 1, seems to patch this and get it to work.

1 Like

Tacking on another trick, when using Mantine editors and components, there is a case where they will not follow the styling, here is a little trick:

function mantineWrapper(component) {
    return React.createElement(window.dash_mantine_components.MantineProvider,
        {
        children: component,
        theme: {'colorScheme': document.body.classList.contains('dark') ? 'dark' : 'light'}
        }
    )
}

For this to work, you need to have a clientside callback hooked to your theme switcher that will add the dark class to your document.body when it is in dark mode.

Then you just do this:

// renders component into the editor element
        ReactDOM.render(
            mantineWrapper(
                    React.createElement(window.dash_mantine_components.DatePicker, {
                        setProps,
                        ...params,
                        value: this.value,
                        style: {width: params.column.actualWidth-2, zIndex: 1000, ...params.style},

                    })),
            this.eInput
        );

image

2 Likes

Another trick:

When using things like select, you’ll want the user to be able to navigate and select options via the keyboard. This gets frustrating when trying to push “Enter” because the event bubbles and stops the edit from manifesting in the cell.

var keyDown = (e) => {if (e.key == 'Enter') {e.stopPropagation()}}

...component(onKeyDown: keyDown)

This will prevent the event from bubbling outside to the grid and allow the component to finish passing the updated value to the cell.

2 Likes

@AnnMarieW

I can’t seem to find the dmc.Select in the Docs, was it removed? I was just looking for a few pointers on how I need to pass the dropdown options in the grid creation code.

Hi @bgivens33

That example was in the preliminary docs but has not yet been included in the Plotly docs.

You can find those examples (and more) in the dash-ag-grid Github:

Note- the comment says dcc.Dropdown, but it is actually a dmc.Select:

1 Like

Perfect! Thanks.

Hi @AnnMarieW,
Thank you for these precious examples! I’ve unfortunately not been able to make it run with dmc v0.14 - have you also observed this?

Hello @asalles,

Welcome to the community!

Using dmc 14 is a bit trickier due to restrictions of the Provider, we have been looking into this to see if there is a way to demonstrate how these types of editors can be added to the application.

3 Likes

Hi,
this worked for me:

dagfuncs.DMC_Select = class {
    // gets called once before the renderer is used
    init(params) {
        // store the params
        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();
            }
        };

        // create an element for the editor
        this.eInput = document.createElement('div');

        // create the root for rendering the React component
        this.root = ReactDOM.createRoot(this.eInput);

        // Get MantineProvider and Select from window
        const MantineProvider = window.dash_mantine_components.MantineProvider;
        const Select = window.dash_mantine_components.Select;

        // Get global theme, styles, and other settings if available
        const globalMantineConfig = {
            theme: window.dash_mantine_components.mantineTheme || {},
            styles: window.dash_mantine_components.mantineStyles || {},
            colorScheme: window.dash_mantine_components.mantineColorScheme || 'light',
            emotionCache: window.dash_mantine_components.mantineEmotionCache || null,
            withGlobalStyles: true,
            withNormalizeCSS: true,
        };
        // Render the Select component wrapped in the MantineProvider
        this.root.render(
            React.createElement(
                MantineProvider,
                globalMantineConfig,
                React.createElement(Select, {
                    data: params.options,
                    value: params.value,
                    setProps,
                    style: {
                        position: 'fixed',
                        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,
                })
            )
        );

        // allow focus event
        this.eInput.tabIndex = '0';

        // set editor value to the value from the cell
        this.value = params.value;
    }

    // gets called once when grid is 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';

            // disable keyboard events while the cell editor is open
            this.params.colDef.suppressKeyboardEvent = (params) => {
                const gridShouldDoNothing = params.editing;
                return gridShouldDoNothing;
            };
            // show dropdown options
            inp.focus();
        }, 100);
    }

    // focus and select can be done after the GUI is attached
    afterGuiAttached() {
        // store the active cell
        this.prevFocus = document.activeElement;

        // add event listener to trigger event to go into dash component
        this.eInput.addEventListener('focus', this.focusChild());

        // trigger focus event
        this.eInput.focus();
    }

    // returns the new value after editing
    getValue() {
        return this.value;
    }

    // safely unmount the React component
    destroy() {
        // Delay the unmounting to avoid race conditions during render
        setTimeout(() => {
            if (this.root) {
                // unmount the component and clean up
                this.root.unmount();
                this.root = null;  // clear reference to avoid further operations
            }

            // set focus back to the grid's previously active cell
            if (this.prevFocus) {
                this.prevFocus.focus();
            }
        }, 0); // ensure unmount happens after rendering is complete
    }
};

I then define the column with these two parameters:

            "cellEditor": {"function": "DMC_Select"},
            "cellEditorParams": {
                "options": ["value1", "value2", "value3"],
                "maxDropdownHeight": 280,
            },

EDIT: i forgot the parameters are set for dmc.Select v12 so double check that!!!

EDIT: corrected:

dagfuncs.DMC_Select = class {
    // gets called once before the renderer is used
    init(params) {
        // store the params
        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();
            }
        };

        // create an element for the editor
        this.eInput = document.createElement('div');

        // create the root for rendering the React component
        this.root = ReactDOM.createRoot(this.eInput);

        // Get MantineProvider and Select from window
        const MantineProvider = window.dash_mantine_components.MantineProvider;
        const Select = window.dash_mantine_components.Select;

        // Get global theme, styles, and other settings if available
        const globalMantineConfig = {
            theme: window.dash_mantine_components.mantineTheme || {},
            styles: window.dash_mantine_components.mantineStyles || {},
            colorScheme: window.dash_mantine_components.mantineColorScheme || 'light',
            emotionCache: window.dash_mantine_components.mantineEmotionCache || null,
            withGlobalStyles: true,
            withNormalizeCSS: true,
        };
        // Render the Select component wrapped in the MantineProvider
        this.root.render(
            React.createElement(
                MantineProvider,
                globalMantineConfig,
                React.createElement(Select, {
                    data: params.options,
                    value: params.value,
                    setProps,
                    style: {
                        position: 'fixed',
                        width: params.column.actualWidth - 2,
                        ...params.style,
                    },
                    allowDeselect: params.allowDeselect,
                    checkIconPosition: params.checkIconPosition,
                    className: params.className,
                    classNames: params.classNames,
                    clearButtonProps: params.clearButtonProps,
                    clearable: params.clearable,
                    comboboxProps: params.comboboxProps,
                    darkHidden: params.darkHidden,
                    description: params.description,
                    descriptionProps: params.descriptionProps,
                    disabled: params.disabled,
                    dropdownOpened: params.dropdownOpened,
                    error: params.error,
                    errorProps: params.errorProps,
                    hiddenFrom: params.hiddenFrom,
                    hiddenInputProps: params.hiddenInputProps,
                    inputWrapperOrder: params.inputWrapperOrder,
                    label: params.label,
                    labelProps: params.labelProps,
                    leftSection: params.leftSection,
                    leftSectionPointerEvents: params.leftSectionPointerEvents,
                    leftSectionProps: params.leftSectionProps,
                    leftSectionWidth: params.leftSectionWidth,
                    lightHidden: params.lightHidden,
                    limit: params.limit,
                    loading_state: params.loading_state,
                    maxDropdownHeight: params.maxDropdownHeight,
                    mod: params.mod,
                    name: params.name,
                    nothingFoundMessage: params.nothingFoundMessage,
                    persisted_props: params.persisted_props,
                    persistence: params.persistence,
                    persistence_type: params.persistence_type,
                    placeholder: params.placeholder,
                    pointer: params.pointer,
                    radius: params.radius,
                    readOnly: params.readOnly,
                    required: params.required,
                    rightSection: params.rightSection,
                    rightSectionPointerEvents: params.rightSectionPointerEvents,
                    rightSectionProps: params.rightSectionProps,
                    rightSectionWidth: params.rightSectionWidth,
                    scrollAreaProps: params.scrollAreaProps,
                    searchValue: params.searchValue,
                    searchable: params.searchable,
                    selectFirstOptionOnChange: params.selectFirstOptionOnChange,
                    size: params.size,
                    styles: params.styles,
                    tabIndex: params.tabIndex,
                    variant: params.variant,
                    visibleFrom: params.visibleFrom,
                    withAsterisk: params.withAsterisk,
                    withCheckIcon: params.withCheckIcon,
                    withErrorStyles: params.withErrorStyles,
                    withScrollArea: params.withScrollArea,
                    wrapperProps: params.wrapperProps,
                })
            )
        );

        // allow focus event
        this.eInput.tabIndex = '0';

        // set editor value to the value from the cell
        this.value = params.value;
    }

    // gets called once when grid is 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';

            // disable keyboard events while the cell editor is open
            this.params.colDef.suppressKeyboardEvent = (params) => {
                const gridShouldDoNothing = params.editing;
                return gridShouldDoNothing;
            };
            // show dropdown options
            inp.focus();
        }, 100);
    }

    // focus and select can be done after the GUI is attached
    afterGuiAttached() {
        // store the active cell
        this.prevFocus = document.activeElement;

        // add event listener to trigger event to go into dash component
        this.eInput.addEventListener('focus', this.focusChild());

        // trigger focus event
        this.eInput.focus();
    }

    // returns the new value after editing
    getValue() {
        return this.value;
    }

    // safely unmount the React component
    destroy() {
        // Delay the unmounting to avoid race conditions during render
        setTimeout(() => {
            if (this.root) {
                // unmount the component and clean up
                this.root.unmount();
                this.root = null;  // clear reference to avoid further operations
            }

            // set focus back to the grid's previously active cell
            if (this.prevFocus) {
                this.prevFocus.focus();
            }
        }, 0); // ensure unmount happens after rendering is complete
    }
};