Failure to create my first UI table

Hi folks,

I'm using the node-red-node-ui-table for the first time, but - although it seems a very nice component - I cannot get it working.

In the following example flow, I inject a message to fill the table with two scenario's for my home security system:

[{"id":"7d48e13.90ee22","type":"ui_table","z":"7d79c0ef.af3d3","group":"571a38b1.5e3638","name":"Scenarios","order":1,"width":"8","height":"3","columns":[{"field":"status","title":"Status","width":"30px","align":"left","formatter":"html","formatterParams":{"target":"_blank"}},{"field":"scenario","title":"Scenario","width":"70%","align":"left","formatter":"html","formatterParams":{"target":"_blank"}},{"field":"action","title":"Action","width":"20%","align":"left","formatter":"html","formatterParams":{"target":"_blank"}}],"outputs":1,"cts":true,"x":1100,"y":500,"wires":[["b8bec67c.99ebb8"]]},{"id":"b8bec67c.99ebb8","type":"debug","z":"7d79c0ef.af3d3","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"false","statusVal":"","statusType":"auto","x":1270,"y":500,"wires":[]},{"id":"93e4fcd0.05aa9","type":"inject","z":"7d79c0ef.af3d3","name":"","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"[{\"status\":\"<i class='fa fa-square-o'></i>\",\"scenario\":\"Scenario 1\",\"action\":\"<button type='button' onclick='$scope.send({payload: 1})'>Activate</button>\"},{\"status\":\"<i class='fa fa-square-o'></i>\",\"scenario\":\"Scenario 2\",\"action\":\"<button type='button' onclick='$scope.send({payload: 2})'>Activate</button>\"}]","payloadType":"json","x":910,"y":500,"wires":[["7d48e13.90ee22"]]},{"id":"571a38b1.5e3638","type":"ui_group","name":"Alarm scenarios","tab":"29ec6908.552b36","order":1,"disp":true,"width":"8","collapse":false},{"id":"29ec6908.552b36","type":"ui_tab","name":"Home","icon":"track_changes","order":1,"disabled":false,"hidden":false}]

And this is what I get:

image

There are a number of failures:

  1. The table doesn't fit the available space (i.e. I keep getting a horizontal scrollbar), although I have used percentages for the widths of the columns. This kind of styling stuff keeps failing on me every time ...
  2. I wanted to have buttons in the last column that send an output message when clicked. However I had to activate "send data on click" in order to get an output on the Table-node. But now an output message is being send whereever I click on a row. Not only on the button.

Would appreciate if anybody could help me to get a nice looking table, because the lady of the house is going to use it :cold_face:

Thanks!!
Bart

Seems that my click handler is called, but the $scope doesn't exist:

image

Not clear to me yet why I can't use the AngularJs scope in the html content for the last column:

image

Hi Bart.

  1. The column widths is not pixel perfect. I think it don’t take the (possible) vertical scrollbar into account. Try to play with the percentages or choose pixels with a little bit left over for the scrollbar.
  2. the default cellClick function should give you column and row information, as far as I remember or You have to write your own cellClickcallback function for the column of your choice only. Callback functions can be sent via a msg.ui_control Message. There are several treads and examples how to do that perhaps this one and the following messages

I have no clue in all Angular related stuff. Perhaps because tabulator is vanilla JS and so the Angular scope is lost?
Tabulator callbacks can access the widget context send function by using this.send();

Hey Chris,
Thanks for the tips!
I will try tomorrow evening if I get the this.send in the callback running, because my time is up for today.
Will get back here afterwards most probably with extra questions :wink:
Bart

My time too. A last Gin&Tonic ...

1 Like

Morning,
So if I keep the default cellClick function (to keep my flow simple), I could do it perhaps like this: instead of showing a button, I show a div without click handler. Because I get an output message for every cell in the row, and I only need to check whether the last column index has been clicked...

So I thought having a look at the style of the standard Node-RED buttons, to mimic those buttons with a div element in my table:

So I injected that style for the DIV element in the last column of my table:

image

But I'm not really blown away from the result:

image

