Uibuilder and vue router

Hi. Uibuilder has been great so far and I am really get into the swing of it! Thank you very much for that! :+1:

I have run into a problem that I am unable to use Vue Router properly. Perhaps it has something to do with the uibuilder.

Copying the HTML and JavaScript code from https://router.vuejs.org/guide/#html (Open that in Edge) into the index.html and index.js: Ends up showing index.html without any links:

NoLinks

Looking at JSFiddle it should work:

https://jsfiddle.net/yyx990803/xgrjzsup/

Any ideas?

Thanks!

I don't know what the issue would be, but I don't use Edge myself. I've successfully used Vue Router without a build step in Uibuilder before.

Let me see if I can link the files here for my demo that uses both Vue router and VueX.
Single node uibuilder on /demo:

[{"id":"dbb0bd8f.7242a","type":"uibuilder","z":"fe5232bf.c0c7f","name":"","topic":"","url":"demo","fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"showfolder":false,"x":170,"y":680,"wires":[[],[]]}]

With index.html:

<!doctype html>
<!-- Note that adding an appcache really speeds things up after the first load
    You need to amend the appcache file to meet your needs.
    Don't forget to change the appcache file if you update ANY
    of the files in it otherwise the old versions will ALWAYS be used.
<html lang="en" manifest="./uibuilder.appcache">
-->
<html lang="en">
<!--
    This is the default, template html for uibuilder.
    It is meant to demonstrate the use of VueJS & bootstrap-vue to dynamically
    update the ui based on incoming/outgoing messages from/to the
    Node-RED server.

    You will want to alter this to suite your own needs. To do so,
    copy this file to <userDir>/uibuilder/<url>/src.
-->
<head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, minimum-scale=1, initial-scale=1, user-scalable=yes">

    <title>Node-RED UI Builder</title>
    <meta name="description" content="Node-RED UI Builder - VueJS + bootstrap-vue version">

    <link rel="icon" href="./images/node-blue.ico">

    <!-- See https://goo.gl/OOhYW5 -->
    <link rel="manifest" href="./manifest.json">
    <meta name="theme-color" content="#3f51b5">

    <!-- Used if adding 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">

    <!-- Used if adding 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 for Apple mobile use if required
        <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 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" />
    
    <link rel="stylesheet" href="./index.css" media="all">
</head>
<body>
    <div id="app">
        <!-- Self closing tags in general WON'T work with http-vue-loader!!! -->
        <router-view name="menu"></router-view>
        <b-container fluid>
            <router-view></router-view>
        </b-container>
    </div>

    <!-- 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>

    <!-- --- Vendor Libraries - Load in the right order --- -->
    <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>

    <!-- HTTP Vue Loader -->
    <script src="../uibuilder/vendor/http-vue-loader/src/httpVueLoader.js"></script>

    <!-- VueX -->
    <script src="../uibuilder/vendor/vuex/dist/vuex.js"></script>
    <!-- Vue Router -->
    <script src="../uibuilder/vendor/vue-router/dist/vue-router.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" type="module"></script>

</body>

</html>

With index.js:

// @ts-nocheck
/*
  Copyright (c) 2019 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'

import store from './store.js';
import router from './router.js';

/** @see https://github.com/TotallyInformation/node-red-contrib-uibuilder/wiki/Front-End-Library---available-properties-and-methods */

// eslint-disable-next-line no-unused-vars
const app1 = new Vue({
    el: '#app',
    data: {
        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,
    }, // --- End of data --- //
    computed: {
        hLastRcvd: function() {
            var msgRecvd = this.msgRecvd
            if (typeof msgRecvd === 'string') return 'Last Message Received = ' + msgRecvd
            else return 'Last Message Received = ' + this.syntaxHighlight(msgRecvd)
        },
        hLastSent: function() {
            var msgSent = this.msgSent
            if (typeof msgSent === 'string') return 'Last Message Sent = ' + msgSent
            else return 'Last Message Sent = ' + this.syntaxHighlight(msgSent)
        },
        hLastCtrlRcvd: function() {
            var msgCtrl = this.msgCtrl
            if (typeof msgCtrl === 'string') return 'Last Control Message Received = ' + msgCtrl
            else return 'Last Control Message Received = ' + this.syntaxHighlight(msgCtrl)
        },
        hLastCtrlSent: function() {
            var msgCtrlSent = this.msgCtrlSent
            if (typeof msgCtrlSent === 'string') return 'Last Control Message Sent = ' + msgCtrlSent
            //else return 'Last Message Sent = ' + this.callMethod('syntaxHighlight', [msgCtrlSent])
            else return 'Last Control Message Sent = ' + this.syntaxHighlight(msgCtrlSent)
        },
    }, // --- End of computed --- //
    methods: {
        increment: function() {
            // Increment the count by one
            this.counterBtn = 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 --- //

        // 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: init,mounted,updated,destroyed
    mounted: function(){
        //console.debug('[indexjs:Vue.mounted] app mounted - setting up uibuilder watchers')

        /** **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('/nr/uib', '/nr/uibuilder/vendor/socket.io') // change to use your paths/names
         */
        uibuilder.start()

        var vueApp = this

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

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

        //#region ---- Trace Received Messages ---- //
        // If msg changes - msg is updated when a standard msg is received from Node-RED over Socket.IO
        // newVal relates to the attribute being listened to.
        uibuilder.onChange('msg', function(newVal){
            //console.info('[indexjs:uibuilder.onChange] msg received from Node-RED server:', newVal)
            vueApp.msgRecvd = newVal
        })
        // As we receive new messages, we get an updated count as well
        uibuilder.onChange('msgsReceived', function(newVal){
            //console.info('[indexjs:uibuilder.onChange] Updated count of received msgs:', newVal)
            vueApp.msgsReceived = newVal
        })

        // 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(newVal){
            //console.info('[indexjs:uibuilder.onChange:ctrlMsg] CONTROL msg received from Node-RED server:', newVal)
            vueApp.msgCtrl = newVal
        })
        // Updated count of control messages received
        uibuilder.onChange('msgsCtrl', function(newVal){
            //console.info('[indexjs:uibuilder.onChange:msgsCtrl] Updated count of received CONTROL msgs:', newVal)
            vueApp.msgsControl = newVal
        })
        //#endregion ---- End of Trace Received Messages ---- //

        //#region ---- Trace Sent Messages ---- //
        // 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(newVal){
            //console.info('[indexjs:uibuilder.onChange:sentMsg] msg sent to Node-RED server:', newVal)
            vueApp.msgSent = newVal
        })
        // Updated count of sent messages
        uibuilder.onChange('msgsSent', function(newVal){
            //console.info('[indexjs:uibuilder.onChange:msgsSent] Updated count of msgs sent:', newVal)
            vueApp.msgsSent = newVal
        })

        // If we send a control message to Node-RED, we can get a copy of it here
        uibuilder.onChange('sentCtrlMsg', function(newVal){
            //console.info('[indexjs:uibuilder.onChange:sentCtrlMsg] Control message sent to Node-RED server:', newVal)
            vueApp.msgCtrlSent = newVal
        })
        // And we can get an updated count
        uibuilder.onChange('msgsSentCtrl', function(newVal){
            //console.info('[indexjs:uibuilder.onChange:msgsSentCtrl] Updated count of CONTROL msgs sent:', newVal)
            vueApp.msgsCtrlSent = newVal
        })
        //#endregion ---- End of Trace Sent Messages ---- //

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

    }, // --- End of mounted hook --- //
    store: new Vuex.Store(store),
    router: new VueRouter(router),

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

// EOF

With router.js:

const HeaderMenu = httpVueLoader('./components/HeaderMenu.vue');
const IndexView = httpVueLoader('./views/IndexView.vue');
const AdminView = httpVueLoader('./views/AdminView.vue');

export default {
    routes: [
        {
            path: '/',
            name: 'index',
            components: {
                default: IndexView,
                menu: HeaderMenu,
            },
        },
        {
            path: '/rooms',
            name: 'rooms_overview',
            components: {
                default: httpVueLoader('./components/RoomsOverview.vue'),
                menu: HeaderMenu,
            },
            children: [
                {
                    path: 'rooms/:roomId',
                    name: 'room',
                    component: () => Promise.resolve(httpVueLoader('./components/Room.vue')),
                },
            ],
        },
        {
            path: '/admin',
            name: 'admin', 
            components: {
                default: AdminView,
                menu: HeaderMenu,
            },
            children: [
                {
                    path: 'info',
                    name: 'usage_info',
                    component: httpVueLoader('./components/UsageInformation.vue'),
                }
            ]
        },
    ],
};

With store.js:

import rooms from './store-modules/rooms.js';
import devices from './store-modules/devices.js';

export default {
    state: {
        unconfiguredRooms: [],
        unconfiguredDevices: [],
    },
    getters: {
        hasUnconfiguredItems: state => state.unconfiguredRooms.length !== 0 || state.unconfiguredDevices.length !== 0,
    },
    mutations: {

    },
    actions: {

    },
    modules: {
        rooms,
        devices,
    },
};

And a few more files not at top level but I'm not connected to the machine atm so I can't easily link those, but that should give you an idea. If I remember I'll put this in a standalone demo flow for the flows library.

3 Likes

How do I passed msg.payload from node-red/uibuilder from index.js to IndexView.vue ??

I have to moved all the components from index.html to template in IndexView.vue but all my binding are gone ..

old index.html
<my-summary v-bind:myenv="env"></my-env>

new IndexView.vue, this obviously did not work bcos env is in index.js rather than in data() of IndexView
<my-summary v-bind:myenv="env"></my-env>

How are you passing that env to the IndexView. You’re passing it as a prop to MySummary, which assumes it to be present in your view one way or another. So as you say it’s only present in your index.js file, you need a way to pass it to your IndexView component.

That was my question actually , how do I passed props to IndexView ?

Can I pass it directly from index.js to IndexView ??

Or should I passed to and passed to IndexView ( this sounds kinda silly )

Put it in perspective: you said it was in your data() of the vue app in index.js, but you were passing them as props to my-summary. How did my-summary get it? Through the props.
Since this is a vue-router topic I’m assuming IndexView is reached through the router rather than being directly nested in your outer app?
This would be the regular way to do so: https://router.vuejs.org/guide/essentials/passing-props.html#boolean-mode
I am however also assuming by its name that your env is not something easily passed in the url. Can you explain what the variable is and what you use it for? It might be better handling this differently, and there’s more than one way leading to Rome.

msg.payload from Node-Red get send to uibuider...
msg.payload = { name:env, temp:nnn, rh:nnnn }

Just to display the numbers, no modifications needed...

The env object is passed to my-summary as props, yes. I would like to do the same when passed to IndexView.

So in summary, how can I passed env object from index.js to router.js to IndexView then to my-summary components ?

I will try the method u mentioned but I have a question ...

How do I passed the data() { env: {} } in index.js to router.js ???

This would be the regular way to do so: https://router.vuejs.org/guide/essentials/passing-props.html#boolean-mode

Below are the snippits of my codes...

index.js

uibuilder.onChange('msg', function(msg){
            vm.msgRecvd = msg
			if ( msg.payload.hasOwnProperty('name')) {
				vm.env = msg.payload;
			}

index.html
<my-summary v-bind:myenv="env"></my-summary>

router.js

export default {
    routes: [
        {
            path: '/',
            name: 'index',
            components: {
                default: IndexView,
                menu: HeaderMenu,
            },
        },

I solved it after a few trial and errors...

I passed env as indexenv in index.html

<router-view :indexenv="env" ></router-view>

IndexView, recv as props indexenv passed as myenv

<my-summary :myenv=indexenv ></my-summary>

my-summary recv as props as myenv and display the data