Create a template table from message object

I want to create a table using a template node.

I have a message object composed of circuit boards identified by serial numbers. Each circuit board has two channels that each have their own outputs.

These numbers may change though, for example some circuit boards in the future will have more channels or I will add another circuit board to the system. So I need the table to be created presumably using nested loops to traverse the message object and pull out whatever information is there at the time.

Here is an example of my object:

{ "serialNum": {
            "1a": {
                    "channel": {
                        "0": {
                            "status": "running",
                            "value" : 100.0,
                            "setpoint": 200.0
                        },
                        "1": {
                            "status": "running",
                            "value" : 100.1,
                            "setpoint": 200.1
                        }
                    }
                },
            "2a": {
                    "channel": {
                        "0": {
                            "status": "running",
                            "value" : 200.0,
                            "setpoint": 300.0
                        },
                        "1": {
                            "status": "running",
                            "value" : 200.1,
                            "setpoint": 300.1
                        }
                    }
                },
            "3a": {
                    "channel": {
                        "0": {
                            "status": "stopped",
                            "value" : 300.0,
                            "setpoint": 400.0
                        },
                        "1": {
                            "status": "stopped",
                            "value" : 300.1,
                            "setpoint": 400.1
                        }
                    }
                }
            }
    }

The table should look like this:
.table_example

I think my key problem is not really understanding how to loop through the object in the template node to create the table. So I'm hoping someone here can provide some guidance on how to do this because surely this must be pretty simple. Here is my flow, thanks!:

[{"id":"3cb165b3.a1b84a","type":"tab","label":"Flow 6","disabled":false,"info":""},{"id":"28eaaa6e.7ca216","type":"function","z":"3cb165b3.a1b84a","name":"setup object","func":"var boards = {}\n\nboards = { \"serialNum\": {\n            \"1a\": {\n                    \"channel\": {\n                        \"0\": {\n                            \"status\": \"running\",\n                            \"value\" : 100.0,\n                            \"setpoint\": 200.0\n                        },\n                        \"1\": {\n                            \"status\": \"running\",\n                            \"value\" : 100.1,\n                            \"setpoint\": 200.1\n                        }\n                    }\n                },\n            \"2a\": {\n                    \"channel\": {\n                        \"0\": {\n                            \"status\": \"running\",\n                            \"value\" : 200.0,\n                            \"setpoint\": 300.0\n                        },\n                        \"1\": {\n                            \"status\": \"running\",\n                            \"value\" : 200.1,\n                            \"setpoint\": 300.1\n                        }\n                    }\n                },\n            \"3a\": {\n                    \"channel\": {\n                        \"0\": {\n                            \"status\": \"stopped\",\n                            \"value\" : 300.0,\n                            \"setpoint\": 400.0\n                        },\n                        \"1\": {\n                            \"status\": \"stopped\",\n                            \"value\" : 300.1,\n                            \"setpoint\": 400.1\n                        }\n                    }\n                }\n            }\n    }\n\nmsg.boardMsg = boards\nreturn msg;","outputs":1,"noerr":0,"x":513.560791015625,"y":283.78646087646484,"wires":[["b98cf582.ac0278","c26e6df8.fbfdd"]]},{"id":"9c1798b1.5f63e8","type":"inject","z":"3cb165b3.a1b84a","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":305.5642318725586,"y":283.8802366256714,"wires":[["28eaaa6e.7ca216"]]},{"id":"b98cf582.ac0278","type":"ui_template","z":"3cb165b3.a1b84a","group":"5fc682ac.589f8c","name":"","order":1,"width":0,"height":0,"format":"<!DOCTYPE html>\n<html>\n    \n<style>\ntable, th , td  {\n  border: 1px solid grey;\n  border-collapse: collapse;\n  padding: 5px;\n}\ntable tr:nth-child(odd) {\n  background-color: #f1f1f1;\n}\ntable tr:nth-child(even) {\n  background-color: #ffffff;\n}\n</style>\n\n<table id=\"board status\" border=\"1\" width = 1000px height = 400px>\n    <thead>\n    <tr>\n        <th>Serial #</th>\n        <th>Channel #</th>\n        <th>Status</th>\n        <th>Value</th>\n        <th>Setpoint</th>\n    </tr>\n    </thead>\n    \n    <tbody ng-repeat=\"serial in msg.boardMsg.serialNum\">\n            <tr ng-repeat=\"channelNum in msg.boardMsg.serialNum[serial].channel\">\n            <td> {{serial}} </td>\n            <td>{{channelNum}}</td>\n            <td ng-style = \"{color : msg.boardMsg.serialNum[serial].channel[channelNum].status === 'stopped' ? 'red' : 'green'}\">{{msg.boardMsg.serialNum[serial].channel[channelNum].status == \"stopped\" ? \"stopped\" : \"running\"}}</td>  \n            <td>{{msg.boardMsg.serialNum[serial].channel[channelNum].value</td>\n            <td>{{msg.boardMsg.serialNum[serial].channel[channelNum].setpoint</td>\n        </tr>\n    </tbody>\n</table>\n\n\n\n\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":745.0086669921875,"y":288.00000524520874,"wires":[[]]},{"id":"c26e6df8.fbfdd","type":"debug","z":"3cb165b3.a1b84a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":721.5590744018555,"y":195.76044178009033,"wires":[]},{"id":"5fc682ac.589f8c","type":"ui_group","z":"","name":"template","tab":"a25212a6.c3a72","order":1,"disp":true,"width":25,"collapse":false},{"id":"a25212a6.c3a72","type":"ui_tab","z":"","name":"template","icon":"dashboard","order":8,"disabled":false,"hidden":false}]