I have to add some alignments, hover effects and so on. But it still looks a bit different compared to the button at the bottom? Is it perhaps of the background color of the table rows?

If anybody has styling advise, please let me know!!

Good Morning (? - think I had too many G&T with my wife yesterday :crazy_face:)
Have you tried this:


form here (Tabulator)
and insert a button instead of the icon? This should give you the desired hover effect and button look?

I hacked this into the interactive demo flow:

[
    {
        "field": "button",
        "title": "button",
        "visible": true,
        "formatter": "function(cell, formatterParams, onRendered){     return \"<button>Activate</button>\"; }"
    }
]

image
gives you that:
image

The topic carries the column info and the row show ... the row :wink:

[{"id":"ecf457e4.37b978","type":"ui_button","z":"c4712650.59b5e8","name":"","group":"ff9fdb9a.7da098","order":12,"width":"4","height":"1","passthru":false,"label":"add/show buttons","tooltip":"add a new column","color":"","bgcolor":"","icon":"","payload":"[{\"field\":\"button\",\"title\":\"button\",\"visible\":true,\"formatter\":\"function(cell, formatterParams, onRendered){     return \\\"<button>Activate</button>\\\"; }\"}]","payloadType":"json","topic":"columns","topicType":"str","x":736,"y":1088,"wires":[["16664cef.5b26b3"]]},{"id":"ff9fdb9a.7da098","type":"ui_group","name":"TEST","tab":"7dcc246f.ee661c","order":1,"disp":false,"width":"8","collapse":false},{"id":"7dcc246f.ee661c","type":"ui_tab","name":"TEST","icon":"dashboard","order":3,"disabled":false,"hidden":false}]

I'm completely not surprised. When you wrote yesterday "...last Gin&Tonic..." I thought to myself: all the information that he is sharing at this moment is unreliable, so I simply have to wait until he is sober again... :rofl:

Yes I had seen that already. But to be honest, I had no idea where it fitted into my flow :relaxed:

That is awesome information. I love it when you are sober :joy:. I really appreciate your support!!!
Will try it this evening. Now helping the son preparing for his stoichiometric examination... So I hope when I use an md-button in your callback function, that it automatically gets the correct theme look. Fingers crossed ...

2 Likes

OK I forgot the styling part:
image

I did a little bit copy paste:
form here
image

to here

[
    {
        "field": "button",
        "title": "Button",
        "visible": true,
        "formatter": "function(cell, formatterParams, onRendered){     return '<button class=\"md-raised md-button md-ink-ripple\">Activate</button>'; }"
    }
]

Although doesn't specifically answer your questions, there's some really useful ui_table information in this thread - Examples for node-red-node-ui-table which may assist.

1 Like

That looks VERY nice!!!!
Cannot wait to try it this evening...
One of the things that I didn't understand last night was the style changes of the dashboard buttons when you hoover them (the .md-button:hover in my screenshot above). That was not automatically applied when I used the md-button` style for some reason... I think that is the last missing piece in this issue.

Yes Paul, indeed. I have browsed through that thread and other articles and the example flow in the Import menu. But I was a bit confused how the ui_control worked. Thought first it was via the ui_control node, but then it seemed to be a field in the input message. I have never had time to play with the ui_control node yet, so it was very new to me ...

1 Like

@Christian-Me,
Hmmm, I got to the point where I am going to ask very stupid questions. But I'm stuck...

I have declared my 3 columns via the config screen:

image

But in the dropdown with build-in formatters there is no "Custom" option. Does this mean I have to:

  1. Choose format "HTML" (or whatever other type) for my third column, and inject a message at startup (with command "setColumns") to set my custom cell formatter on the third column (via
    "ui_control.tabulator.columns")?
  2. Leave the config screen empty (like in the ui_control example flow), add setup all my columns via the input message?

It would seem useful to me if a "Custom" formatter option would be available, and a related input field for the Javascript callback function code (that is displayed as soon as the "Custom" option is selected):

image

But unfortunately there is no TypedInput for Javascript code, because otherwise the field cannot be expanded ...

Hi Bart,

