Ui-table in conjunction with ui-table handler

Hi all,

I must say that the table situation in Node-Red is quite challenging, at least from a beginners perspective. And dont get me wrong, of course versatile tools as ui-tabe a very powerful. I am just thinking about very simple tables where individual cells could be updated without higher programming effort.

Therefore I really appreciate the efforts of @Christian-Me for providing a handy tool (ui-table handler) serving as interface between the input data and ui-table.
But Chris, what would really help is a minimum working example. BTW: It's worth thinking about to provide one in the flow section together with your subflow. As a matter of fact I am not able to make it work. Below I have provided a small example flow. Could you please help sorting my issues out ? I think, the example is self-explanatory. Your help is really appreciated.


Sorry for using an external link, but I could not insert the flow here (Body is limited to 32000 characters; you entered sth > 32000). I think this comes from your subflow :wink:

Have you tried this demo I prepared recently:

1 Like

Thanks CHris for the fast response,

No, haven't found this demo. Just downloaded it and will investigate. Thanks. I just have realized, that you have made a new version. Thanks. Will report later.

Thanks again (also writing from Berlin ;-))

1 Like

Dear @Christian-Me,

this is all fabulous. I understood the working principle so far. Really good job. I even have started playing around with styling and layout. That is easier than expected... Thank you so much :smiley:

I wonder if you may help me finding a solution for the following situation: I would like to change the text color of individual cells depending on the value. E.g. for values between 100-70:green, 70-40:orange and 40-0:red. I have already realized that using the progress bar (as built in function in Tabulator). However, the changing text color corresponds to what I am actually looking for. Do you know any examples where this was done ?

However, whatever your answer will be... Thank you so much!

Hi .... Yes This can be done.

