Dash AGGrid PropertiesBox (Any interactive Dash Component in Any Aggrid Cell)

Hi everyone I’m trying to build a generic properties-box.

The properties box is an AGGrid with 2 columns (property key and value)
It is defined by a simple dictionary with the name, value and type per variable.

Eg.

properties = [
		{'name':'bool', 'value':False, 'type' : 'bool'},
		{'name':'int', 'value':0,  'type' : 'int'},
		{'name':'float', 'value': 0.0, 'type' : 'float'},
		{'name':'range', 'value': None, 'type' : 'range'}, 
		{'name':'text', 'value':'POO', 'type' : 'text'},
		{'name':'email', 'value': None, 'type' : 'email'},
		{'name':'password', 'value': None, 'type' : 'password'},
		{'name':'select', 'value': None, 'type' : 'select', 
		 'args' : {
			 'options' : 
					[
						{"label": "Option 1", "value": "opt_1"},
						{"label": "Option 2", "value": "opt_2"},
						{"label": "Option 3", "value": "opt_3", "disabled": True},
						{"label": "Option 4", "value": "opt_4"},
						{"label": "Option 5", "value": "opt_5"},
						], 
					},
			 },
		{'name':'date', 'value': datetime.datetime.utcnow(), 'type' : 'date'},
		{'name':'color', 'value': '#FF0000', 'type' : 'color'},
		]

the resulting Aggrid would look something like this.

Almost everything works.
I have issues with

  • the ‘select’ dropdown (a bootstrap Select) : onChange() does not trigger
  • the date input (a dash mantine DatePicker) : Datepicker doesn’t show correctly
  • aligning / sizing the Date and Color inputs to their cell.

Here is the code
python:

import json
import datetime

import dash
from dash import dcc, html, Output, Input, State	
import dash_ag_grid as dag
import dash_bootstrap_components as dbc
import dash_mantine_components as dmc


type_components = {
	'bool':	
	{
		'library' : 'dash_bootstrap_components',
		'name' : 'Checkbox',
		'args'	: {},
		},
	'int': 
	{
		'library' : 'dash_bootstrap_components',
		'name' : 'Input',
		'args'	: {
			'type' : 'number',
			'step' : 1,
			'placeholder' : 'enter integer...',
			},
		},
	'float':
	{
		'library' : 'dash_bootstrap_components',
		'name' : 'Input',
		'args'	: {
			'type' : 'number',
			'step' : 0.001,
			'placeholder' : 'enter float...',
			},
		},
	'range':
	{
		'library' : 'dash_bootstrap_components',
		'name' : 'Input',
		'args'	: {
			'type' : 'range',
			},
		},
	'text':
	{
		'library' : 'dash_bootstrap_components',
		'name' : 'Input',
		'args'	: {
			'type' : 'text',
			'placeholder' : 'enter text...',
			},
		},
	'email':
	{
		'library' : 'dash_bootstrap_components',
		'name' : 'Input',
		'args'	: {
			'type' : 'email',
			'placeholder' : 'enter email...',
			},
		},
	'password':
	{
		'library' : 'dash_bootstrap_components',
		'name' : 'Input',
		'args'	: {
			'type' : 'password',
			'placeholder' : 'enter password...',
			},
		},
	'select':
	{
		'library' : 'dash_bootstrap_components',
		'name' : 'Select',
		'args'	: {
			'placeholder' : 'select option...',					
			},
		},
	'date':
	{
		'library' : 'dash_mantine_components',
		'name' : 'DatePicker',
		'args'	: {
			'placeholder' : 'select date...',							
			},
		},
	'color':
	{
		'library' : 'dash_bootstrap_components',
		'name' : 'Input',
		'args'	: {
			'type' : 'color',
			},
		},
	}  


def dash_aggrid_properties_box(id =None, properties = None):
	if not properties:
		return

	box_properties =  []
	for prop in properties:
		box_prop = dict(prop, **{'component' : type_components.get(prop['type'])})
		if 'args' in box_prop:
			box_prop['component']['args'] = dict(box_prop['component']['args'], **box_prop.pop('args') )
		box_properties.append(box_prop)

	return dag.AgGrid(				
				id=id,
				columnDefs=[
					{'field' : 'name', 'minWidth' : 50,'suppressSizeToFit':True},
					{'field' : 'value', 'cellRenderer' : 'componentRenderer'}, 
					{'field' : 'type', 'hide':True}, 
					{'field' : 'component', 'hide':True},
					],
				rowData=box_properties,
				columnSize="responsiveSizeToFit",
				defaultColDef={
					'display' : 'flex',
					'flex-flow': 'row nowrap',
					"suppressMovable" : True,
					'resizable':True,
					'minWidth' : 200,
					"sortable": False,
					"filter": False,
				} ,
				dashGridOptions={
					"rowHeight": 50, # to check correct filling of the cell
					},
				style = {
					'flex' : '1 0 0',
					}
				)