perhaps I have to dive deeper into the theory of operation of ui_contol messages in case of the ui-table node.

  1. the configuration done in the config UI is the foundation of the tabulator object configuring the table.
  2. each subsequent msg.ui_control.tabulator message add or alter the parameters used to configure the table. (they merge and NOT replace the current configuration) for maximum flexibility
  3. subsequent messages msg.ui_control.tabulator.columns also can add columns or add/alter parameters. To alter a parameter the unique field (called property in the ui) must match otherwise a new column will be created

So a mixed use of config UI settings and subsequent ui_control.tabulator messages is possible

In your case a node sending this should add the custom formatter to your "action" column

{
    "payload": null,
    "ui_control": {
        "tabulator": {
            "columns": [
                {
                    "field": "action",
                    "formatter": "function(cell, formatterParams, onRendered){     return '<button class=\"md-raised md-button md-ink-ripple\">Activate</button>'; }"
                }
            ]
        }
    }
}

I trigger a change or function node by the ui control node on change tab events in order to make it 100% sure that the configuration is up to date. Perhaps play around with example #6 and inspect what is sent to ui-table

The format definition of the column in the config UI then becomes overwritten to a custom formatter to plain text is the best choice. You can even alter the formatter function to make the label defined by the cell value replace 'Activate' by cell.getValue()

I use a function node to write my call back functions and copy paste them into a JSON (typed input) using the visual editor to take care that all inverted commas are escaped correctly (CTRL-A, DEL, CTRL-V).

  • the format of the column is in this case irrelevant as you create your own formatter later by ui_control
  • you can leave it empty and define all your colums via ui_control or mix both (see point 3 above). The example 6 use this (i.e. the id column is added by a ui_control message)

last but not least to your idea having a custom formatter option in the config ui:
It seams to be appealing in the beginning but as you wrote there is not an easy way to do this. I could think of the to use the JavaScript editor widget but I see many problems:

  • Placing it into the ui make it look easy for users but it is not. You have to gain basic knowledge of the tabulator API to do it right. (and read the docs) This could lead to frustration to users with less JavaScript know how.
  • where to stop? I use in my latest table 24 references to call back functions and I think 12 unique functions. i.e. you can add a legend to a progress bar formatter and define the columns .... or several context functions to trigger the context menu node at the right place :wink:
  • many build in formatters have already several parameters not represented in the UI.
  • before that missing build in formatters like the date time (important for sorting) should be added.
  • tabulator has so much to offer you will never able to cover everything by a config UI

Personally: I'm only a fan of low code to a certain point. If it ends in a ton of options in obscure places I preferer a few lines of good old code and full access to the APIs. Strangely the "computer literacy" (good old BBC) is lost over the years. I learned Basic on a PET (bevor taught in class) and quickly switched to Pascal as soon a compiler was available on XT PCs. Good old Pascal - It is still my foundation I base all my C/C++, Javascript and what not obscure scripting language I use today. My son did and will not not learn any programming language in German High school :frowning: . A big loss for the so called "digital natives" generation.

One extra thought: Can a visual JSON editor be done with a template in the background defining the possible properties and default values, including code / functions? A User can select options parameters form drop down lists, get basic info what this will do, type definitions, limits, default values, code prototypes and validation. Could be useful for many complex APIs - it is quite frustrating that a missing comma in a complex JSON can destroy your JSON with a click on Done.

So enough text for now ... (perhaps sometimes take a look in the ui-table handler subfow - this is my "workhorse" for all my tables)

Here my latest "creation":

and the JSON for this:

