justpy-org / justpy

An object oriented high-level Python Web Framework that requires no frontend programming
https://justpy.io
Apache License 2.0
1.22k stars 96 forks source link

Ag-Grid: Permit Custom Cell Renderers #276

Closed othalan closed 2 years ago

othalan commented 3 years ago

I propose a few simple changes to permit use of custom cell renderers as detailed in this page:

Ag-Grid: Component Cell Renderer

Justification

While similar visual effects can be obtained by use of HTML columns, this solution has the problem of converting numeric data into text. Numbers converted to text will not filter and sort properly.

Cell Renderers are specifically designed to bypass this problem by modifying only the displayed value, not the underlying data value.

Implementation

This can be made possible with a few simple changes to existing code. A cell renderer must be implemented in javascript as the code needs to run on the client side. A location for such components already exists on the javascript side for the implementation of the checkboxRenderer already available in the code. The only change that is necessary is to provide a hook to populate the javascript grid_def.components variable from python.

Diff based on version 0.1.5, the most recent version of code available at this time.

diff --git a/justpy/gridcomponents.py b/justpy/gridcomponents.py
index 1055522..0738900 100644
--- a/justpy/gridcomponents.py
+++ b/justpy/gridcomponents.py
@@ -30,6 +30,7 @@ class AgGrid(JustpyBaseComponent):
         self.classes = ''
         self.style = 'height: 99vh; width: 99%; margin: 0.25rem; padding: 0.25rem;'
         self.evaluate = []  # Fields for evaluation
+        self.components = {} # Javascrtipt Function Components
         self.show = True
         self.pages = {}
         self.auto_size = True   # If True, automatically resize columns after load to optimal fit
@@ -119,4 +120,5 @@ class AgGrid(JustpyBaseComponent):
         d['events'] = self.events
         d['html_columns'] = self.html_columns
         d['evaluate'] = self.evaluate
+        d['components'] = self.components
         return d
diff --git a/justpy/templates/js/aggrid.js b/justpy/templates/js/aggrid.js
index 6cf8426..3de3a0f 100644
--- a/justpy/templates/js/aggrid.js
+++ b/justpy/templates/js/aggrid.js
@@ -54,6 +54,10 @@ Vue.component('grid', {
                 checkboxRenderer: CheckboxRenderer
             };

+            // Convert user components into javascript objects
+            for (const [key, value] of Object.entries(this.$props.jp_props.components)) {
+                eval('grid_def.components[key] = ' + value);
+            }

             new agGrid.Grid(document.getElementById(this.$props.jp_props.id.toString()), grid_def);  // the api calls are added to grid_def
             cached_grid_def['g' + this.$props.jp_props.id] = grid_def;

Use Case Example

The following code displays the "Price" column formatted in US Currency.

import justpy as jp

grid_options = {
    'defaultColDef': {
        'filter': True,
        'sortable': True,
        'unSortIcon': True,
    },
    'columnDefs': [
      {'headerName': "Make", 'field': "make"},
      {'headerName': "Model", 'field': "model"},
      {'headerName': "Price", 'field': "price", 'cellRenderer': "renderCurrencyUSD"}
    ],
    'rowData': [
      {'make': "Toyota", 'model': "Celica", 'price': 35000},
      {'make': "Ford", 'model': "Mondeo", 'price': 3200},
      {'make': "Porsche", 'model': "Boxter", 'price': 72000}
    ],
}

def grid_test():
    wp = jp.QuasarPage()
    wp.grid = jp.AgGrid(a=wp, options=grid_options)
    wp.grid.components['renderCurrencyUSD'] =  """function RenderCurrencyUSD (params) {
            var inrFormat = new Intl.NumberFormat('en-US', {
                style: 'currency',
                currency: 'USD',
                minimumFractionDigits: 2
            });
            return inrFormat.format(params.value);
        }
        """

    return wp

jp.justpy(grid_test)
othalan commented 3 years ago

I notice this suggestion was not included in the latest version (v0.2.2). Is there a different mechanism which will achieve the same result? Or could this functionality be added to the next version?

WolfgangFahl commented 2 years ago

This is a tough one. Your specific solution certainly works. Personally I'd love to see a much more general approach to issues such as this one.

othalan commented 2 years ago

I no longer use JustPy in my current personal projects as I decided to write the JavaScript code myself, but I still face the same type of problem of passing short JavaScript functions from Python to JavaScript via a JSON data structure. I have in essence taken exactly what I wrote above in my original submission of this issue and made it more generic, which is quite simple:

First off, use JavaScript Function instead of eval. It is safer and is the current recommended practice.

Mozilla Reference: Fuction

If you want any field to be able to be compiled as JavaScript code, simply create an internal use only field in your JSON dictionary which lists other fields which should be compiled. Similarly, if you want generic fields to be able to be set in grid_def, I would use the exact same pattern as above, just changing the special field name and associated JavaScript action.

On the Python side, you might have:

send = {
  'field_name': ['a', 'b', 'return a+b'],
  '_compile': ['field_name'],
  '_global': ['field_name'],
}

On the JavaScript side, making things generic with the above data is equally simple. If you assume data is the incoming JSON data structure ....

data['_compile'].forEach(field => data[field] = Function(...data[field]));
data['_global'].forEach(field => grid_def[field] = data[field]);

Back in Python, you can easily automate the creation for '_compile' and '_global' flags through any number of methods. For compiling JavaScript, I simply created a class based on list, no customization needed. When that custom class (JSFunction) is detected during conversion to JSON format, a special handler automatically creates the _compile field in the dictionary. The python user code could then simplify to:

send = {
  'field_name': JSGlobalFunction(['a', 'b', 'return a+b']),
}

I found this solution to be simple, elegant, and generic. I have used this same pattern in a couple of different ways to simplify my life communicating between Python and JavaScript.