Ui-table selection callback

I am trying to implement a callback to track the currently selected table row.

Here is my code.

msg.ui_control = {
    tabulator: {
        // This callback saves the current selected row
        rowSelected:function(row) {
            //row - row component for the selected row
            //flow.set('currentSelection', row)
            console.log('Yippee')
            //var newMessage = {
            //    'topic': this.config.topic,
            //    'ui_control': {
            //        'callback':'rowSelected',
            //        'row': row
            //    }
            //}
            //this.send([undefined, newmessage])
        },
        // This callback is triggered when ever the table contents change
        dataChanged:function(data) {
            //data - the updated table data
        },
        "columns": [
            {
                "title": "Torque",
                "field": "torque",
                "editor": "number",
                "editorParams": { "step": 0.1}
            },
            {
                "title": "Duration",
                "field": "duration",
                "editor": "number"
            }
        ],
        "selectable":true,
        "layout": "fitColumns",
        "tooltips":true,            //show tool tips on cells
        "history":true,             //allow undo and redo actions on the table
        "pagination":"local",       //paginate the data
        "paginationSize":14,         //allow 7 rows per page of data
        "movableRows": true,
        "height":"100%",
        "data": [
            {
            "torque": 20,
            "duration": 1000
            },{
            "torque": 1.5,
            "duration": 100
            },{
            "torque": 10,
            "duration": 1000
            },{
            "torque": 30,
            "duration": 1000
            },{
            "torque": 40,
            "duration": 1000
            },{
            "torque": 50,
            "duration": 1000
            }
        ]
    }
}
// Add an empty row to force header to be displayed

msg.payload = [{"command": "addRow"}]

return msg;

I never see the console.log("Yippee") in the node red journal.

Jun 30 19:18:08 dragon Node-RED[7255]: 30 Jun 19:18:08 - [info] Starting flows
Jun 30 19:18:08 dragon Node-RED[7255]: 30 Jun 19:18:08 - [debug] red/nodes/flows.start : starting flow : global
Jun 30 19:18:08 dragon Node-RED[7255]: 30 Jun 19:18:08 - [debug] red/nodes/flows.start : starting flow : 76e403d6.61e914
Jun 30 19:18:08 dragon Node-RED[7255]: 30 Jun 19:18:08 - [info] Started flows

That is all I get.

Any help gratefully received. I have looked at the examples and forum threads.
But I must be missing something

I think my mistake may be having the value of the rowSelected object as function type not a string type. Can anyone give me a definitive answer or pointer on this?

My code now looks like this.

msg.ui_control = {
    tabulator: {
        // This callback saves the current selected row
        rowSelected:"function(row) {"+
            "flow.set('currentSelection', row); "+
            "console.log('Yippee'); "+
            "var newMessage = { "+
                "'topic': this.config.topic, "+
                "'ui_control': { "+
                    "'callback':'rowSelected', "+
                    "'row': row "+
                "}"+
            "}"+
            "this.send([undefined, newmessage])"+
        "}",
        // This callback is triggered when ever the table contents change
        dataChanged:function(data) {
            //data - the updated table data
        },
        "columns": [
            {
                "title": "Torque",
                "field": "torque",
                "editor": "number",
                "editorParams": { "step": 0.1}
            },
            {
                "title": "Duration",
                "field": "duration",
                "editor": "number"
            }
        ],
        "selectable":true,
        "layout": "fitColumns",
        "tooltips":true,            //show tool tips on cells
        "history":true,             //allow undo and redo actions on the table
        "pagination":"local",       //paginate the data
        "paginationSize":14,         //allow 7 rows per page of data
        "movableRows": true,
        "height":"100%",
        "data": [
            {
            "torque": 20,
            "duration": 1000
            },{
            "torque": 1.5,
            "duration": 100
            },{
            "torque": 10,
            "duration": 1000
            },{
            "torque": 30,
            "duration": 1000
            },{
            "torque": 40,
            "duration": 1000
            },{
            "torque": 50,
            "duration": 1000
            }
        ]
    }
}
// Add an empty row to force header to be displayed

msg.payload = [{"command": "addRow"}]

return msg;

This deploys Ok, but the table is empty with only the header showing. I have lost the row data for some reason.

Thanks

The browser console shows this this

10:05:28.318 ui-table message arrived: <unavailable> app.min.js line 598 > eval:86:37
10:05:28.319 ui-table msg:  <unavailable> app.min.js line 598 > eval:88:37

I have no idea whether this good or bad.