I have progressed a little bit. I can now generate the table I want but when I try to use DataTables so I can sort the rows I get bizarre behavior. If you keep refreshing the browser, the table switches between styles and the little sort ascending/descending arrows seemingly randomly appear then go away. Also, clearly the way I am using ng-repeat-start is not correct because the table will have a row that says "no data available in table."

In my flow I also tried flattening my object, hoping to circumvent this whole mess and use just one ng-repeat, but now I don't even think that will solve this.

Below is my flow and a couple screenshots of how the table looks now as you keep clicking the refresh button:

[{"id":"3cb165b3.a1b84a","type":"tab","label":"Flow 6","disabled":false,"info":""},{"id":"28eaaa6e.7ca216","type":"function","z":"3cb165b3.a1b84a","name":"setup object","func":"var boards = {}\n\nboards = { \"serialNum\": {\n            \"1a\": {\n                    \"channel\": {\n                        \"0\": {\n                            \"status\": \"running\",\n                            \"value\" : 100.0,\n                            \"setpoint\": 200.0\n                        },\n                        \"1\": {\n                            \"status\": \"running\",\n                            \"value\" : 100.1,\n                            \"setpoint\": 200.1\n                        }\n                    }\n                },\n            \"2a\": {\n                    \"channel\": {\n                        \"0\": {\n                            \"status\": \"running\",\n                            \"value\" : 200.0,\n                            \"setpoint\": 300.0\n                        },\n                        \"1\": {\n                            \"status\": \"running\",\n                            \"value\" : 200.1,\n                            \"setpoint\": 300.1\n                        }\n                    }\n                },\n            \"3a\": {\n                    \"channel\": {\n                        \"0\": {\n                            \"status\": \"stopped\",\n                            \"value\" : 300.0,\n                            \"setpoint\": 400.0\n                        },\n                        \"1\": {\n                            \"status\": \"stopped\",\n                            \"value\" : 300.1,\n                            \"setpoint\": 400.1\n                        }\n                    }\n                }\n            }\n    }\n\n/*var msgList = []\nconst boardIDs = Object.keys(boards.serialNum)\n\nfor (var x in boardIDs){\n    msg[boardIDs[x]] = boardIDs[x]\n}\nreturn msg*/\n\nvar msgList = []\nvar boardIDs = Object.keys(boards.serialNum)\nfor (var x in boardIDs){\n    var channelNum = Object.keys(boards.serialNum[boardIDs[x]].channel)\n    for (var y in channelNum){\n        var props = Object.keys(boards.serialNum[boardIDs[x]].channel[channelNum[y]])\n        for (var z in props){\n            value = boards.serialNum[boardIDs[x]].channel[channelNum[y]][props[z]]\n            msg[boardIDs[x] + \"_\" + channelNum[y] + \"_\" + props[z]] = value\n            msgList.push(boardIDs[x] + \"_\" + channelNum[y] + \"_\" + props[z])\n        }\n    }\n}\n\n\n\nmsg.boardMsg = boards\nmsg.list = msgList\nreturn msg;","outputs":1,"noerr":0,"x":513.560791015625,"y":283.78646087646484,"wires":[["c26e6df8.fbfdd","7595457.3bbcebc"]]},{"id":"9c1798b1.5f63e8","type":"inject","z":"3cb165b3.a1b84a","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":305.5642318725586,"y":283.8802366256714,"wires":[["28eaaa6e.7ca216"]]},{"id":"c26e6df8.fbfdd","type":"debug","z":"3cb165b3.a1b84a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":721.5590744018555,"y":195.76044178009033,"wires":[]},{"id":"7595457.3bbcebc","type":"ui_template","z":"3cb165b3.a1b84a","group":"5fc682ac.589f8c","name":"","order":1,"width":0,"height":0,"format":"<!DOCTYPE html>\n<html>\n<head>\n\n<!--<script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js\"></script>\n<link rel=\"stylesheet\" type=\"text/css\" href=\"/home/pi/DataTables/DataTables-1.10.20/css/jquery.dataTables.css\"/>\n<script type=\"text/javascript\" src=\"/home/pi/DataTables/DataTables-1.10.20/js/jquery.dataTables.js\"></script> -->\n\n\n<script src=\"https://ajax.googleapis.com/ajax/libs/jquery/3.3.1/jquery.min.js\"></script>\n<link rel=\"stylesheet\" type=\"text/css\" href=\"https://cdn.datatables.net/v/dt/dt-1.10.20/datatables.min.css\">\n<script type=\"text/javascript\" charset=\"utf8\" src=\"https://cdn.datatables.net/v/dt/dt-1.10.20/datatables.min.js\"></script>\n\n<style>\ntable, th , td  {\n  border: 1px solid grey;\n  border-collapse: collapse;\n  padding: 5px;\n}\ntable tr:nth-child(odd) {\n  background-color: #f1f1f1;\n}\ntable tr:nth-child(even) {\n  background-color: #ffffff;\n}\n\n</style>\n\n<script type=\"text/javascript\" class=\"init\">\n\n      $(document).ready(function() {\n        $('#boards').dataTable();\n      } );\n\n</script>\n\n</head>\n\n<table id=\"boards\" class=\"display\" style=\"width:100%\">\n    <thead>\n    <tr>\n        <th>Serial #</th>\n        <th>Channel #</th>\n        <th>Status</th>\n        <th>Value</th>\n        <th>Setpoint</th>\n    </tr>\n    </thead>\n    <tbody>\n    <tr ng-repeat-start=\"(serialNum, serialVal) in msg.boardMsg.serialNum\"></tr>\n        <tr ng-repeat=\"(channelNum, channelVal) in serialVal.channel\">\n            <td> {{serialNum}} </td>\n            <td>{{channelNum}}</td>\n            <td ng-style = \"{color : channelVal.status === 'stopped' ? 'red' : 'green'}\">{{channelVal.status == \"stopped\" ? \"stopped\" : \"running\"}}</td>  \n            <td>{{channelVal.value}}</td>\n            <td>{{channelVal.setpoint}}</td>\n        </tr>\n    \n    <tr ng-repeat-end></tr>\n    </tbody>\n    \n</table>\n</html>\n\n\n\n\n\n\n\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":757.0086669921875,"y":353.7795104980469,"wires":[[]]},{"id":"5fc682ac.589f8c","type":"ui_group","z":"","name":"template","tab":"a25212a6.c3a72","order":1,"disp":true,"width":25,"collapse":false},{"id":"a25212a6.c3a72","type":"ui_tab","z":"","name":"template","icon":"dashboard","order":8,"disabled":false,"hidden":false}]

