UIBuilder 6.0 step-by-step

Ok... this may sound odd, but I am having a heck of a time getting the UIBuilder to work. I installed the new UIBuilder 6.0 on my Node Red. (This is for an escape room that I am building, following a lot of PlayTech/Alastair's guides). However for older versions I could walk through his guide, but since the introduction of v6, I am stuck and things do not wish to work correctly.

At present I am stuck getting my msg.payload (or any msg) to display correctly on the screen. It is just showing as {{msg.formattedTimeRemaining}}

Has anyone written down or found a video that shows a step by step from install to first run of the new UIBuilder 6.0?

2 Likes

TotallyInformation is the developer and will probably be able to help you. He's very active and responsive in the forum. I just wanted to let you know you aren't alone in trying to figure that out.

I spent at least a couple weeks trying to figure out UIBuilder about two years ago and got nowhere with it and went back to the regular dashboard. I would have liked to have gotten UIBuilder working in my applications because it's capable of so much more and the interfaces you can build with it look awesome, but there simply wasn't enough documentation with good examples of what I needed to do at the time. I might have to revisit the docs at some point and see if they've improved.

2 Likes

I'm like a genie - you call and I'm here! :wink:

That means that Vue is not actually working - most likely because it hasn't loaded.

It is meant to be trivial :grin:

This is the official guide: Let's get started! (totallyinformation.github.io)

While that was written prior to v6, it is generic enough to work fine with v6.

However, there is one thing I'm going to recommend to you. Once you've created your test uibuilder node and deployed it the first time, re-open the settings and change the template from the default to the one that says "No framework, modern IIFE client". Click load and accept the warning. This gives you a much better starter code to work with. Details of changing templates is about 2/3 of the way through that walkthrough.

That is because I ran out of time to get all of the templates updated before I published v6. v6.1 is in the works with a much nicer set of updated templates.

Now, of course, the example you gave uses VueJS which is quite a popular framework. Quite probably though, your immediate issue with that code is almost certainly due to the fact that the Vue project decided to suddenly change their default deployment from v2 to v3 which is quite a different set of code. So to use most of the Vue-based examples for uibuilder, you will need to install Vue v2 (or consume it from an Internet CDN). To install v2 rather than v3, open uibuilder's settings, navigate to the library tab, add a new library and install "vue@2". That will overwrite v3 with v2 and quite possibly, the example will just start working.

I regularly get really fed up with the enforced changes from frameworks which is why I'm currently improving uibuilder's ability to create native HTML interfaces without the need for (but still generally compatible with) a framework.

I've just never been able to find time to sit down and record/edit one I'm afraid.

1 Like

Sorry to here that. The documentation continues to improve over time. Not sure if the Tech Docs existed last time you looked. Home (totallyinformation.github.io). They are probably still a bit too much of a brain dump but I have tried to focus on some more basic docs.

Also, don't forget that there are a number of examples that come with uibuilder. These are ready-made basic flows that give you the basics already set up.

It's been a while since I last looked at the docs, but they did exist at the time. I'll have to revisit them. I've also deployed the examples and played with them but couldn't figure out how to adapt them into my own apps.

Don't get me wrong. I love what you've done with UIBuilder. The problem is that I've never been able to figure out how to actually use it and I really want to. I've seen some amazing things people have built with it and would love to make my interfaces look even better.

One of the things I was struggling with, among several, was populating a table with results from MySQL queries. Perhaps I'll give it another try in the near future and engage you in the forum again. I don't want to hijack the OP's thread. :slight_smile:

Ah, well, did you see my recent posts on uibuilder? Including a post where I showed uibuilder creating a list from an array and then creating a table in one of the list entries - again from an array of objects? Not the most efficient approach ever but insanely flexible. And no frameworks needed :grin:

I've previously shared examples of using bootstrap-vue's custom components to output/update tables as well - I think I may even have shared an example from one of my home dashboards (no longer in use).

By all means start a new thread and I'll help you through some ideas.

1 Like

I definitely missed that post but will look for it. Thank you! :slight_smile:

@TotallyInformation: You are DA MAN!! Thank you, I have now got it working. Very much appreciated.

@TotallyInformation:
Thank you for this information, however I am right now getting a ton of Converting Circular structure to JSON coming from the uibuilder. (Sometimes multiple times a second).

Full Error message:

TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'IncomingMessage'
    |     property 'res' -> object with constructor 'ServerResponse'
    --- property 'req' closes the circle

Here is the code of the uibuilder:
index.html:

<!doctype html>
<html lang="en"><head>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>Epicenter Hotel/Casino Elevator Information System</title>

    <link type="text/css" rel="stylesheet" href="../uibuilder/vendor/bootstrap/dist/css/bootstrap.min.css" />
    <link type="text/css" rel="stylesheet" href="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.css" />
    <!-- Your own CSS -->
    <link type="text/css" rel="stylesheet" href="./index.css" media="all">

</head><body>
<div id="app" class="uib" v-cloak>
    <!-- All UI code needs to be in here -->
    <p class="timerText">{{msg.formattedTimeRemaining}}</p>
    <p class="clueText">{{msg.clueText}}</p>
    <p class="exitCode">{{msg.exitCode}}</p>
    <p class="messageTest">R: <b>{{msgsReceived}}</b>, S: <b>{{msgsSent}}</b></p>


    <b-container id="app_container">
        <!-- Wraps the bootstrap-vue formatting -->

    </b-container>
</div>

    <!-- #region Supporting Scripts. These MUST be in the right order. Note no leading / -->
    <script src="../uibuilder/vendor/socket.io/socket.io.js">/* REQUIRED: Socket.IO is loaded only once for all instances. Without this, you don't get a websocket connection */</script>
    <!-- Vendor Libraries - Load in the right order, use minified, production versions for speed -->
    <script src="../uibuilder/vendor/vue/dist/vue.min.js">/* prod version with component compiler */</script>
    <!-- <script src="../uibuilder/vendor/vue/dist/vue.js">/* dev version with component compiler */</script> -->
    <!-- <script src="../uibuilder/vendor/vue/dist/vue.runtime.min.js">/* prod version without component compiler */</script> -->
    <script src="../uibuilder/vendor/bootstrap-vue/dist/bootstrap-vue.min.js">/* remove 'min.' to use dev version */</script> <!--  -->

    <script src="./uibuilderfe.min.js">/* REQUIRED: remove 'min.' to use dev version */</script>
    <script src="./index.js">/* OPTIONAL: Put your custom code here */</script>
    <!-- #endregion -->

</body></html>

index.css:

/* Load the defaults from `<userDir>/node_modules/node-red-contrib-uibuilder/front-end/src/uib-styles.css` */
@import url("./uib-styles.css");

/* Cloak elements on initial load to hide the possible display of {{ ... }} 
 * Add to the app tag or to specific tags
 * To display "loading...", change to the following:
 *    [v-cloak] > * { display:none }
 *    [v-cloak]::before { content: "loadingā€¦" }
 */
[v-cloak] { display: none; }


body {
    padding: 0;
    margin: 0;
    //overflow: hidden; /* Don't display any scroll bars */
    background: url("images/EpicenterHotelCasino.png");
    background-size: cover;
    background-repeat: no-repeat;
}
#app {

    height: 1080px;
    width: 100%;
    text-align: center;
    margin: auto;
    font-family: 'Staatliches', cursive;
    color: #AAA;
}

