Publish an HTML Mustache template with “live data” to a web page

I am fairly new to CSS, HTML and Node-Red and only recently started using these tools. I have read and looked at many examples trying to figure this all out. I am using the dashboard and it works very well, but I would like a separate web page (that is not part of the dashboard) since the display format will be quite different. I have started looking into UIBuilder (it looks very promising) and I was able to add and display some JSON data in the “Normal Messages”, but I need to better understand Vue.js. I also looked at the Lego “Front-end Website with Node-RED: Example Form” which uses AJAX but I understand that even less. :confused:

I have a simple HTML template that I developed for formatting emails using the Mustache template, which is working (although I am not sure why there are extra line breaks when it is displayed in Office365 mail?). For the purposes of testing, I am trying to use the same template for a web page. Please see the attached code (Note that I have removed the email address).

[{"id":"8abc961a.097828","type":"http in","z":"d934eba.5ef7618","name":"","url":"/hello","method":"get","upload":false,"swaggerDoc":"","x":140,"y":220,"wires":[["6412a186.6b94a"]]},{"id":"6412a186.6b94a","type":"template","z":"d934eba.5ef7618","name":"page","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<html>\n    <head></head>\n    <body>\n        <h1>Hello World!</h1>\n    </body>\n</html>","x":670,"y":220,"wires":[["3217a9ba.41bb26"]]},{"id":"3217a9ba.41bb26","type":"http response","z":"d934eba.5ef7618","name":"","statusCode":"","headers":{},"x":910,"y":220,"wires":[]},{"id":"dafb8629.ba1a18","type":"template","z":"d934eba.5ef7618","name":"HTTP Template","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<style> \n    \n    p {\n        background-color: yellow;\n        color:black;\n        display: inline;\n        margin: 0,0,0,0;\n        height: 12px;\n    }\n    \n    .txtcolor {\n        color: {{payload.data.color}}; \n    };\n   \n</style>\n\n<HTML>\n    {{payload.data.color}}\n    {{payload.data.val}}\n    <br> <br>\n    <p> The value is:</p> <p class=\"txtcolor\">{{payload.data.val}}</p>\n    <p> The color is:</p> <p class=\"txtcolor\">{{payload.data.color}}</p>\n</HTML>","output":"str","x":700,"y":400,"wires":[["730ff254.6c1a2c","9072fba8.7c66e8"]]},{"id":"730ff254.6c1a2c","type":"http response","z":"d934eba.5ef7618","name":"HttpResponse","statusCode":"","headers":{},"x":940,"y":360,"wires":[]},{"id":"730a2966.a7b028","type":"inject","z":"d934eba.5ef7618","name":"","topic":"","payload":"inc","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":620,"wires":[["dca904b4.74e0a8"]]},{"id":"fc896279.1b11","type":"function","z":"d934eba.5ef7618","name":"Prepare HTML","func":"\n\nvar val;\nvar color;\n\nif (context.get(\"val\") === undefined) {\n    val = 5;\n    context.set(\"val\",val);\n}\n\n\nif (msg.payload === \"inc\") {\n    val = context.get(\"val\") + 1;\n    context.set(\"val\",val);\n}\n\nif (msg.payload === \"dec\") {\n    val = context.get(\"val\") - 1;\n    context.set(\"val\",val);\n}\n\nif (val > 5) {\n    color = 'red';\n} else { \n    color = 'green';\n}\n\nmsg.payload = {\n    \"data\": {\n        \"val\": val,\n        \"color\": color\n    }\n};\n\nmsg.url = \"/myData\";\nreturn msg;","outputs":1,"noerr":0,"x":320,"y":400,"wires":[["dafb8629.ba1a18","4a3d0f41.53458"]]},{"id":"e1f508dd.0a1368","type":"inject","z":"d934eba.5ef7618","name":"","topic":"","payload":"dec","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":660,"wires":[["dca904b4.74e0a8"]]},{"id":"415c4742.0d6228","type":"http in","z":"d934eba.5ef7618","name":"","url":"/myData","method":"get","upload":false,"swaggerDoc":"","x":110,"y":380,"wires":[["fc896279.1b11"]]},{"id":"9072fba8.7c66e8","type":"debug","z":"d934eba.5ef7618","name":"Http_Debug2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":930,"y":400,"wires":[]},{"id":"a1d1eb0c.044328","type":"e-mail","z":"d934eba.5ef7618","server":"smtp.gmail.com","port":"465","secure":true,"tls":false,"name":"","dname":"Send Email","x":930,"y":620,"wires":[]},{"id":"dca904b4.74e0a8","type":"function","z":"d934eba.5ef7618","name":"Prepare Test Email","func":"// Prepare the color & value payload\nvar val;\nvar color;\n\nif (context.get(\"val\") === undefined) {\n    val = 5;\n    context.set(\"val\",val);\n}\n\n\nif (msg.payload === \"inc\") {\n    val = context.get(\"val\") + 1;\n    context.set(\"val\",val);\n}\n\nif (msg.payload === \"dec\") {\n    val = context.get(\"val\") - 1;\n    context.set(\"val\",val);\n}\n\nif (val > 5) {\n    color = 'red';\n} else { \n    color = 'green';\n}\n\nmsg.payload = {\n    \"data\": {\n        \"val\": val,\n        \"color\": color\n    }\n};\n\n\n// Add the msg info information for email\nmsg.to = \"FName.LName@gmail.com\";\nmsg.from = \"Me\";       \nmsg.topic = \"Email Test\";\n\n\nreturn msg;\n","outputs":1,"noerr":0,"x":430,"y":620,"wires":[["63c87554.d3bbec","4a905567.448d3c","dafb8629.ba1a18"]]},{"id":"5a48c159.3527f","type":"debug","z":"d934eba.5ef7618","name":"Email Debug2","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":940,"y":660,"wires":[]},{"id":"63c87554.d3bbec","type":"template","z":"d934eba.5ef7618","name":"eMial Template","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<style> \n    \n    p {\n        background-color: yellow;\n        color:black;\n        display: inline;\n        margin: 0,0,0,0;\n        height: 12px;\n    }\n    \n    .txtcolor {\n        color: {{payload.data.color}}; \n    };\n   \n</style>\n\n<HTML>\n    {{payload.data.color}}\n    {{payload.data.val}}\n    <br> <br>\n    <p> The value is:</p> <p class=\"txtcolor\">{{payload.data.val}}</p>\n    <p> The color is:</p> <p class=\"txtcolor\">{{payload.data.color}}</p>\n</HTML>","output":"str","x":700,"y":620,"wires":[["a1d1eb0c.044328","5a48c159.3527f"]]},{"id":"4a905567.448d3c","type":"debug","z":"d934eba.5ef7618","name":"Email Debug1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":700,"y":580,"wires":[]},{"id":"4a3d0f41.53458","type":"debug","z":"d934eba.5ef7618","name":"Http Debug1","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","x":690,"y":480,"wires":[]},{"id":"e418ff5a.96aee","type":"inject","z":"d934eba.5ef7618","name":"","topic":"","payload":"inc","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":440,"wires":[["fc896279.1b11"]]},{"id":"4413081b.8d6d58","type":"inject","z":"d934eba.5ef7618","name":"","topic":"","payload":"dec","payloadType":"str","repeat":"","crontab":"","once":false,"onceDelay":0.1,"x":130,"y":480,"wires":[["fc896279.1b11"]]}]