{
    "customHeight": 18,
    "tabulator": {
        "index": "id",
        "layout": "fitColumns",
        "movableColumns": true,
        "groupBy": "",
        "dataTree": true,
        "columns": [
            {
                "title": "<i class='fa fa-tag fa-rotate-90'></i>&nbsp;id",
                "field": "id",
                "width": 140,
                "frozen": true,
                "tooltip": true,
                "headerSort": false,
                "headerTooltip": "Element",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContextNoHide'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "title": "Device",
                "field": "deviceId",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "title": "Node",
                "field": "nodeId",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "title": "Property",
                "field": "propertyId",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "title": "Program",
                "field": "programId",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "title": "<i class='fa fa-map-marker'></i>&nbsp;Bezeichung",
                "field": "label",
                "width": 100,
                "headerTooltip": "Alias Name",
                "tooltip": true,
                "headerSort": true,
                "editor": "autocomplete",
                "editorParams": {
                    "freetext": true,
                    "allowEmpty": true,
                    "showListOnEmpty": true,
                    "values": true
                },
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "formatterParams": {
                    "outputFormat": "HH:mm",
                    "inputFormat": "x",
                    "invalidPlaceholder": ""
                },
                "title": "<i class='fa fa-undo'></i>&nbsp;Start",
                "field": "start",
                "topCalc": "function(values, data, calcParams){     var secs = 0;     var pad = function (num) {         return ('0'+num).slice(-2);     };          values.forEach(function(value){         secs+=Number(value);     });          var minutes = Math.floor(secs / 60);     secs = secs%60;     var hours = Math.floor(minutes/60);     minutes = minutes%60;     return pad(hours)+':'+pad(minutes)+':'+pad(secs); }",
                "editor": "number",
                "formatter": "datetime",
                "width": 40,
                "headerSort": true,
                "headerTooltip": "Startzeit",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "title": "<i class='fa fa-undo'></i>&nbsp;Dauer",
                "field": "duration",
                "formatter": "function(cell, formatterParams, onRendered) {     var pad = function (num) {         return ('0'+num).slice(-2);     };          var secs = Number(cell.getValue());     if (Number.isNaN(secs)) return;     var minutes = Math.floor(secs / 60);     secs = secs%60;     var hours = Math.floor(minutes/60);     minutes = minutes%60;     var days = Math.floor(hours/24);     hours = hours%24;     if (days>0)         return days+'d '+pad(hours)+':'+pad(minutes);     else         return pad(hours)+':'+pad(minutes)+'.'+pad(secs);  }",
                "editor": "number",
                "editorParams": {
                    "min": 1,
                    "max": 60,
                    "step": 1
                },
                "width": 40,
                "headerSort": false,
                "headerTooltip": "Übergangszeit in minuten",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            },
            {
                "formatterParams": {
                    "min": 0,
                    "max": 100,
                    "color": [
                        "#061a00",
                        "#0d3300",
                        "#134d00",
                        "#1a6600",
                        "#208000",
                        "#269900",
                        "#2db300",
                        "#33cc00",
                        "#39e600",
                        "#40ff00"
                    ],
                    "legend": "function (value) {return \"<span style='color:#FFFFFF;'>\"+value+\" %</span>\";}",
                    "legendColor": "#FFFFFF",
                    "legendAlign": "center"
                },
                "title": "<i class='fa fa-wifi'></i>&nbsp;Wert",
                "field": "value",
                "formatter": "progress",
                "editor": "number",
                "editorParams": {
                    "min": 0,
                    "max": 100,
                    "step": 5
                },
                "width": 70,
                "headerSort": false,
                "headerTooltip": "Eingestellter Wert",
                "headerContext": "function(e,column){ this.send({ui_control:{callback:'headerContext'},position:{\"x\":e.x,\"y\":e.y},payload:column._column.field}); e.preventDefault(); }"
            }
        ],
        "columnResized": "function(column){     var newColumn = {         field: column._column.field,         visible: column._column.visible,         width: column._column.width,         widthFixed: column._column.widthFixed,         widthStyled: column._column.widthStyled     }; this.send({topic:this.config.topic,ui_control:{callback:'columnResized',columnWidths:newColumn}}); }",
        "columnMoved": "function(column, columns){     var newColumns=[];     columns.forEach(function (column) {         newColumns.push({'field': column._column.definition.field, 'title': column._column.definition.title});     });     this.send({topic:this.config.topic,ui_control:{callback:'columnMoved',columns:newColumns}}); }",
        "rowFormatter": "function(row){     var data = row.getData();     switch (data.$state) {         case \"lost\":             row.getElement().style.backgroundColor = \"#9e2e66\";             row.getElement().style.color = \"#a6a6a6\";             break;         case \"sleeping\":             row.getElement().style.backgroundColor = \"#336699\";             break;         case \"disconnected\":             row.getElement().style.backgroundColor = \"#cc3300\";             row.getElement().style.color = \"#a6a6a6\";             break;         case \"alert\":             row.getElement().style.backgroundColor = \"#A6A6DF\";             break;         case \"init\":             row.getElement().style.backgroundColor = \"#f2f20d\";             break;         case \"ready\":             row.getElement().style.backgroundColor = \"\";             row.getElement().style.color = \"\";             break;         } }",
        "rowContext": "function(e, row){     this.send({ui_control:{callback:'rowContext'},position:{\"x\":e.x,\"y\":e.y},payload:row.getData(),\"topic\":row.getData().id});          e.preventDefault(); }",
        "cellEdited": "function(cell){          this.send({         ui_control:(cell.getColumn().getField()===\"value\") ? {callback:'valueEdited'} : {callback:'cellEdited'},                  payload:cell.getValue(),         oldValue:cell.getOldValue(),         field:cell.getColumn().getField(),         id:cell.getRow().getCell('id').getValue()     });  }",
        "rowMoved": "function(row){     var rowOrder=[];     row._row.parent.rows.forEach((row,index) => {         rowOrder.push(row.data.id);     });     this.send({ui_control:{\"callback\":'rowMoved',\"rowOrder\":rowOrder}}); }",
        "rowTap": "function(e, row){this.send({ui_control:{callback:'rowTap'},position:{\"x\":e.x,\"y\":e.y},payload:{\"$name\":row._row.data.$name,\"$localip\":row._row.data.$localip,\"name\":row._row.data.name},\"topic\":row._row.data.id}); e.preventDefault();}"
    }
}