if __name__=='__main__':
	test_properties = [
		{'name':'bool', 'value':False, 'type' : 'bool'},
		{'name':'int', 'value':0,  'type' : 'int'},
		{'name':'float', 'value': 0.0, 'type' : 'float',
		   'args' : {
			   'min' : -10.0,
			   'max' : 10.0,
			   'step' : 0.1,
			   'placeholder' : 'enter 1 decimal float between -10 <-> 10...',
			   },
		   },
		{'name':'range', 'value': None, 'type' : 'range'}, 
		{'name':'text', 'value':'POO', 'type' : 'text'},
		{'name':'email', 'value': None, 'type' : 'email'},
		{'name':'password', 'value': None, 'type' : 'password'},
		{'name':'select', 'value': None, 'type' : 'select', 
		 'args' : {
			 'options' : 
					[
						{"label": "Option 1", "value": "opt_1"},
						{"label": "Option 2", "value": "opt_2"},
						{"label": "Option 3", "value": "opt_3", "disabled": True},
						{"label": "Option 4", "value": "opt_4"},
						{"label": "Option 5", "value": "opt_5"},
						], 
					},
			 },
		{'name':'date', 'value': datetime.datetime.utcnow(), 'type' : 'date'},
		{'name':'color', 'value': '#FF0000', 'type' : 'color'},
		]


	app = dash.Dash(__name__)
	app.layout = html.Div( 		[
			html.Div(
				'', 
				id='output-text',
				style = {
					'flex' : '0 0 1',
					}
				),
			dash_aggrid_properties_box(
				id ='properties-box',
				properties = test_properties
				),			
			],
			style = {
				'display' : 'flex',
				'flex-flow': 'column wrap',
				'height': '100vh',
				}
	)	

	@app.callback(
    Output("output-text", "children"),
    Input("properties-box", "cellValueChanged"),
	)
	def selected(changed):	
		return f"\nYou have selected {json.dumps(changed, indent=4)}\n"

	app.run_server(debug=True)

and the javascript with a dynamic componentRenderer in the dagcomponentfunc’s (assets folder)

var dagcomponentfuncs = (window.dashAgGridComponentFunctions = window.dashAgGridComponentFunctions || {});

dagcomponentfuncs.componentRenderer = function (props) {
    const { data } = props;

    // on click
    function onClick() {
        if (event == undefined) return;
        var value;

        // type specs
        if (props.data.type == 'bool') {
            value = event.target.checked;
            // exception for checkbox trigger callback
            var colId = props.column.colId;
            props.node.setDataValue(colId, value);
            this.value = value;

        } else if (props.data.type == 'select') {
            if (event.target.selectedIndex > 0) {
                value = event.target.selectedOptions[0].text;
            };

        } else {
            value = event.target.value;
        };
    };

    // on change
    function onChange() {
        var value = event.target.value;
        if (value === '') return;
        if (!event.target.validity || !event.target.validity.valid) return;

        // type specs
        if (props.data.type == 'int') {
            value = parseInt(value);

        } else if (props.data.type == 'float') {
            value = parseFloat(value);
        };

        this.value = value;

        // trigger callback
        const colId = props.column.colId;
        props.node.setDataValue(colId, value);
    };

    // init component 
    var component_func = window[data.component.library][data.component.name];
    var component_args = {
        ...data.component.args,
        value: props.value,
        onChange: onChange,
        style: {
            cursor: 'pointer',
            flex: '1 1 0',
            border: '1px solid #0000FF', // to check correct filling of the cell
        }
    };

    //// exception for check box
    if (data.type == 'bool') component_args['checked'] = component_args.value;  // don't know a direct(python-like) way to pop the value from an object

    //// build component
    component = React.createElement(
        'div',
        {
            onClick: onClick,
            style: {
                height: props.eGridCell.scrollHeight,
                flex: '1 1 0',
                display: 'flex',
            },
        },
        [
        React.createElement(
            component_func,
            component_args
            )
        ]
    );

    return component;
};