The first time I deploy Node-Red and refresh the web page (/myData), it looks the way I expect it to look.
image

When I refresh the web page, the “val=5” disappears? Why?
image

For the purpose of the example I have created an Inc / Dec Inject node. It increments / decrements the val to simulate a changing payload. If val > 5 the color should be red and if val <=5 the color should be green. The plan is to continuously update the payload (approx. every 3s - not shown for the purpose of this example) and use this data with the Mustache Template to populate the web page.

There seem to be some “conditions” that are causing it to work the very first time when Node-Red is first deployed (Restart Flows), so is there something that I could do / add that would simulate this behavior? (I guess the first time it loads, it publishes the static web page which has the mustache payload data loaded. I am assuming that after the first time, the mustache payload data becomes null, although I don’t understand why “green” is still there?). In other words, as the payload val is changed, can it “update” the web page with the new payload data? Would this require a $scope.watch or some AJAX stuff? Examples would be greatly appreciated. Thank you.

I don't understand the purpose of the flow, but in the prepare HTML function node you do the following:

if (context.get("val") === undefined) {
    val = 5;
    context.set("val",val);
}

You set val to 5, context.get("val") and will no longer be undefined - when you refresh the val variable will not exist, an else clause is needed - what if context.get("val") is defined and has a value ?

To simplify the function:

m = msg.payload
val = context.get("val") || 5

if (m === "inc") { val++ }
if (m === "dec") { val-- }

color = (val>5) ? "red" : "green"
context.set("val",val)

msg.payload = {data:{val:val,color:color}}

return msg;

It will set val to 5 if context.get("val") does not exist or is null (OR clause)
In the end set the context once before sending the msg.

Also note that msg.url = "/myData"; is not used by the http response node (line can be removed)

Hi backman,

Thank you, I see that your code is a lot more efficient and works much better than the code I provided, although I still do not understand why?

if (context.get("val") === undefined) {
    val = 5;
    context.set("val",val);
}

