Failure to create my first UI table

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.

Well I think the fix is/was in later level of tabulator - so as long as you are using new one it should be ok... finger crossed.

1 Like

Oh no, now I see it. Indeed I have totally misinterpreted that parameter usage...
Seems here that I need to set the tabulator row height via css...

Thanks again ...
Was not my intention to burry you under a pile of questions, but I had not thought in advance that I would have been stuck so often while creating a small table :roll_eyes:
But now it gets really embarrasing. Have tried a series of npm install commands, but cannot install your fork :flushed:. Seems that the commands that I always use, only work when the package.json file is in the root of the Github repository...
If somebody can share an install command, that would be nice!

I'm also still struggling to understand how to get my table columns to fill the available width in my dashboard

The tabulator documentation says this:

And my parent div fills nicely the available width, as offered by the dashboard:

image

And have two columns with a predefined width, and one column without width:

So I had expected that the third column would fill the available width, but instead the table becomes wider and I get a horizontal scrollbar :weary: (even before I inject row data):

image

I don't know what I have done wrong in my previous life to deserve this, but it must have been something very bad :thinking:

I don't know a easy way to install a package via npm form a GitHub repro subfolder. I would clone the repro and install it form the local directory.

To your width problem ... I always use the resizable columns ... as my ui-table handler record all the changes I adjust them by hand visually and if necessary prohibit resizing:
image

This adds up to 977px ... all collums have the exact spacing as recorded (and played back on connects).

So I have to dig deeper to investigate your problem ....

1 Like

Hi ... Sorry I didn't had enough time to dig deeper but please try to do a full deploy or even "restart flows" if you change parameters in the UI config ... The first thing I found out is that config seams not to get updated as expected in the frontend when you only deploy modified nodes.

Hi,

I did a lot of extensive testing .. and on my side everything work as expected.
Some mechanics of msg.ui_control messages as they are currently implemented in ui-table

  1. the config UI only defines the basic tabulator.columns array
  2. msg.ui_control overwrites the columns indentified by the field property
  3. subsequent changes in the UI don't effect the config in the frontend until flow restart when they where overwritten once by ui_control
  4. msg.ui_control.tabluator.columns=[] stops overwriting the UI config by stored ui_control messages . A page reload is necessary to get back to the UI config (could and should be improved somehow in the future)
  5. partial updates are possible as long the columns array contains the matching field property (see width inject)

This was done to enable serial modifications of the table layout starting from the config UI.

@dceejay The upgrade will take a while I already found a incompatibility: align is now hozAlign... and we don't want to break things :wink:

Here my simple test flow ...

[{"id":"a5af6227.a0126","type":"ui_table","z":"404c8482.ceaa2c","group":"ce5a47b6.8972d8","name":"","order":0,"width":0,"height":0,"columns":[{"field":"col1","title":"Column 1","width":"20%","align":"left","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"col2","title":"Column 2","width":"20%","align":"left","formatter":"plaintext","formatterParams":{"target":"_blank"}},{"field":"col3","title":"Column 3","width":"","align":"left","formatter":"plaintext","formatterParams":{"target":"_blank"}}],"outputs":1,"cts":true,"x":577,"y":408,"wires":[["d98d2869.3e9f58"]]},{"id":"55e9a602.229598","type":"inject","z":"404c8482.ceaa2c","name":"custom cols","props":[{"p":"ui_control","v":"{\"tabulator\":{\"layout\":\"fitColumns\",\"columns\":[{\"field\":\"col1\",\"title\":\"col1\",\"width\":\"25%\"},{\"field\":\"col2\",\"title\":\"col2\",\"width\":\"15%\"},{\"field\":\"col3\",\"title\":\"col3\"}]}}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payloadType":"str","x":223,"y":357,"wires":[["9a6831b3.b05e8"]]},{"id":"9a6831b3.b05e8","type":"function","z":"404c8482.ceaa2c","name":"","func":"msg.payload = null;\nreturn msg;","outputs":1,"noerr":0,"initialize":"","finalize":"","x":400,"y":408,"wires":[["a5af6227.a0126","17f90c7e.756c34"]]},{"id":"d98d2869.3e9f58","type":"debug","z":"404c8482.ceaa2c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":713,"y":408,"wires":[]},{"id":"17f90c7e.756c34","type":"debug","z":"404c8482.ceaa2c","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":577,"y":459,"wires":[]},{"id":"fce7b91.dc5bd48","type":"inject","z":"404c8482.ceaa2c","name":"clear columns","props":[{"p":"ui_control","v":"{\"tabulator\":{\"columns\":[]}}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":223,"y":459,"wires":[["9a6831b3.b05e8"]]},{"id":"302c6c48.cd2454","type":"inject","z":"404c8482.ceaa2c","name":"width","props":[{"p":"ui_control","v":"{\"tabulator\":{\"layout\":\"fitColumns\",\"columns\":[{\"field\":\"col1\",\"width\":\"25%\"},{\"field\":\"col2\",\"width\":\"15%\"},{\"field\":\"col3\",\"width\":\"\"}]}}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":203,"y":408,"wires":[["9a6831b3.b05e8"]]},{"id":"ce5a47b6.8972d8","type":"ui_group","name":"Vanilla Table","tab":"bae8e16f.a9faa","order":1,"disp":true,"width":"9","collapse":false},{"id":"bae8e16f.a9faa","type":"ui_tab","name":"ui-table","icon":"dashboard","order":1,"disabled":false,"hidden":false}]

Evening @Christian-Me,
Sorry for not responding earlier. But I got back to work and I already have to work this weekend. So unfortunately not much Node-RED time for me this week...
Will get back to you!
Thanks for investigating my questions!!!!!
Bart