I have got things narrowed down a bit more. My latest version of the flow demonstrates the columnMoved callback working as expected. But the rowSelectionChanged callback causing a "InternalError: too much recursion". The rowSelected callback never fires. I suspect the rowSelection column formatter has something to do with this.

This is the latest table setup node

[{"id":"845329d3.eb6ed","type":"change","z":"76e403d6.61e914","name":"Setup Tabulator","rules":[{"t":"set","p":"ui_control","pt":"msg","to":"{\"tabulator\":{\"columnMoved\":\"function(column, columns){     var newColumns=[];     columns.forEach(function (column) {         newColumns.push({'field': column._column.field});     });     this.send({topic:this.config.topic,ui_control:{callback:'columnMoved',columns:newColumns}}); }\",\"rowSelected\":\"function(row_selected){ this.send({topic:this.config.topic,ui_control:{'callback':'rowSelected','row':row_selected}}); }\",\"rowSelectionChanged\":\"function(data, rows){var newData=[]; data.forEach(function(data){newData.push(data);}); var newRows=[]; rows.forEach(function(row){newRows.push(row);}); this.send({topic:this.config.topic,ui_control:{callback:'rowSelectionChanged', data:newData, rows:newRows}});}\",\"layout\":\"fitColumns\",\"selectable\":1,\"movableColumns\":true,\"columns\":[{\"formatter\":\"rowSelection\",\"align\":\"center\",\"width\":\"2%\",\"headerSort\":false},{\"title\":\"Torque\",\"field\":\"torque\",\"editor\":\"number\",\"editorParams\":{\"step\":0.1}},{\"title\":\"Duration\",\"field\":\"duration\",\"editor\":\"number\"}]},\"customHeight\":12}","tot":"json"},{"t":"set","p":"payload","pt":"msg","to":"[{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10}]","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":440,"y":100,"wires":[["b0495229.d891d8","840cb515.88f298"]]}]

Here is the the overall flow

[{"id":"76e403d6.61e914","type":"tab","label":"Sandpit","disabled":false,"info":""},{"id":"840cb515.88f298","type":"ui_table","z":"76e403d6.61e914","group":"880623ea.2d0768","name":"tabulator","order":0,"width":"5","height":"10","columns":[],"outputs":1,"cts":true,"x":680,"y":100,"wires":[["df0037a9.300af8"]]},{"id":"3721ea12.95402e","type":"inject","z":"76e403d6.61e914","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":true,"onceDelay":0.1,"topic":"","payload":"","payloadType":"str","x":210,"y":100,"wires":[["845329d3.eb6ed"]]},{"id":"b0495229.d891d8","type":"debug","z":"76e403d6.61e914","name":"table input sandpit","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":730,"y":20,"wires":[]},{"id":"df0037a9.300af8","type":"debug","z":"76e403d6.61e914","name":"table output sandpit","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":910,"y":100,"wires":[]},{"id":"a09ac705.964108","type":"function","z":"76e403d6.61e914","name":"Dummy function for verifying callback Javascript","func":"// I use this editor to write callback functions and check that the Javascript is valid.\n// I then compress the function onto a single line and then use the contents of the line after\n// the equals sign as a string value for value of the relevant callback property in the tabulator object.\n\n// This is line is for the columnMoved callback\n// \"function(column, columns){     var newColumns=[];     columns.forEach(function (column) {         newColumns.push({'field': column._column.field});     });     this.send({topic:this.config.topic,ui_control:{callback:'columnMoved',columns:newColumns}}); }\"\nvar columnMoved = function(column, columns){     var newColumns=[];     columns.forEach(function (column) {         newColumns.push({'field': column._column.field});     });     this.send({topic:this.config.topic,ui_control:{callback:'columnMoved',columns:newColumns}}); }\n\n// This line is for the row SelectedCallback\n// \"function(row_selected){ this.send({topic:this.config.topic,ui_control:{callback:'rowSelected',row:row_selected}}); }\"\nvar rowSelected = function(row_selected){ this.send({topic:this.config.topic,ui_control:{callback:'rowSelected',row:row_selected}}); }// This line is for the row SelectedCallback\n\n// This line is for the row SelectedCallback\n// \"function(data, rows){ this.send({topic:this.config.topic,ui_control:{callback:'rowSelectionChanged', 'rows':rows, 'data':data}});}\"\nvar rowSelectionChanged = function(data, rows){console.log(`rowSelectionChanged triggered - data ${JSON.stringify(data)} rows ${JSON.stringify(rows)}`); this.send({topic:this.config.topic,ui_control:{callback:'rowSelectionChanged', rows:rows, data:data}});}\n","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":740,"y":320,"wires":[[]]},{"id":"845329d3.eb6ed","type":"change","z":"76e403d6.61e914","name":"Setup Tabulator","rules":[{"t":"set","p":"ui_control","pt":"msg","to":"{\"tabulator\":{\"columnMoved\":\"function(column, columns){     var newColumns=[];     columns.forEach(function (column) {         newColumns.push({'field': column._column.field});     });     this.send({topic:this.config.topic,ui_control:{callback:'columnMoved',columns:newColumns}}); }\",\"rowSelected\":\"function(row_selected){ this.send({topic:this.config.topic,ui_control:{'callback':'rowSelected','row':row_selected}}); }\",\"rowSelectionChanged\":\"function(data, rows){var newData=[]; data.forEach(function(data){newData.push(data);}); var newRows=[]; rows.forEach(function(row){newRows.push(row);}); this.send({topic:this.config.topic,ui_control:{callback:'rowSelectionChanged', data:newData, rows:newRows}});}\",\"layout\":\"fitColumns\",\"selectable\":1,\"movableColumns\":true,\"columns\":[{\"formatter\":\"rowSelection\",\"align\":\"center\",\"width\":\"2%\",\"headerSort\":false},{\"title\":\"Torque\",\"field\":\"torque\",\"editor\":\"number\",\"editorParams\":{\"step\":0.1}},{\"title\":\"Duration\",\"field\":\"duration\",\"editor\":\"number\"}]},\"customHeight\":12}","tot":"json"},{"t":"set","p":"payload","pt":"msg","to":"[{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10},{\"torque\":1,\"duration\":10}]","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":440,"y":100,"wires":[["b0495229.d891d8","840cb515.88f298"]]},{"id":"4df1d3a9.a99b54","type":"inject","z":"76e403d6.61e914","name":"inject","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":210,"y":160,"wires":[["5ba8034e.9b85ac"]]},{"id":"5ba8034e.9b85ac","type":"change","z":"76e403d6.61e914","name":"Setup from example","rules":[{"t":"set","p":"ui_control","pt":"msg","to":"{\"tabulator\":{\"columnMoved\":\"function(column, columns){     var newColumns=[];     columns.forEach(function (column) {         newColumns.push({'field': column._column.field});     });     this.send({topic:this.config.topic,ui_control:{callback:'columnMoved',columns:newColumns}}); }\",\"layout\":\"fitColumns\",\"movableColumns\":true,\"groupBy\":\"\",\"columns\":[{\"title\":\"eTorque\",\"field\":\"torque\",\"editor\":\"number\",\"editorParams\":{\"step\":0.1}},{\"title\":\"eDuration\",\"field\":\"duration\",\"editor\":\"number\"}]},\"customHeight\":12}","tot":"json"},{"t":"set","p":"payload","pt":"msg","to":"{\"command\":\"addRow\"}","tot":"json"}],"action":"","property":"","from":"","to":"","reg":false,"x":420,"y":160,"wires":[["8b10457e.b151a"]]},{"id":"8b10457e.b151a","type":"debug","z":"76e403d6.61e914","name":"table input example sandpit","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":740,"y":160,"wires":[]},{"id":"fff5c16e.a75478","type":"comment","z":"76e403d6.61e914","name":"N.B. Do not open the UI window until you have done the inject!!!!","info":"","x":350,"y":40,"wires":[]},{"id":"9f010854.96371","type":"ui_button","z":"76e403d6.61e914","name":"add_at_top","group":"8e465b51.c1e048","order":2,"width":0,"height":0,"passthru":false,"label":"Add at top","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":230,"y":440,"wires":[[]]},{"id":"616a46e7.0e817","type":"ui_button","z":"76e403d6.61e914","name":"add_at_bottom","group":"8e465b51.c1e048","order":3,"width":0,"height":0,"passthru":false,"label":"Add at bottom","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":240,"y":480,"wires":[[]]},{"id":"d0397e09.3bb17","type":"ui_button","z":"76e403d6.61e914","name":"run","group":"ff6f54a4.60fab","order":1,"width":0,"height":0,"passthru":false,"label":"Run","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":210,"y":260,"wires":[[]]},{"id":"84cd1d53.5d903","type":"ui_button","z":"76e403d6.61e914","name":"stop","group":"ff6f54a4.60fab","order":1,"width":0,"height":0,"passthru":false,"label":"Stop","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":210,"y":300,"wires":[[]]},{"id":"5d905b1.c3f26a4","type":"ui_button","z":"76e403d6.61e914","name":"step","group":"ff6f54a4.60fab","order":1,"width":0,"height":0,"passthru":false,"label":"Step","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":210,"y":340,"wires":[[]]},{"id":"aefaf.1db0c052","type":"ui_button","z":"76e403d6.61e914","name":"insert_above","group":"8e465b51.c1e048","order":3,"width":0,"height":0,"passthru":false,"label":"Insert above","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":230,"y":520,"wires":[[]]},{"id":"77c8a8b0.24515","type":"ui_button","z":"76e403d6.61e914","name":"insert_below","group":"8e465b51.c1e048","order":3,"width":0,"height":0,"passthru":false,"label":"Insert below","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":230,"y":560,"wires":[[]]},{"id":"93e7d240.91488","type":"ui_button","z":"76e403d6.61e914","name":"delete_selected","group":"8e465b51.c1e048","order":3,"width":0,"height":0,"passthru":false,"label":"Delete selected","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":240,"y":600,"wires":[[]]},{"id":"9b7e7d72.5f74e","type":"ui_button","z":"76e403d6.61e914","name":"delete_all","group":"8e465b51.c1e048","order":3,"width":0,"height":0,"passthru":false,"label":"Delete All","tooltip":"","color":"","bgcolor":"","icon":"","payload":"","payloadType":"str","topic":"topic","topicType":"msg","x":220,"y":640,"wires":[[]]},{"id":"880623ea.2d0768","type":"ui_group","name":"Programme","tab":"1d92acc1.5c9d03","order":2,"disp":true,"width":"5","collapse":false},{"id":"8e465b51.c1e048","type":"ui_group","name":"Edit","tab":"1d92acc1.5c9d03","order":3,"disp":true,"width":"3","collapse":false},{"id":"ff6f54a4.60fab","type":"ui_group","name":"Run","tab":"1d92acc1.5c9d03","order":1,"disp":true,"width":"3","collapse":false},{"id":"1d92acc1.5c9d03","type":"ui_tab","name":"Sandpit","icon":"dashboard","order":1,"disabled":false,"hidden":false}]

I seem to be whistling in the dark here. Once again any help gratefully received.

Hi,
Perhaps a look into the docs helps here:

  • use this.send({}) to pass result to Node-RED. (to avoid a loopback add ui_control.callback="someText" )
this.send({topic: "anyTopic",payload:"anyPayload",ui_control: {callback:"myCallback"}});

It seams that you miss the callback property if you get the too much recursion error.
For a deeper insight on callbacks perhaps take a look into example 6 and especially the debug node outputs.

On tip for coding the callbacks: I use a function node to code my callbacks (with basic syntax error checking) and then copy/paste into any node supporting typed inputs with JSON type support (like inject, change ore even buttons) and paste your code into the visual JSON editor tab doing most escaping of special characters for you.

Hope this helps. It isn't dark here :wink: but it is Summer and less timer to spend on the bench :sun_with_face:

To see the console.log()output of callbacks you have to take a look into the browser console (developer tools) of your dashboard as the callback is executed there. Here you also find error messages if any.

Hi Chris,

Thanks for your responses.

I have been looking in the browser console as well as the node red systemd journal for rhe console.log messages.

Thanks for the tip about putting the callback code in a function node to check it. I picked that up from the examples and have been doing that. This is what my dummy node contains.

// I use this editor to write callback functions and check that the Javascript is valid.
// I then compress the function onto a single line and then use the contents of the line after
// the equals sign as a string value for value of the relevant callback property in the tabulator object.
// "columnMoved": "function(column, columns){     var newColumns=[];     columns.forEach(function (column) {         newColumns.push({'field': column._column.field});     });     this.send({topic:this.config.topic,ui_control:{callback:'columnMoved',columns:newColumns}}); }",
// "rowSelected": "function(row_selected){ this.send({topic:this.config.topic,ui_control:{'callback':'rowSelected','row':row_selected}}); }",
// "rowSelectionChanged": "function(data, rows){var newData=[]; data.forEach(function(data){newData.push(data);}); var newRows=[]; rows.forEach(function(row){newRows.push(row);}); this.send({topic:this.config.topic,ui_control:{callback:'rowSelectionChanged', data:newData, rows:newRows}});}",

var columnMoved = function(column, columns) {
    var newColumns=[];
    columns.forEach( function (column) {
        newColumns.push({'field': column._column.field});
    });
    this.send({
        topic:this.config.topic,
        ui_control:{
            callback:'columnMoved',
            columns:newColumns
        }
    });
}

var rowSelected = function(row_selected) {
    this.send({
        topic:this.config.topic,
        ui_control:{
            'callback':'rowSelected',
            'row':row_selected            
        }
    });
}

var rowSelectionChanged = function(data, rows) {
    var newData=[];
    data.forEach( function (data) {
        newData.push(data);
    });
    var newRows=[];
    rows.forEach(function (row) {
        newRows.push(row);
    });
    this.send({
        topic:this.config.topic,
        ui_control:{
            callback:'rowSelectionChanged',
            data:newData,
            rows:newRows
        }
    });
}

I suspect that somewhere I am putting javascript instead of JSON or vice versa. I try to fully restart node red and the browser windows for each test, as I find that I sometimes end up running the previous version in the dashboard ui. I would be nice to find a way to clear the tabulator to default settings when the flow is restarted.

I have not looked in detail at example six. I will do that now.

Thank you for your excellent work on this and for putting up with my struggles. Go out and enjoy the summer, we have thunderstorms at the moment in the Lake District of Northwest England. It is raining buckets.

Roger

Ok,

Can anyone see the difference between these two functions?

// The this.send in this causes an excessive recursion failure
var rowSelectionChanged = function(data, rows) {
  var newData = [];
  data.forEach(function(data) {
    newData.push(data);
  });
  var newRows = [];
  rows.forEach(function(row) {
    newRows.push(row);
  });
  this.send({
    topic: this.config.topic,
    ui_control: {
      callback: 'rowSelectionChanged',
      data: newData,
      rows: newRows
    }
  });
}

// The this.send in this one does not.
columnMoved = function(column, columns) {
  var newColumns = [];
  columns.forEach(function(column) {
    newColumns.push({
      'field': column._column.field
    });
  });
  this.send({
    topic: this.config.topic,
    ui_control: {
      callback: 'columnMoved',
      columns: newColumns
    }
  });
}

I cannot. But maybe I have looked too often.

I have just noticed one. I am pushing an object reference rather than an object.
I will check that out later.

Roger

columns.forEach(function(column) {

You have columns instead of column At the start and the reverse at the end

I would not mess with the internals of the component objects (_column) as they might change in the future. Use the get / set functions instead. (there is a ton of useful methods available too)

var columnMoved = function(column, columns) {
  var newColumns = [];
  columns.forEach(function(column) {
    newColumns.push({
      'field': column.getField(),
      'width': column.getWidth()
    });
  });
  this.send({
    topic: this.config.topic,
    payload: newColumns,
    ui_control: {
      callback: 'columnMoved'
    }
  });
}

I added the width property for demonstration only (an object with only one property looks so lonely :wink:
You can put your payload outside the ui_control object too.

Hold on a minute guys. It is the first function that does not work. The second is taken from example three. I do not need to use that one. I just used it as a template for for using this.send to send a ui_control message to a ui_table node.

Roger

Ok, I see - But same "problem" here. rows is an array of row component objects.

instead of newRows.push(row); use newRows.push(row.getData());

A component object can't be converted to a JSON object and sent from the UI to the backend. In the end your callback will probably send back the hole table when the selection changes. Perhaps cherry pick the data you really need.

BTW: Data gives you the data to the last selected row and can be sent directly. Rows gives you a array of row components if you have multiple selections enabled:

This might work:

var rowSelectionChanged = function(data, rows) {
  var selectedRows = [];
  rows.forEach(function(row) {
    selectedRows.push(row.getData());
  });
  this.send({
    topic: this.config.topic,
    payload: data,
    selectedRows: selectedRows,
    ui_control: {
      callback: 'rowSelectionChanged'
    }
  });
}

I was just typing the following when your reply came in !

"Ok I have now got rowSelected and rowSelectionChanged callbacks work. But only if I I do not send any properties in the message that include objects that have been passed into the callback function. I am going to try JSON.stringify on the objects I need."

I will now follow your suggestion. Many many thanks. I am embarrassed to admit how much time I have wasted looking at this. The table data I am working with is very small.

Don't forget that you might gain knowledge by try and error :wink:
JSON.stringify() might not work with component objects (as they includes methods=code not only data). This is what caused your original issue as this.send does exactly that.
if you use a console.log()on your data you can investigate what type of data it is (and the methods you can use to access the data)


All properties starting with an _ indicates data of a component and should not accessed directly.

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.