Could someone help me out please.
Thx in advance

PS:
I also tried to do something similar with Editors but this failed miserably…

Feel free to add other types :slightly_smiling_face:

Hello @popo,

I would look into using the setProps instead of the onChange, and use it to pull the value being passed from the underlying component.

Also, you need to tie it into updating the grid’s info, something like this:

props.setDataValue(props.column.colId, newValue)

Cant remember if that is the right function.

Thx jinnyzor,

I am using props.setDataValue(props.column.colId, newValue)
This to trigger the python callback on cellValueChanged prop.

The problem is that the Bootstrap Select does not trigger onChange handle itself. (all other bootstrap input components seem to have no trouble doing this)
Should I add a custom eventlistener for this? and How?

I do not know for sure how to use setProps as a handle/listener … (i’m a javascript noob)
It seems like setProps is usually ran from inside the onChange to trigger a callback (on another prop than cellValueChanged)

Note that cellValueChanged dumps the whole row data incl old and newvalue. Very useful!
SetProps seems to be reserved for Dash Components only.
props.setDataValue(props.column.colId, newValue) can be used to trigger a python callback ( cellValueChanged) for every React Component (i think, or so it seems)

I thought I could easily use any Dash component inside a AGGrid cell. But it looks like it’s not that straightforward…

The ColorPicker works perfectly, just as expected !

I think the Date popup pops up but its hidden because it is rendered inside the cell and is not on top of everything

My next step was to integrate a Date/Time/Timezone dropdowns row into 1 cell. But this seems way too far fetched when simple dropdowns and Date popups cause problems.
Should I just give up trying to implement custom components per row cells in an AGGrid?

Thought it would be a nice addition to the forum. (AGGrids with any Component inside of Any Cell)

setProps is given to every dash component and is used to adjust the underlying component. Therefore, this should always be available and allow you to pull the value.

Look at the examples in the docs from the cellRenderers and cellEditors.

I used ‘setProps’ instead of ‘onChange’
like explained here the docs

I do not really know what I am doing exactly
But this did the trick for the Bootstrap Select Component !!!
It seems to be IMPORTANT : ‘setProps’ MUST be the 2nd argument!

    ...

    //function onChange()
    function setProps() {
        var value = event.target.value;
        if (value === '') return;
        if (!event.target.validity || !event.target.validity.valid) return;

        // type specs
        if (props.data.type == 'int') {
            value = parseInt(value);

        } else if (props.data.type == 'float') {
            value = parseFloat(value);
        };

        this.value = value;

        // trigger callback
        const colId = props.column.colId;
        props.node.setDataValue(colId, value);
    };

    ...

    var component_args = {
        value: props.value,
        //onChange: onChange,
        setProps,
        ...data.component.args,

        style: {
            cursor: 'pointer',
            flex: '1 1 0',
            border: '1px solid #0000FF', // to check correct filling of the cell
        }
    };
    
    ...

Now I’m still left with the DatePicker.
It seems to render inside the cell, thus it is cut off by the cells’ boundaries.

DatepickerIssue

Yes, for a datePicker or select, or anything with drop downs, it is better to use a cellEditor.

Ok…

But this throws an hideous monkey wrench into the uniformity…

‘Bootstrap Select’ and ‘Bootstrap Input Color’ do throw popups and work perfectly fine…

Is it just Dash-Mantine… ?

I’ll try another Datepicker and (very unfortunately) throw Mantine overboard…

Keep u posted.
Thx Jinnyzor

PS: I just fell off my chair… Bootsrap does NOT hava a DatePicker nor a time picker… ???
Although the Bootstrap React components do (and a real nice one too!)
This is ‘seriously painfull’ for Dash.

I tested a few datepickers (core components/ dash datetimepicker / … ), other stuff that throws popups…
It’s not just Dash-Mantine…

‘Bootstrap’ popups seem to work perfectly fine…

Is there anyway round this?

I also had issues (others) doing this purely with all ComponentEditors. (probably due to my lack of jJS knowledge…)

When using the cellEditor, you need to pass cellEditorPopup: True.

I know that I use this and everything works fine (as long as the grid div is large enough to have all the info displayed).