"dependencies": {
"node-red-contrib-uibuilder": "*",
"riot": "^3.6.1"
} uibuilder: {
userVendorPackages: ['riot'],
debug: true
}[{"id":"502557a7.e19678","type":"debug","z":"106ba95c.ff91e7","name":"","active":true,"console":"false","complete":"true","x":570,"y":100,"wires":[]},{"id":"c6005228.32f9c","type":"uibuilder","z":"106ba95c.ff91e7","name":"a","url":"riot/","fwdInMessages":true,"customFoldersReqd":true,"x":419.9461975097656,"y":100.18403625488281,"wires":[["502557a7.e19678"]]},{"id":"db7c70c3.20f8f","type":"change","z":"106ba95c.ff91e7","name":"","rules":[{"t":"set","p":"payload","pt":"msg","to":"{ \"ts\": $now(), \"cool\":\"very\"}","tot":"jsonata"}],"action":"","property":"","from":"","to":"","reg":false,"x":260,"y":100,"wires":[["c6005228.32f9c"]]},{"id":"865a81d8.4e333","type":"inject","z":"106ba95c.ff91e7","name":"","topic":"","payload":"","payloadType":"date","repeat":"","crontab":"","once":false,"x":100,"y":100,"wires":[["db7c70c3.20f8f"]]}]h1 { color: #448}
h2 { color: #585}<!doctype html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">
<title>Node-RED UI Builder - RiotJS</title>
<meta name="description" content="Node-RED UI Builder - Testing RiotJS">
<link rel="icon" href="images/logo-red.png">
<!-- See https://goo.gl/OOhYW5 -->
<link rel="manifest" href="manifest.json">
<meta name="theme-color" content="#3f51b5">
<!-- Add to homescreen for Chrome on Android. Fallback for manifest.json -->
<meta name="mobile-web-app-capable" content="yes">
<meta name="application-name" content="Node-RED UI Builder">
<!-- Add to homescreen for Safari on iOS -->
<meta name="apple-mobile-web-app-capable" content="yes">
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
<meta name="apple-mobile-web-app-title" content="Node-RED UI Builder">
<!-- Homescreen icons
<link rel="apple-touch-icon" href="/images/manifest/icon-48x48.png">
<link rel="apple-touch-icon" sizes="72x72" href="/images/manifest/icon-72x72.png">
<link rel="apple-touch-icon" sizes="96x96" href="/images/manifest/icon-96x96.png">
<link rel="apple-touch-icon" sizes="144x144" href="/images/manifest/icon-144x144.png">
<link rel="apple-touch-icon" sizes="192x192" href="/images/manifest/icon-192x192.png">
-->
<link rel="stylesheet" href="vendor/normalize.css/normalize.css">
<link rel="stylesheet" href="index.css">
<!-- <script src="js/vendor/modernizr-2.8.3.min.js"></script> -->
</head>
<body>
<h1>Testing RiotJS</h1>
<!-- inlined tag definition, Custom tags can be empty, HTML only or JavaScript only -->
<my-tag></my-tag><!-- mount point -->
<script type="riot/tag">
<my-tag>
<h2>(0) Some local stuff first</h2>
<p>
msgCounter (global JS variable): {msgCounter.data}
</p>
<p>This only updates when riot.update is called. We call that in <i>index.js</i> when we receive a new msg from SocketIO.</p>
<h2>(1) Next is an HTML-only tag, no JS allowed ...</h2>
<tag1>
<p> We get no local html here so this doesn't show up unless you use <yield/> in your tag definition.</p>
</tag1>
<h2>(2) Next up, a JavaScript only Riot tag (see the console log) ...</h2>
<tag2>
<p>
HTML and content is inside the tag, the tag definition is only Javascript.
The next line is an attribute (tag2Msg) from the JavaScript:
</p>
{ tag2Msg }
</tag2>
<tag3 />
<h2>(4) Next, an external tag file (<i>with-tags.js</i>) ...</h2>
<example message="We are passing a message!"/>
this.on('update', function() {
// allows recalculation of context data before the update
console.log('my-tag was updated')
console.log(msgCounter.data)
//console.log(msg)
})
</my-tag>
<tag1>
<p>This is an HTML-only tag, no JS allowed. So the following will be empty:</p>
<p>A message on mount: { opts.mymsg }</p>
<p>But using <yield/> lets us show information inserted where the tag is mounted (see <i>my-tag</i> in the source):</p>
<div style="margin-left:20%;margin-right:20%;border:1px solid silver;text-align:center;">
<yield/>
</div>
</tag1>
<tag2>
// This is a JavaScript-only tag, no HTML allowed
this.tag2Msg = 'Welcome to tag2'
console.log('tag2, a JavaScript-only tag')
this.on('mount', function(){
console.log('tag2 was mounted!')
})
</tag2>
<tag3>
<h2 onclick={updateMe}>(3) Another tag ...</h2>
<p></p>
<p>Parent opts.mymsg: { parent.opts.mymsg }</p>
<p>Global msgCounter.data: { msgCounter.data }</p>
<p>This mymsg: { mymsg }</p>
var self = this
this.mymsg = 'Poo'
this.on('mount', function(){
//self.update()
console.log('tag3 was mounted')
})
this.on('update', function() {
// allows recalculation of context data before the update
console.log('tag3 was updated')
})
updateMe(e) {
console.log('tag3: updateMe')
self.mymsg = 'You clicked milord?'
console.dir(e)
//console.log(parent.opts.msgCounter)
//self.update()
}
</tag3>
</script>
<!-- <example/> is specified on external file -->
<script data-src="with-tags.tag" type="riot/tag"></script>
<h2>(5) That's it for RiotJS content. See the Javascript file as well.</h2>
<p>
There really isn't much point using anything other than JQuery unless
you need more complex interactions with the page. Simple pages are best done
just with JQuery.
</p>
<h1>Dynamic Data (via JQuery)</h1>
<p>Messages Received: <span id="msgsReceived"></span></p>
<p>Control Messages Received: <span id="msgsControl"></span></p>
<p>Messages Sent: <span id="msgsSent"></span></p>
<p>Last Message Received:</p>
<code id="showMsg"></code>
<p>Last Message Sent:</p>
<code id="showMsgSent"></code>
<!-- Socket.IO is loaded only once for all instances -->
<script src="/uibuilder/socket.io/socket.io.js"></script>
<!-- Note no leading / -->
<script src="vendor/jquery/dist/jquery.min.js"></script>
<!-- include riot.js and the compiler -->
<script type="text/javascript" src="vendor/riot/riot+compiler.min.js"></script>
<script src="index.js"></script>
<!-- mount normally -->
<script>
</script>
</body>
</html>/*global document,$,window,io,riot */
/*
Copyright (c) 2017 Julian Knight (Totally Information)
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
var debug = true,
ioChannels = {control: 'uiBuilderControl', client: 'uiBuilderClient', server: 'uiBuilder'},
msgCounter = {control: 0, sent: 0, data: 0},
msg = {},
cookies = [],
ioNamespace = '/' + readCookie('uibuilder-namespace'),
socket,
retryMs = 2000, // retry ms period for manual socket reconnections workaround
timerid
// When JQuery is ready, update
$( document ).ready(function() {
debug && console.log('Document Ready: IO Namespace: ' + ioNamespace)
// Create the socket - make sure client uses Socket.IO version from the uibuilder module (using path)
socket = io(ioNamespace, {
path: '/uibuilder/socket.io',
transports: ['polling', 'websocket']
})
$('#msgsReceived').text(msgCounter.data)
$('#msgsControl').text(msgCounter.control)
$('#msgsSent').text(msgCounter.sent)
$('#showMsg').text(JSON.stringify(msg))
riot.mount('*', {'msg': msg, 'mymsg': 'Hello there'})
// When the socket is connected .................
socket.on('connect', function() {
debug && console.log('SOCKET CONNECTED - Namespace: ' + ioNamespace)
// Reset any reconnect timers
if (timerid) {
window.clearTimeout(timerid)
retryMs = 2000
timerid = null
}
// When Node-RED uibuilder template node sends a msg over Socket.IO...
socket.on(ioChannels.server, function(recievedMsg) {
debug && console.info('uibuilder:socket.connect:socket.on.data - msg received - Namespace: ' + ioNamespace)
//console.dir(wsMsg)
// Make sure that msg is an object & not null
if ( recievedMsg === null ) {
recievedMsg = {}
} else if ( typeof recievedMsg !== 'object' ) {
recievedMsg = { 'payload': recievedMsg }
}
// Save the msg for further processing
msg = recievedMsg
// Track how many messages have been recieved
msgCounter.data++
$('#msgsReceived').text(msgCounter.data)
$('#showMsg').text(JSON.stringify(msg))
// Note that, unlike more complex libraries, Riot.JS does
// NOT update data dynamically! You have to call update.
riot.update()
// TODO: Add a check for a pre-defined global function here
// to make it easier for users to add their own code
// to process reciept of new msg
// OR MAYBE use msg.prototype to add a function?
// Test auto-response
if (debug) {
sendMsg({payload: 'We got a message from you, thanks'})
}
}) // -- End of websocket recieve DATA msg from Node-RED -- //
// Recieve a CONTROL msg from Node-RED
socket.on(ioChannels.control, function(recievedCtrlMsg) {
debug && console.info('uibuilder:socket.connect:socket.on.control - msg received - Namespace: ' + ioNamespace)
//console.dir(wsMsg)
// Make sure that msg is an object & not null
if ( recievedCtrlMsg === null ) {
recievedCtrlMsg = {}
} else if ( typeof recievedCtrlMsg !== 'object' ) {
recievedCtrlMsg = { 'payload': recievedCtrlMsg }
}
msgCounter.control++
$('#msgsControl').text(msgCounter.control)
$('#showMsg').text(JSON.stringify(recievedCtrlMsg))
switch(recievedCtrlMsg.type) {
case 'shutdown':
// We are shutting down
break
case 'connected':
// We are connected to the server
break
default:
// Anything else
}
// Test auto-response
if (debug) {
sendMsg({payload: 'We got a control message from you, thanks'})
}
}) // -- End of websocket recieve CONTROL msg from Node-RED -- //
}) // --- End of socket connection processing ---
// When the socket is disconnected ..............
socket.on('disconnect', function(reason) {
// reason === 'io server disconnect' - redeploy of Node instance
// reason === 'transport close' - Node-RED terminating
debug && console.log('SOCKET DISCONNECTED - Namespace: ' + ioNamespace + ', Reason: ' + reason)
// A workaround for SIO's failure to reconnect after a NR redeploy of the node instance
if ( reason === 'io server disconnect' ) {
if (timerid) window.clearTimeout(timerid) // we only want one running at a time
timerid = window.setTimeout(function(){
debug && console.log('Manual SIO reconnect attempt, timeout: ' + retryMs)
socket.connect() // Try to reconnect
retryMs = retryMs + 1000 // extend timer for next time round
}, retryMs)
}
}) // --- End of socket disconnect processing ---
/* We really don't need these, just for interest
socket.on('connect_error', function(err) {
debug && console.log('SOCKET CONNECT ERROR - Namespace: ' + ioNamespace + ', Reason: ' + err.message)
//console.dir(err)
}) // --- End of socket connect error processing ---
socket.on('connect_timeout', function(data) {
debug && console.log('SOCKET CONNECT TIMEOUT - Namespace: ' + ioNamespace)
console.dir(data)
}) // --- End of socket connect timeout processing ---
socket.on('reconnect', function(attemptNum) {
debug && console.log('SOCKET RECONNECTED - Namespace: ' + ioNamespace + ', Attempt #: ' + attemptNum)
}) // --- End of socket reconnect processing ---
socket.on('reconnect_attempt', function(attemptNum) {
debug && console.log('SOCKET RECONNECT ATTEMPT - Namespace: ' + ioNamespace + ', Attempt #: ' + attemptNum)
}) // --- End of socket reconnect_attempt processing ---
socket.on('reconnecting', function(attemptNum) {
debug && console.log('SOCKET RECONNECTING - Namespace: ' + ioNamespace + ', Attempt #: ' + attemptNum)
}) // --- End of socket reconnecting processing ---
socket.on('reconnect_error', function(err) {
debug && console.log('SOCKET RECONNECT ERROR - Namespace: ' + ioNamespace + ', Reason: ' + err.message)
//console.dir(err)
}) // --- End of socket reconnect_error processing ---
socket.on('reconnect_failed', function(data) {
debug && console.log('SOCKET RECONNECT FAILED - Namespace: ' + ioNamespace)
console.dir(data)
}) // --- End of socket reconnect_failed processing ---
socket.on('ping', function() {
debug && console.log('SOCKET PING - Namespace: ' + ioNamespace)
}) // --- End of socket ping processing ---
socket.on('pong', function(data) {
debug && console.log('SOCKET PONG - Namespace: ' + ioNamespace + ', Data: ' + data)
}) // --- End of socket pong processing ---
*/
});
// ----- UTILITY FUNCTIONS ----- //
// send a msg back to Node-RED, NR will generally expect the msg to contain a payload topic
var sendMsg = function(msgToSend) {
// Track how many messages have been sent
msgCounter.sent++
$('#msgsSent').text(msgCounter.sent)
$('#showMsgSent').text(JSON.stringify(msgToSend))
socket.emit(ioChannels.client, msgToSend)
} // --- End of Send Msg Fn --- //
function readCookie(name,c,C,i){
// @see http://stackoverflow.com/questions/5639346/what-is-the-shortest-function-for-reading-a-cookie-by-name-in-javascript
if(cookies.length > 0){ return cookies[name]; }
c = document.cookie.split('; ')
// @ts-ignore
cookies = {}
for(i=c.length-1; i>=0; i--){
C = c[i].split('=')
cookies[C[0]] = C[1]
}
return cookies[name]
} // --- End of readCookie fn --- //
// ----------------------------- //
// EOF<example>
<h1 show={this.show_message} onclick={uppercase}>{ message }</h1>
<input onkeyup={show_text} ref="input_text"/>
<button onclick={toggle_message}> toggle message </button>
<p>Parent mymsg: { this.parent.opts.mymsg }</p>
<p>Parent Keys:</p>
<ul>
<li each={ item, i in aparent }>
({ i }) { item }
</li>
</ul>
<p>Parent Opts Keys:</p>
<ul>
<li each={ item, i in Object.keys(this.parent.opts) }>
({ i }) { item }
</li>
</ul>
<p>Parent Opts Msg Keys:</p>
<ul>
<li each={ item, i in Object.keys(this.parent.opts.msg) }>
({ i }) { item }
</li>
</ul>
<p>Parent Msg Type: { typeof this.parent.opts.msg }</p>
<div if={Object.keys(msg).length}>
<p>Global Msg:</p>
<ul if={Object.keys(msg.payload).length}>
<li each={ item, i in msg.payload }>
{ i }: { item }
</li>
</ul>
</div>
<script>
this.message = opts.message || 'Hello Riot'
this.show_message = true
this.mymsg = opts.mymsg || this.parent.opts.mymsg
this.pmsg = this.parent.opts.msg
this.aparent = Object.keys(this.parent)
uppercase() {
this.message = this.message.toUpperCase()
}
this.show_text = function(e) {
this.message = this.refs.input_text.value
}
this.toggle_message = function(e) {
this.show_message = !this.show_message
}
console.info('Riot: Loaded this from external file ./with-tags.tag')
</script>
</example>Well I got the riot example going with no problems. I had to run npm
install to install riot which I managed to work out.
It loads super fast on my phone which is good.
A question though, having experimented a bit, if I send it a message
with sensor value (and topic), from MQTT for example, I can display it
ok, based on the topic. However, if I send the node a message and then
open the page in the browser then it does not show the value till I
send the value again, which might not be for some time. Am I missing
something?
By the way there is a certain inconsistency in the spelling of receive
in the node and docs. Some of them are recieved :)
d
sendMsg({command: 'reload', payload: 'Browser connected'})
Then use this flow to save topic/payload pairs passed to the uibuilder node and automatically reload them when a new browser connection is made
[
{
"id": "4752fa78.9d12dc",
"type": "comment",
"z": "a64c0170.a07c38",
"name": "Msg for uibuilder go in here ->",
"info": "",
"x": 130,
"y": 120,
"wires": []
},
{
"id": "3bc80526.35ff62",
"type": "function",
"z": "a64c0170.a07c38",
"name": "Topic repeater",
"func": "// Given messages containing topic/payload values this saves the latest payload for each topic\n// and passes the message on.\n// If it receives a message with msg.command set to 'reload' it retransmits each saved pair as\n// individual messages.\n// If it receives a message with msg.command set to 'reset' it removes all saved data\nif (msg.command && msg.command === 'reload') {\n // a reload message so re-send all messages saved\n var keys = context.keys();\n for (var i = 0; i < keys.length; i++) {\n node.send({topic: keys[i], payload: context.get(keys[i])});\n } \n msg = null;\n} else if (msg.command && msg.command === 'reset') {\n // a reset command so remove all saved messages\n var keys = context.keys();\n for (var i = 0; i < keys.length; i++) {\n context.set(keys[i], undefined);\n }\n msg= null;\n} else {\n // a normal message so add/update the payload for this topic, provided there is a topic\n if (msg.topic) {\n context.set(msg.topic, msg.payload);\n }\n}\n// pass on the message unless it has been nulled\nreturn msg;\n",
"outputs": 1,
"noerr": 0,
"x": 380,
"y": 120,
"wires": [
[
"fb556f6b.d97658",
"fccca17e.d4a148"
]
]
},
{
"id": "fb556f6b.d97658",
"type": "uibuilder",
"z": "a64c0170.a07c38",
"name": "",
"url": "uibuilder",
"fwdInMessages": false,
"customFoldersReqd": true,
"x": 380,
"y": 60,
"wires": [
[
"902f597f.daef6"
]
]
},
{
"id": "902f597f.daef6",
"type": "switch",
"z": "a64c0170.a07c38",
"name": "",
"property": "command",
"propertyType": "msg",
"rules": [
{
"t": "null"
},
{
"t": "else"
}
],
"checkall": "true",
"outputs": 2,
"x": 510,
"y": 60,
"wires": [
[],
[
"3bc80526.35ff62"
]
]
},
{
"id": "427cb125.4eeb48",
"type": "comment",
"z": "a64c0170.a07c38",
"name": "Normal msgs on o/p 1",
"info": "",
"x": 680,
"y": 40,
"wires": []
}
]
I don't think this should add any significant overheads and seems to work well.
It won't work for things like charts, but they have to be dealt with separately anyway I think.