as you keep clicking the refresh button

The idea is that the data is refreshed based on the input, not on the browser refresh.

But that aside, I think the setup of your object is not practical. You use property names as values (ie 1a, 2a), the channels are also objects, they would have made it easiers in an array.

I would just make an array of objects instead:

{
	"devices": [
		{
			"serial": "1a",
			"channel": 0,
			"status": "running",
			"value": 100,
			"setpoint": 200
		},
		{
			"serial": "1a",
			"channel": 1,
			"status": "running",
			"value": 100,
			"setpoint": 200
		}
	]
}

No need for nested loops.

@bakman2 thanks for the suggestion. That would work to eliminate the nested ng-repeats.

However, I have managed to get this working (mostly) with nested ng-repeat using jquery tablesorter. The sorting aspect works perfectly, however I am having some conflicts I think with multiple jquery copies being loaded.

I have issues like this: https://github.com/Mottie/tablesorter/issues/1489
and this: https://github.com/Mottie/tablesorter/issues/1179

but when it loads properly, it's exactly what I want:

Do you have any suggestions on how to fix this? I have tried the .noconflict() approach and I have tried setTimeout but neither solves the issue.

FWIW, the key thing to get this working was to use a <tr> tag with ng-repeat-start and ng-repeat-end. I also had to still include a <tbody>. Finally I had to go into the my nodered settings.js file and set httpStatic: '/home/pi/node-red-static/' I then downloaded the jquery tablesorter files into that folder.

