flexxui / pscript

Python to JavaScript compiler
http://pscript.readthedocs.io
BSD 2-Clause "Simplified" License
260 stars 25 forks source link

How to convert Python dict to JS? #43

Closed matkuki closed 4 years ago

matkuki commented 4 years ago

Hi,

I'm trying to wrap Tabulator (http://tabulator.info) into Flexx. The problem I'm facing is this: Tabulator has a method addData with a JS example usage:

table = Tabulator(...)
table.addData([{id:1, name:"bob", gender:"male"}, {id:2, name:"Jenny", gender:"female"}], true);

Now I'm trying to call this method from Flexx with this example:

self.table = TabulatorTableBaseStandard(...) # Wrapped Tabulator class
self.table.addData([ {} ], True)

... but this throws the following exception at the second line:

JS: Command that failed to encode:
JS: INVOKE,TabulatorTableBaseStandard_34,_emit_at_proxy,[object Object]

I also tried some dummy data in place of the empty dict, but no difference. Any ideas?

matkuki commented 4 years ago

Weird observation: if I add a print statement of the returned value, then is works:

def insert_data(self):
    result = self.table.addData([ {} ], True)
    print(result)

... and the error disappears. This example has been directly copy&pasted from my code, the table object is now initialized in the __init__ method. But without the print statement, the error is same as in the previous example.

@almarklein Could you take a look at this issue please? I have no idea what's going on here.

P.S.: The assignment makes it work result = ...! Why does

result = self.table.addData([ {} ], True)

work, but not

self.table.addData([ {} ], True)

?

almarklein commented 4 years ago

So the TabulatorTableBaseStandard is a Flexx wrapper around the native Tabulator class? Is it a JSComponent or PyComponent? And what about the code that had the insert_data method?

matkuki commented 4 years ago

Hi @almarklein

This is the rough structure of the wrapper:

flx.assets.associate_asset(__name__, "tabulator.js", get_file_content("tabulator/js/tabulator.js"))
flx.assets.associate_asset(__name__, "tabulator.css", get_file_content("tabulator/css/tabulator.css"))

class CustomTabulator(flx.PyWidget):
  def init(self):
    ...
    self.tabulator = TabulatorTableBaseStandard(...)
    self.tabulator.reaction("row_added_base", self.row_added) # <-- IF THIS IS COMENTED OUT, THEN THE 'addData' METHOD CALL INSIDE 'row_insert' WORKS!!!
    ...

  def row_added(self, *events):
    pass

  # More code ...

class TabulatorTableBaseStandard(flx.Widget):
  def init(self, name):
    global Tabulator
    options = {
      "height": "100%",
      ...
    }
    self.table = Tabulator(name, options)
    ...

  @flx.emitter
  def row_added_base(self, row_data, row_number):
      return {
          "row_data": row_data,
          "row_number": row_number,
      }  

  def row_insert(self, row_number, row_data, before):
    new_data = {}
    for k,v in row_data.items():
        new_data[k] = v
    self.table.addData([ new_data ], before, row_number)
    self.row_added_base(row_data, row_number)

  @flx.action
  def row_insert_before(self, row_number):
    row = self.table.getRows()[row_number]
    self.row_insert(row_number, row.getData(), True)

  # More code ...

After further trial and error I found that when I connect the row_added_base reaction in the python CustomTabulator's init method to the ˙TabulatorTableBaseStandard˙ emitter, then the error happens everytime the row_insert_before action is called. But if the connection to the reaction is commented out, then it works!

I apologize, but I have no idea what's going on. If you need any more information, just let me know.

Thanks

almarklein commented 4 years ago

What if you instead do this?


    @flx.reaction("tabulator .row_added_base")
    def row_added(self, *events):
        ...
matkuki commented 4 years ago

Tried it, same error:

JS: Command that failed to encode:
JS: INVOKE,TabulatorTableBaseStandard_34,_emit_at_proxy,[object Object]
matkuki commented 4 years ago

Hey @almarklein , I managed to create a self contained example that produces the error. Here it is: tabulator_raw_flexx.zip

Pressing the button shows the error in the console, then if you comment out line 127 in tabs.py, it works (the row actually gets inserted without error). The problem is that in my actual production code I need to propagate the row_added_base event from the CustomTabulator to it's parent through an emmiter.

If you need any extra information, please let me know.

matkuki commented 4 years ago

@almarklein If there are any other ways of propagating an event from a flx.Widget to a flx.PyWidget, please let me know. I'm a bit stuck at this point and don't know of how to solve this.

Thanks

almarklein commented 4 years ago

I think what's going on is that you're adding data to the emitted event that can't be serialized. In the browser console, below the to lines that you reported, it mentions the actual error: TypeError: cannot encode object of type RowComponent.

Since you're listening for the event in Python, Flexx will transfer it over the websocket, and must thus serialize the data, and it does not know what a RowComponent is. The best solution is probably to extract the necessary data (into e.g. a dict) and send that over instead. Also mind that undefined cannot be serialized (but null . None can).

(It is technically also possible to define a serialization for custom objects, but there is not a proper public API for this yet, and I suspect it is overkill in this use-case.)

matkuki commented 4 years ago

Ahhhh, you are absolutely right! I forgot that the Tabulator rowAdded callback uses a RowComponent as a parameter. Excellent, that's an easy fix. As you recomended, I'll just repackage the RowComponent into a dictionary.

Thanks again!

almarklein commented 4 years ago

🎆