And the flow (without the actual handling of the data and context menues is quite trivial)


OK ... there are 500+ lines of (to be reworked) javascipt code in the handler subflow :wink:

1 Like

Hey @Christian-Me,
It is very kind of you to explain it all in detail!! Hopefully others can also benefit from your explanation afterwards ...
That indeed clarifies a lot of things. So I am making progress...
BTW. your contextmenu table looks very nice!

Here the same... Completely no interest in programming...
My heart start bleeding when talking about it...

Some time ago I created a fork of Node-RED to control the user input in a json based on a specified json schema. I had already made a lot of progress, but due to a lack of time I have not been able to complete it... But I haven't seen anything about a code editor. You could do that, but then there should be a TypedInput for Javascript code, and that doesn't exist.

Another question: I would like to have the cell content centered vertically, which is required because I made the rows a bit higher (via "customHeight": 15). So based on the tabulator documentation I tried the two possible ways:

image

But both ways fail:

image

This error is generated in the tabulator code:

image

Because the this.defaultOptionList contains a lot of options, but no option for vertical cell alignment.
Have you already tried this yourself?
It makes no sense to my why the option it isn't in that list...

Thanks!

Hi, just quick ... your analyse is totally correct ... searching through the release infos of tabulator, there are issues in the alignment properties ... But there is hope ... Upgrading to the latest version vertAlign seams to work as expected (tested on the number column) in the latest version 4.9.3:

image

But rolling out an upgrade perhaps will need a little bit more testing. Think you can grab a test version form my GitHub tomorrow (or so)

Chris

1 Like

I pushed the upgrade to my Github (node-red-ui-nodes/node-red-node-ui-table at master · Christian-Me/node-red-ui-nodes · GitHub) perhaps you like to test it

Hmmm ... The customHeightparamter was not intended to customized the row hight:
The readme.md says:

by adding headers , footers , line or column grouping it is sometimes not possible to determine the amount of lines. Therefore the height can be defined by sending msg.ui_control.customHeight=lines

Is there an unknown side effect?

Will this also sweep up - node-red-node-ui-table regex problem with ":" · Issue #59 · node-red/node-red-ui-nodes · GitHub ?

I don't know, I hope :slight_smile: ... sorry I'm a regex noob ... But I will test it ... perhaps something to learn;)

I also have to test it with all demos and my own (quite complex) tables.
And I like to check out some of the new features.