I also have a dedicated template node for the html head, but that actually doesn't seem to matter. It seems I could put all the code into my main template node and use <html> tags.

Here is my flow. There are some comments in there for DataTables as well. However DataTables is not capable of sorting a table in the way I have set it up (nested rows.)

[{"id":"3cb165b3.a1b84a","type":"tab","label":"Flow 6","disabled":false,"info":""},{"id":"28eaaa6e.7ca216","type":"function","z":"3cb165b3.a1b84a","name":"setup object","func":"var boards = {}\n\nboards = { \"serialNum\": {\n            \"1a\": {\n                    \"channel\": {\n                        \"0\": {\n                            \"status\": \"running\",\n                            \"value\" : 100.0,\n                            \"setpoint\": 200.0\n                        },\n                        \"1\": {\n                            \"status\": \"running\",\n                            \"value\" : 100.1,\n                            \"setpoint\": 200.1\n                        }\n                    }\n                },\n            \"2a\": {\n                    \"channel\": {\n                        \"0\": {\n                            \"status\": \"running\",\n                            \"value\" : 200.0,\n                            \"setpoint\": 300.0\n                        },\n                        \"1\": {\n                            \"status\": \"running\",\n                            \"value\" : 200.1,\n                            \"setpoint\": 300.1\n                        }\n                    }\n                },\n            \"3a\": {\n                    \"channel\": {\n                        \"0\": {\n                            \"status\": \"stopped\",\n                            \"value\" : 300.0,\n                            \"setpoint\": 400.0\n                        },\n                        \"1\": {\n                            \"status\": \"stopped\",\n                            \"value\" : 300.1,\n                            \"setpoint\": 400.1\n                        }\n                    }\n                }\n            }\n    }\n\n/*var msgList = []\nconst boardIDs = Object.keys(boards.serialNum)\n\nfor (var x in boardIDs){\n    msg[boardIDs[x]] = boardIDs[x]\n}\nreturn msg*/\n\nvar msgList = []\nvar boardIDs = Object.keys(boards.serialNum)\nfor (var x in boardIDs){\n    var channelNum = Object.keys(boards.serialNum[boardIDs[x]].channel)\n    for (var y in channelNum){\n        var props = Object.keys(boards.serialNum[boardIDs[x]].channel[channelNum[y]])\n        for (var z in props){\n            value = boards.serialNum[boardIDs[x]].channel[channelNum[y]][props[z]]\n            msg[boardIDs[x] + \"_\" + channelNum[y] + \"_\" + props[z]] = value\n            msgList.push(boardIDs[x] + \"_\" + channelNum[y] + \"_\" + props[z])\n        }\n    }\n}\n\n\n\nmsg.boardMsg = boards\nmsg.list = msgList\nreturn msg;","outputs":1,"noerr":0,"x":513.560791015625,"y":283.78646087646484,"wires":[["c26e6df8.fbfdd","7595457.3bbcebc"]]},{"id":"9c1798b1.5f63e8","type":"inject","z":"3cb165b3.a1b84a","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":305.5642318725586,"y":283.8802366256714,"wires":[["28eaaa6e.7ca216"]]},{"id":"c26e6df8.fbfdd","type":"debug","z":"3cb165b3.a1b84a","name":"","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":721.5590744018555,"y":195.76044178009033,"wires":[]},{"id":"7595457.3bbcebc","type":"ui_template","z":"3cb165b3.a1b84a","group":"5fc682ac.589f8c","name":"","order":1,"width":0,"height":0,"format":"<style>\ntable, th , td  {\n  border: 1px solid grey;\n  border-collapse: collapse;\n  padding: 5px;\n}\ntable tr:nth-child(odd) {\n  background-color: #f1f1f1;\n}\ntable tr:nth-child(even) {\n  background-color: #ffffff;\n}\n\n</style>\n</head>\n\n<table id=\"boards\" class=\"tablesorter\" >\n    <thead>\n    <tr>\n        <th>Serial #</th>\n        <th>Channel #</th>\n        <th>Status</th>\n        <th>Value</th>\n        <th>Setpoint</th>\n    </tr>\n    </thead>\n    <tbody>\n    <tr ng-repeat-start=\"(serialNum, serialVal) in msg.boardMsg.serialNum\"></tr>\n        <tr ng-repeat=\"(channelNum, channelVal) in serialVal.channel\">\n            <td> {{serialNum}} </td>\n            <td>{{channelNum}}</td>\n            <td ng-style = \"{color : channelVal.status === 'stopped' ? 'red' : 'green'}\">{{channelVal.status == \"stopped\" ? \"stopped\" : \"running\"}}</td>  \n            <td>{{channelVal.value}}</td>\n            <td>{{channelVal.setpoint}}</td>\n        </tr>\n    <tr ng-repeat-end></tr>\n    </tbody>\n    \n</table>\n\n<!--\n<script type=\"text/javascript\">\n\n//the timeout is necessary to allow the source to load\nsetTimeout(function() {\n    $('#boards').dataTable();\n},100 );\n\n</script> -->\n\n<script type=\"text/javascript\">\n//the timeout is necessary to allow the source to load, it usually loads in under 100ms, but not always, so 200 is better choice\nsetTimeout(function() {\n    $('#boards').tablesorter();\n},500);\n</script>\n\n\n\n\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"local","x":757.0086669921875,"y":353.7795104980469,"wires":[[]]},{"id":"7966faa9.07d074","type":"ui_template","z":"3cb165b3.a1b84a","group":"930683c4.e4232","name":"head","order":0,"width":0,"height":0,"format":"<link rel=\"stylesheet\" href=\"http://pinodered.local:1880/tablesorter/tablesorter-master/css/theme.default.css\">\n<script type=\"text/javascript\" src=\"http://pinodered.local:1880/tablesorter/tablesorter-master/docs/js/jquery-latest.min.js\"></script>\n<script type=\"text/javascript\" src=\"http://pinodered.local:1880/tablesorter/tablesorter-master/js/jquery.tablesorter.js\"></script>\n<script type=\"text/javascript\" src=\"http://pinodered.local:1880/tablesorter/tablesorter-master/js/jquery.tablesorter.widgets.js\"></script> \n\n<!-- works with the Datatables js\n<link rel=\"stylesheet\" href=\"http://pinodered.local:1880/DataTables/DataTables-1.10.20/css/jquery.dataTables.min.css\">\n<script type=\"text/javascript\" charset=\"utf8\" src=\"http://pinodered.local:1880/DataTables/DataTables-1.10.20/js/jquery.dataTables.min.js\"></script>-->\n\n","storeOutMessages":true,"fwdInMessages":true,"templateScope":"global","x":494.55902099609375,"y":395.6875228881836,"wires":[[]]},{"id":"5fc682ac.589f8c","type":"ui_group","z":"","name":"template","tab":"a25212a6.c3a72","order":1,"disp":true,"width":25,"collapse":false},{"id":"930683c4.e4232","type":"ui_group","z":"","name":"Group 1","tab":"","order":1,"disp":true,"width":"16","collapse":true},{"id":"a25212a6.c3a72","type":"ui_tab","z":"","name":"template","icon":"dashboard","order":8,"disabled":false,"hidden":false}]