It was in my 1st table you can find in the examples. This is the HTML Version (look into column BOOST_STATE-value


but the more elegant way to do:

  • you will need a formatter callback more here
  • I use a function node as an editor and the copy paste it into the visual JSON editor (to escape all inverted commas) CTRL-A + CTRL-V
var customFormatter = function (cell, formatterParams, onRendered) {
    //cell - the cell component
    //formatterParams - parameters set for the column
    //onRendered - function to call when the formatter has been rendered

    let value = cell.getValue();
    let color = 'orange';
    if (value < 40) {
        color = 'red';
    } else if (value > 70) {
        color = 'green';
    cell.getElement().style.color = color;

    return value;

copy everything from function ( ... } into the formatter value.

Tip: The callbacks are running in the dashboard / browser so use the development tools for debugging. simple console.log() might help for easy bugs

You can alter the DOM with the style object ... so a lot of possibilities!

Good night :wink:

For the record and all forum members,

this is how I solved the conditional text colors...

                "title": "<i class='fa fa-clock-o'></i>&nbsp;Last seen",
                "field": "lastseen",
                "formatter": "function(cell, formatterParams, onRendered){var value = cell.getValue();  if(value < 60) {cell.getElement().style.color=\"green\";} else {cell.getElement().style.color=\"red\";} return value+'&nbsp;&nbsp;'+'minutes';}"

Hi @Christian-Me,

I am getting deeper and deeper into this table stuff (quite time consuming I must admit, but I thinks it's worth spending time here). Maybe a bit naive but I am trying to 'change' the cell values. E.g. the table gets minutes. If the value is less than 60 minutes I would like to have minutes. For larger values I would like to have hours. Additionally, for times larger than 12 hours the text colour should change to red. I tried to perform these calucaltions directly in the formatter. However, apparently I am on a completely wrong way. Do you have any suggestions how to achieve that ?

function(cell, formatterParams, onRendered) {
var value = cell.getValue(); 
if(value < 60) {
result = value + '&nbsp;&nbsp;' + 'Min';
} else if (value >60 && value <= 720) {
hrs = Math.floor(value/60);
result = hrs + '&nbsp;&nbsp;' + 'Std';
else {
hrs = Math.floor(value/60);
result = hrs + '&nbsp;&nbsp;' + 'Std';
return result;


Looking at the code everything looks totally fine. It will format the content and style the color of the text.

I believe you like to implement something like "last seen" or "last update". I think you can't do it with ui-table run on its own. But if you send regular updates in the interval you need it isn't difficult:

The trigger simulates a new update (reset last seen with a new timestamp). update Data does simple "formatting". The trigger node resents the last seen timestamp and delta does a simple Date.now()-timestamp

The formatter then looks like this (I reduced it to seconds for faster debugging)

function (cell, formatterParams, onRendered) {
    let updatedSince = Number(cell.getValue());
    if (Number.isNaN(updatedSince)) {
        cell.getElement().style.color = 'red';
        return 'never';
    if (updatedSince < 10) {
        cell.getElement().style.color = 'lime';
        return updatedSince + '&nbsp;&nbsp;' + 's (<10)';
    } else if (updatedSince < 20) {
        cell.getElement().style.color = 'orange';
        return updatedSince + '&nbsp;&nbsp;' + 's (<20)';
    cell.getElement().style.color = 'coral';
    return updatedSince + '&nbsp;&nbsp;' + 's (>20)';


Simply feed this flow into the ui-table-handler and add the "lastSeen" column

[{"id":"b6d568762ff16077","type":"trigger","z":"d2952ff54c89c139","name":"","op1":"","op2":"0","op1type":"pay","op2type":"str","duration":"-1","extend":false,"overrideDelay":false,"units":"s","reset":"","bytopic":"all","topic":"topic","outputs":1,"x":500,"y":1540,"wires":[["333515516f7ae884"]]},{"id":"df10a25a5973aef6","type":"inject","z":"d2952ff54c89c139","name":"trigger","props":[{"p":"payload"},{"p":"index","v":"1","vt":"num"},{"p":"column","v":"lastSeen","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payloadType":"date","x":170,"y":1540,"wires":[["d449b1a7538a3949"]]},{"id":"d449b1a7538a3949","type":"function","z":"d2952ff54c89c139","name":"update Data","func":"let value = msg.payload;\nmsg.payload = { id: msg.index};\nmsg.payload[msg.column] = value;\nreturn msg;","outputs":1,"noerr":0,"initialize":"// Code added here will be run once\n// whenever the node is started.\ncontext.set('index',0);","finalize":"","libs":[],"x":310,"y":1540,"wires":[["b6d568762ff16077"]]},{"id":"b56f7f19a054d87b","type":"debug","z":"d2952ff54c89c139","name":"update","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"payload","targetType":"msg","statusVal":"","statusType":"auto","x":810,"y":1540,"wires":[]},{"id":"89ac1fed6b1e1132","type":"inject","z":"d2952ff54c89c139","name":"reset","props":[{"p":"reset","v":"true","vt":"bool"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":330,"y":1580,"wires":[["b6d568762ff16077"]]},{"id":"333515516f7ae884","type":"function","z":"d2952ff54c89c139","name":"delta","func":"msg.payload.lastSeen = Math.floor((Date.now() - msg.payload.lastSeen) / 1000);\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","libs":[],"x":650,"y":1540,"wires":[["b952ac65.8ddfb","b56f7f19a054d87b"]]}]

I first tried only to resend the "lastSeen" timestamp. This does not work because the cell formatter is only triggered if the value changes. For many rows you should think about sending an array of all rows every minute (as this is your desired scope) to the table handler.

A last tip: Use a "unconnected" function node to edit your callbacks and then copy past the function into your JSON via the Visual Editor. By doing so you can take advantage of the syntax checking (i.e. undeclared variables) and don't worry about escaping inverted commas ... Don't use comments as they might break the complete code behind the comment as CR seams not to be translated into /n


Thanks once more for your entiring effort. This is all great and useful. However, my basic point here was the possibility to perform smaller calculations directly in the formatter. As shown above e.g. to calculate hours from minutes etc. At least during my tests that didn't work.

Than my last paragraph is relevant:


so addressing the issues here like this

gives you this




So everything as you could expect it.

But even with your issues the callback works as expected (at least when tested it)

So perhaps take a look in your browsers console or place a simple console.log({cell, formatterParams, onRendered, value:cell.getValue()}); at the beginning of your callback to check if it's even triggered. (sometimes refreshing the browser could help)


my demo data (expecting the callback in column test)

[{"id":"b974034a6e684cc8","type":"inject","z":"d2952ff54c89c139","name":"10","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"id\":2,\"test\":10}","payloadType":"json","x":190,"y":1660,"wires":[["3eda5cde9960731e"]]},{"id":"838e6b908e503b50","type":"inject","z":"d2952ff54c89c139","name":"80","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"id\":2,\"test\":80}","payloadType":"json","x":190,"y":1700,"wires":[["3eda5cde9960731e"]]},{"id":"8d63651866a8414d","type":"inject","z":"d2952ff54c89c139","name":"800","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"{\"id\":2,\"test\":800}","payloadType":"json","x":190,"y":1740,"wires":[["3eda5cde9960731e"]]},{"id":"3eda5cde9960731e","type":"link out","z":"d2952ff54c89c139","name":"table","mode":"link","links":["d010ebc3f8435624"],"x":305,"y":1700,"wires":[]}]

Yes! Not only basic stuff ... you have access to the DOM and and the complete table content via the cell component