.timerText { 
    position: fixed;
    top:375px;
    left:50%;
    margin-right: -50%;
    transform: translate(-50%, -50%);
    font-size: 200px;
    z-index: 12;
    color: #7E7;
    
}
.clueText {
    position: fixed;
    text-align: center;
    top:650px;
    left:50%;
    margin-right: -50%;
    transform: translate(-50%, -50%);
    font-size: 120px;
    z-index: 15;
    color: #C2F;
}
.exitCode {
    position: fixed;
    text-align: center;
    top: 875px;
    left:50%;
    margin-right: -50%;
    transform: translate(-50%, -50%);
    font-size: 120px;
    z-index: 14;
    color: rgb(95, 34, 238);
}
.messageTest {
    position: fixed;
    text-align: left;
    top:1040px;
    left:5px;
    font-size: 25px;
    z-index: 13;
    color:#FFF;
}

index.js

/* jshint browser: true, esversion: 5, asi: true */
/*globals Vue, uibuilder */
// @ts-nocheck
/*
  Copyright (c) 2021-2022 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.
*/
'use strict'

/** @see https://totallyinformation.github.io/node-red-contrib-uibuilder/#/front-end-library */

// eslint-disable-next-line no-unused-vars
const app = new Vue({
    el: '#app',

    data() { return {

        startMsg    : 'Vue has started, waiting for messages',
        feVersion   : '',
        counterBtn  : 0,
        inputText   : null,
        inputChkBox : false,
        socketConnectedState : false,
        serverTimeOffset     : '[unknown]',
        imgProps             : { width: 75, height: 75 },

        msgRecvd    : '[Nothing]',
        msgsReceived: 0,
        msgCtrl     : '[Nothing]',
        msgsControl : 0,

        msgSent     : '[Nothing]',
        msgsSent    : 0,
        msgCtrlSent : '[Nothing]',
        msgsCtrlSent: 0,

        isLoggedOn  : false,
        userId      : null,
        userPw      : null,
        inputId     : '',

    }}, // --- End of data --- //

    computed: {

        hLastRcvd: function() {
            var msgRecvd = this.msgRecvd
            if (typeof msgRecvd === 'string') return 'Last Message Received = ' + msgRecvd
            return 'Last Message Received = ' + this.syntaxHighlight(msgRecvd)
        },
        hLastSent: function() {
            var msgSent = this.msgSent
            if (typeof msgSent === 'string') return 'Last Message Sent = ' + msgSent
            return 'Last Message Sent = ' + this.syntaxHighlight(msgSent)
        },
        hLastCtrlRcvd: function() {
            var msgCtrl = this.msgCtrl
            if (typeof msgCtrl === 'string') return 'Last Control Message Received = ' + msgCtrl
            return 'Last Control Message Received = ' + this.syntaxHighlight(msgCtrl)
        },
        hLastCtrlSent: function() {
            var msgCtrlSent = this.msgCtrlSent
            if (typeof msgCtrlSent === 'string') return 'Last Control Message Sent = ' + msgCtrlSent
            return 'Last Control Message Sent = ' + this.syntaxHighlight(msgCtrlSent)
        },

    }, // --- End of computed --- //

    methods: {

        // Called from the increment button - sends a msg to Node-RED
        increment: function(event) {
            console.log('Button Pressed. Event Data: ', event)

            // Increment the count by one
            this.counterBtn += 1
            var topic = this.msgRecvd.topic || 'uibuilder/vue'
            uibuilder.send( {
                'topic': topic,
                'payload': {
                    'type': 'counterBtn',
                    'btnCount': this.counterBtn,
                    'message': this.inputText,
                    'inputChkBox': this.inputChkBox
                }
            } )

        }, // --- End of increment --- //

        // REALLY Simple method to return DOM events back to Node-RED. See the 2nd b-button on the default html
        doEvent: uibuilder.eventSend,

        // return formatted HTML version of JSON object
        syntaxHighlight: function(json) {
            json = JSON.stringify(json, undefined, 4)
            json = json.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;')
            json = json.replace(/("(\\u[a-zA-Z0-9]{4}|\\[^u]|[^\\"])*"(\s*:)?|\b(true|false|null)\b|-?\d+(?:\.\d*)?(?:[eE][+\-]?\d+)?)/g, function (match) {
                var cls = 'number'
                if ((/^"/).test(match)) {
                    if ((/:$/).test(match)) {
                        cls = 'key'
                    } else {
                        cls = 'string'
                    }
                } else if ((/true|false/).test(match)) {
                    cls = 'boolean'
                } else if ((/null/).test(match)) {
                    cls = 'null'
                }
                return '<span class="' + cls + '">' + match + '</span>'
            })
            return json
        }, // --- End of syntaxHighlight --- //

    }, // --- End of methods --- //

    // Available hooks: beforeCreate,created,beforeMount,mounted,beforeUpdate,updated,beforeDestroy,destroyed, activated,deactivated, errorCaptured

    /** Called after the Vue app has been created. A good place to put startup code */
    created: function() {

        // Example of retrieving data from uibuilder
        this.feVersion = uibuilder.get('version')

        /** **REQUIRED** Start uibuilder comms with Node-RED @since v2.0.0-dev3
         * Pass the namespace and ioPath variables if hosting page is not in the instance root folder
         * e.g. If you get continual `uibuilderfe:ioSetup: SOCKET CONNECT ERROR` error messages.
         * e.g. uibuilder.start('/uib', '/uibuilder/vendor/socket.io') // change to use your paths/names
         * @param {Object=|string=} namespace Optional. Object containing ref to vueApp, Object containing settings, or String IO Namespace override. changes self.ioNamespace from the default.
         * @param {string=} ioPath Optional. changes self.ioPath from the default
         * @param {Object=} vueApp Optional. Reference to the VueJS instance. Used for Vue extensions.
         */
        uibuilder.start(this) // Single param passing vue app to allow Vue extensions to be used.

        //console.log(this)

    }, // --- End of created hook --- //

    /** Called once all Vue component instances have been loaded and the virtual DOM built */
    mounted: function(){

        //console.debug('[indexjs:Vue.mounted] app mounted - setting up uibuilder watchers')

        var app = this  // Reference to `this` in case we need it for more complex functions

        // If msg changes - msg is updated when a standard msg is received from Node-RED over Socket.IO
        uibuilder.onChange('msg', function(msg){
            //console.info('[indexjs:uibuilder.onChange] msg received from Node-RED server:', msg)
            app.msgRecvd = msg
            app.msgsReceived = uibuilder.get('msgsReceived')
        })

        //#region ---- Debug info, can be removed for live use ---- //

        /** You can use the following to help trace how messages flow back and forth.
         * You can then amend this processing to suite your requirements.
         */

        // If we receive a control message from Node-RED, we can get the new data here - we pass it to a Vue variable
        uibuilder.onChange('ctrlMsg', function(msg){
            //console.info('[indexjs:uibuilder.onChange:ctrlMsg] CONTROL msg received from Node-RED server:', msg)
            app.msgCtrl = msg
            app.msgsControl = uibuilder.get('msgsCtrl')
        })

        /** You probably only need these to help you understand the order of processing
         * If a message is sent back to Node-RED, we can grab a copy here if we want to
         */
        uibuilder.onChange('sentMsg', function(msg){
            //console.info('[indexjs:uibuilder.onChange:sentMsg] msg sent to Node-RED server:', msg)
            app.msgSent = msg
            app.msgsSent = uibuilder.get('msgsSent')
        })

        /** If we send a control message to Node-RED, we can get a copy of it here */
        uibuilder.onChange('sentCtrlMsg', function(msg){
            //console.info('[indexjs:uibuilder.onChange:sentCtrlMsg] Control message sent to Node-RED server:', msg)
            app.msgCtrlSent = msg
            app.msgsCtrlSent = uibuilder.get('msgsSentCtrl')
        })

        /** If Socket.IO connects/disconnects, we get true/false here */
        uibuilder.onChange('ioConnected', function(connected){
            //console.info('[indexjs:uibuilder.onChange:ioConnected] Socket.IO Connection Status Changed to:', connected)
            app.socketConnectedState = connected
        })
        /** If Server Time Offset changes */
        uibuilder.onChange('serverTimeOffset', function(serverTimeOffset){
            //console.info('[indexjs:uibuilder.onChange:serverTimeOffset] Offset of time between the browser and the server has changed to:', serverTimeOffset)
            app.serverTimeOffset = serverTimeOffset
        })

        /** If user is logged on/off */
        uibuilder.onChange('isAuthorised', function(isAuthorised){
            //console.info('[indexjs:uibuilder.onChange:isAuthorised] isAuthorised changed. User logged on?:', isAuthorised)
            //console.log('authData: ', uibuilder.get('authData'))
            //console.log('authTokenExpiry: ', uibuilder.get('authTokenExpiry'))
            app.isLoggedOn = isAuthorised
        })

        //#endregion ---- Debug info, can be removed for live use ---- //

    }, // --- End of mounted hook --- //

}) // --- End of app1 --- //