Not as much fun as rolling your own - but we do have a ui-widget for tables available if you wanted and were willing to manipulate your data format

@dceejay Thanks, but I have come so far at this point and the table is working, I just need to clear up this issue with what appears to be duplicate copies of jquery conflicting with each other.

Dashboard already loads jquery so you shouldn't have to

@dceejay So it looks like the src is the biggest offender.

I removed: <script type="text/javascript" src="http://pinodered.local:1880/tablesorter/tablesorter-master/docs/js/jquery-latest.min.js"></script>

The page loads without any errors now about 90% of the time. I will still occassionally get a "tablesorter is not a function" but everything looks good and works fine regardless.

The only problem has been occassionally the table is not fully expanding in height and has just a sliver of it visible with scroll bars to the right.

I'm also not sure about the setTimeout I have in my html. I tried using the .ready() instead and I still get errors, and if the table does load, sometimes the sort does not actually function. With the setTimeout, if it loads properly, it functions properly as far as I can tell.

<script type="text/javascript">
//the timeout is necessary to allow the source to load, it usually loads in under 100ms, but not always, so 200-500 is better choice
setTimeout(function() {
    $('#boards').tablesorter();
},500);
</script>

compared with

<script type="text/javascript">
$(document).ready(function() {
    $('#boards').tablesorter();
});
</script>