If val is undefined I set it to 5. (It believe it would be undefined only when NodeRed restarts the flows?). After it has been defined, I save it - context.set("val",val), then I read it back - context.get("val") and increment or decrement the value, and then save it back. I don't understand why it would be undefined but I know it is not working? Is it not stored immediately in the Context variable? What am I missing here?

Now that I have implemented your change and removed the msg.url, it is working! :smiley: The purpose of this function was to quickly change the value of val and see if the web site is updated. After implementing your fix, I now see that it updates correctly, however I now realize that I need to manually "reload" the page.

So the big question is: How can I use the incoming payload to automatically update the web page?

I was hoping that using an incoming payload with the mustache template (or a timer) that the values would be automatically updated, like they are with emails.

Because you don’t check if it was defined. if it is defined, ‘val’ is never set.

OK, got it! Thank you.

So is there a way to automatically update the web page with fresh data when the payload changes?

errr - yes - all the node-red-dashboard nodes do that.

I am using the Dashboard and am very pleased with how it works. :smiley: It is configured to work well with mobile devices. However, I have another page which is a different format and is based on filling an entire 1920 X 1280 pixel screen (vs a much smaller mobile screen).

I would like to have separate web page (to be used as a display board) which which would automatically display and update "real-time" data from a msg.payload using the (mustache format) with no user input required or needed.

I think I may have found a solution at the link below using web sockets:
https://flows.nodered.org/flow/8666510f94ad422e4765
I will review the code and try to understand how the web sockets work.

As a follow up to this post, I made some progress in updating a web page with "realtime" data using the example flow posted in the previous link.

I am now having difficulty setting up a second web service for another web page. I started by making a copy of the existing flow on Flow 3 sheet, and renaming the http In URL along with the WebService In / Out paths as follows:

URL: simple --> AndonBoard
Path: ws/simple --> ws/AndonBoard

When I deploy the application, the status under "ws/simple" is displayed as "connected", but there is no status indication under "ws/AndonBoard ". Also, when I inspect the Configuration Nodes, the web socket listener ws/simple is shown under Flow 3 (where is was created) but ws/AndonBoard is shown under All Nodes (even though it was also created on the Flow 3 sheet). This does not seem to be correct.

I restarted Node-Red and also deleted and re-created the "AndonBoard " web sockets, but this did not help. Also deleted the two ws/AndonBoard nodes and recreated them from scratch. This too did not help. Not sure where to go from here?

Is it permitted to have two sets of web sockets on the same Flow sheet? Where should they appear - under the Flow 3 sheet where it was created and copied or under All Nodes?

For the AndonBoard nodes, I also made the following changes in the javascript:

var ws;
var wsUri = "ws:";
var loc = window.location;
console.log(loc);
if (loc.protocol === "https:") { wsUri = "wss:"; }
// This needs to point to the web socket in the Node-RED flow
// ... in this case it's ws/AndonBoard
wsUri += "//" + loc.host + loc.pathname.replace("AndonBoard","ws/AndonBoard");

Thanks in advance for your help.

I apologize for all these posts, I am really new to all this web stuff. It is like drinking from a fire hose! I was playing around with Chrome inspect and got this screen shot which may help. It seems like it doesn't like the following line:

ws = new WebSocket(wsUri);

which is trying to create a new web socket. Perhaps it is conflicting with the other node? They are both in separate nodes.

var ws;
var wsUri = "ws:";
var loc = window.location;
console.log(loc);
if (loc.protocol === "https:") { wsUri = "wss:"; }
// This needs to point to the web socket in the Node-RED flow
// ... in this case it's ws/AndonBoard
wsUri += "//" + loc.host + loc.pathname.replace("AndonBoard","ws/AndonBoard");

function wsConnect() {
    console.log("connect",wsUri);
    ws = new WebSocket(wsUri);
    
    ws.onmessage = function(msg) {
        // parse the incoming message as a JSON object
        var data = msg.data;
        
        // Get data and transfer to inner HTML elements
        var LineData = JSON.parse(msg.data);
        
        //document.getElementById('modePayload').innerHTML = "<strong>" + LineData.WO.ModeName + "</strong>";
        //document.getElementById('modePayload').innerHTML = "ABC";
        //document.getElementById('modePayload').style.backgroundColor = "red"
        
    }
    
    ws.onopen = function() {
        // update the status div with the connection status
        document.getElementById('status').innerHTML = "connected";
        ws.send("Open for data");
        console.log("connected");
    }
    
    ws.onclose = function() {
        // update the status div with the connection status
        //document.getElementById('status').innerHTML = "not connected";
        // in case of lost connection tries to reconnect every 3 secs
        setTimeout(wsConnect,3000);
    }
}

function doit(m) {
    if (ws) { ws.send(m); }
}

You can have multiple websockets - you need to make sure you add a new one using
image
from the dropdown, rather than editing the existing one .

Thanks dceejay, that solved it.