// EOF

Do not include msg.req or msg.res in any messages that need to be serialised - as in sending them to the front-end or MQTT, etc. That is because they cannot be serialised. Delete those properties before trying to send them.

So if I understand correctly. before the join into UIBuilder. Add change node and set rules as follows:
Delete msg.res
Delete msg.req

Is that correct, and what you are referring to?

Yup, this isn't a uibuilder issue. The res and req properties from ExpressJS can't be serialised because they contain circular references.

@TotallyInformation:

So I added that change node before the UIBuilder. (It goes JOIN, CHANGE, UIBUILDER), and this last run through at first things were going well.... but then it triggered again.
I ran the setup through with a debug node added right before the JOIN, and noted the msg commands being sent. Then I added a Change node before the JOIN and delete msg.res, msg.req, msg._linkSource, msg._event, msg._msgid..... and it fired correctly. and no circular structure error.

You should generally leave msg._msgid alone - though it has no meaning for uibuilder itself, only for Node-RED.

I think that msg._linkSource is added by a link-in node. Again, that should be fine as I think it is just a node id. It allows a link-call node to work.

Not sure what in your flow is adding the msg._event but I expect that is the one that can't be serialised either.

Yes there is a link-in node for this setup, but it does not have a corresponding return node. (Make heavy use of these in my setup), this particular one is a one-way call only though.

Thank you!!

1 Like

There is now the first "official" video!

This topic was automatically closed 14 days after the last reply. New replies are no longer allowed.