Seems a bit over-complex having to use both jQuery & Angular & a jQuery addin.

If you aren't using other features from Dashboard, uibuilder's default template gives you a powerful but simple to use data table via bootstrap-vue.

You can see it in use here. Complete with sorting, embedded details sub-table, colour highlighting and more:

1 Like

@TotallyInformation Thanks for the hint. I think this will work

So I took a look and I don't really understand how to use bootstrap-vue, but I see that tablesorter, jquery, and angular can all be loaded with uibuilder. So I did that and then just stuck in all the code and message object I already have. For some reason it complains it cannot find jquery using the relative path so I had to use an online source.

I get an output. It only complains about my style sheet but not the tablesorter stuff. However, it looks like to me that I'm not actually accessing my message object as you can see here:

Maybe this is some simple change I have to make now that I am using uibuilder?

I accidentally overwrote the default index.html file and cannot seem to find that anywhere now. But I had the default stuff in there about using Vue to access messages. I now have an html file called table.html. Here is what I have in that:

<!doctype html>
<html>
<head>
<link rel="stylesheet" href="../uibuilder/vendor/tablesorter/dist/css/theme.default.css">
<script src="../uibuilder/vendor/angular/index.js"></script>

<style>
table, th , td  {
  border: 1px solid grey;
  border-collapse: collapse;
  padding: 5px;
}
table tr:nth-child(odd) {
  background-color: #f1f1f1;
}
table tr:nth-child(even) {
  background-color: #ffffff;
}

</style>
</head>

<body>
<table id="boards" class="tablesorter" height = 100% >
    <thead>
    <tr>
        <th>Serial #</th>
        <th>Channel #</th>
        <th>Status</th>
        <th>Value</th>
        <th>Setpoint</th>
    </tr>
    </thead>
    <tbody>
    <tr ng-repeat-start="(serialNum, serialVal) in msg.boardMsg.serialNum"></tr>
        <tr ng-repeat="(channelNum, channelVal) in serialVal.channel">
            <td> {{serialNum}} </td>
            <td>{{channelNum}}</td>
            <td ng-style = "{color : channelVal.status === 'stopped' ? 'red' : 'green'}">{{channelVal.status == "stopped" ? "stopped" : "running"}}</td>  
            <td>{{channelVal.value}}</td>
            <td>{{channelVal.setpoint}}</td>
        </tr>
    <tr ng-repeat-end></tr>
    </tbody>
    
</table>


<!-- These MUST be in the right order. Note no leading / -->
<!-- REQUIRED: Socket.IO is loaded only once for all instances
                 Without this, you don't get a websocket connection -->
<script src="../uibuilder/vendor/socket.io/socket.io.js"></script>

<script src="../uibuilder/vendor/vue/dist/vue.js"></script> <!-- dev version with component compiler -->
<!-- <script src="../uibuilder/vendor/vue/dist/vue.min.js"></script>   prod version with component compiler -->
<!-- <script src="../uibuilder/vendor/vue/dist/vue.runtime.min.js"></script>   prod version without component compiler -->
<script src="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.js"></script>


<!-- REQUIRED: Sets up Socket listeners and the msg object -->
<!-- <script src="./uibuilderfe.js"></script>   //dev version -->
<script src="./uibuilderfe.min.js"></script> <!--    //prod version -->
<!-- OPTIONAL: You probably want this. Put your custom code here -->
<script src="./index.js"></script>


<!-- not found for some reason <script type="text/javascript" src=../uibuilder/vendor/jquery/dist/jquery.js></script>-->
<script type="text/javascript" src="http://code.jquery.com/jquery-1.7.1.min.js"></script>
<script type="text/javascript" src="../uibuilder/vendor/tablesorter/dist/js/jquery.tablesorter.js"></script>
<script type="text/javascript" src="../uibuilder/vendor/tablesorter/dist/js/jquery.tablesorter.widgets.js"></script>


<script type="text/javascript">
//the timeout is necessary to allow the source to load, it usually loads in under 100ms, but not always, so 200 is better choice
setTimeout(function() {
    
    $('#boards').tablesorter({
        widgets : ["zebra", "columns"]});
},500);
</script>
</body>
</html>

I only have these errors when I load the page:

Maybe only the style one is important.

Perhaps though you can guide me on how to access the msg object because it looks like that is the source of most of the trouble. Thanks for the help!!!

ahhh, maybe this is not exactly "easy"

I found your jquery example and am taking a look now.

But if I can figure out how to get the msg object I think this will work really nicely.

You need to have:

<div id="app">

your code

</div>

Add type="text/css" to your CSS links - that should fix the refusing to load issue.

You can't access the msg object because index.js has an error. require is a node.js function and is not available in the browser.

Also you don't want to load Vue if you are loading Angular and you may not need Angular anyway if you have jQuery. Each of these libraries is able to display dynamic content quite easily.

This shows you how to do a jQuery-only version with uibuilder: https://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki/Example:-JQuery

This example has a complex table in bootstrap-vue: https://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki/Examples-Vue-Complex-Home-Dashboard

However, I would recommend starting with something a lot simpler. Bootstrap-vue also has 2 simpler forms of table as well. To use it, all you need is a data variable that is an array of objects.

[
    {
        "col1": "item1", "col2": 15, "col3": "On"
    },
    ...
]

So each property of the objects in the array represents a column. By default the table component will take the first entry in the array and build the columns out of that. You can specify a separate data object that defines the columns in great detail though if you need to (as my example does) - adding calculated columns, doing formatting, sorting, handling clicks, etc.


To use the msg object, you need to use uibuilder's event system. This is as simple as using the uibuilder.onChange function.

Here is the jQuery version:

    uibuilder.onChange('msg', function(msg){
        console.info('property msg changed!')
        console.dir(msg)
        $('#showMsg').text(JSON.stringify(msg))
    })

The default Vue version is almost identical except that you don't have to change the DOM yourself, Vue will do that, all you need to do is update the data variable.

One final point - you need to initialise uibuilder in the browser by including uibuilder.start(). If you don't, you won't get access to any of the functions. Originally it started automatically but that doesn't let you pass some parameters that are required if you want to put some code in a sub-folder.

It really is. I think that problem is that you've tried to dive straight into something complex by copying existing code.

Temporarily, go back to some thing simple so you can see how it works. Try this simple example with Vue to get a feel for just how simple it really is.

You don't need multiple libraries which is also confusing you. As I say, Vue, jQuery and Angular all have the ability to dynamically update the page, you don't need all of them. Just pick one.

With jQuery, around 1/2 dozen lines of html and a similar number of lines of JavaScript are all you need to get going. With Vue, you need about the same html but more JS initially. However, you will likely end up writing less code with Vue as your app evolves. It is also easier to structure your code in Vue. But without a doubt, jQuery is the easiest to get going with if you know some programming but aren't especially familiar with web development.

Gee - this all looks so much simpler than reformatting the data for the existing node-red-contrib-ui-table node :wink:

1 Like

Haha! Yes, fair point.

However, the OP was trying to use combinations of jQuery and Angular which is very likely to end badly in my view.

All he really needs is jQuery - well and uibuilder to make the comms easy :smiley:

That will certainly make people happy if they have to use the UI from a mobile device.

uibuilder is never going to be quite as easy as Dashboard to get started with.

@bakman2 thanks, I understand now why I need that.

@dceejay what can I say, I like to suffer a little, but I think this will be worth it.

@TotallyInformation access from a mobile device will be awesome.

So I am using the quote of the day example and that has been really, really helpful.

I basically tweaked a couple lines so it displays my msg object. I am seeing that I need to use a v-if statement if I want to drill down and display my properties of my object. But I have not really got that sorted out completely yet for some reason (I get Cannot read property of undefined). I'll paste my flow in a little bit if I still can't get it. The page displays, but having a error is likely bad.

Ultimately, yes I should jump on this Vue-bootstrap wagon, I was just trying to roll with the limited stuff I know which is angular. The ng-repeat-start allowed me to work with my object exactly as it is and definitely works perfectly to sort the table, I was just dealing with jquery conflicts when using the standard dashboard.

Thank you all for your help and patience. I've learned a ton already.

Yes, those things are very similar to Angular. For the undefined issue, that might just be a timing thing? You can often get around that by defining a default set of values in the data section so that Vue has something to work with even before your updated data has arrived.

Apologies for derailing you!

Personally, I've never liked Angular ever since I built something with it that, while it worked, it was appallingly complex and I couldn't understand it when I came back a couple of months later.

That's not to "diss" Dashboard, it is an amazing achievement and has helped so many people including me. It's just that I dislike being artificially restricted and having to fight frameworks when they should be helping. Dashboard gets you going but then you fall off a cliff and end up fighting with it just to get things done. That's why I've tried to get uibuilder to do just enough to be helpful without getting in the way of creativity. Well, that's what I hope anyway.

No "diss" taken :slight_smile:
As stated the Dashboard project has always been a "side" project from the core of Node-RED - albeit a major side project, just because it has been so necessary and useful. It was always meant to be opinionated/limited so it was easy to get going and use consistently. As with anything UI related everyone has an opinion/style and these days these change all the time so rapidly we could never keep up or satisfy everything - so the ui-builder is a great addition it that it lets people bring whatever tools they like to the party.
So yes all for a bit of derailing if it gets the job done.

2 Likes