UIbuilder VueX and VueRouter

To (amateurs) whom it may concern, some concepts of Vue(X) and VueRouter which kept me busy using the UIBuilder node , and just to add my fun experience :

Passing parameters with Vue between components and/or routes.

  1. In Vue-Router, if you wanted to pass an object as a param , you might have noticed you get errors when trying to display them.

Imagine the following template :

<template>
<div>
	<h1> List </h1>
		<div v-for="timer,i in example" :key="i"> 
  			<router-link :to="{ name:'TimerDetails', params: {id:i, org:timerobject }}">
			<h2 >{{ timer.name }}</h2> 
  			</router-link>
		</div>
</div>
</template>

params: {id:i, org:timerobject }}

Vue-router does not support passing object parameters, so the workaround is to use JSON.stringify :

params: {id:i, org:JSON.stringify(timerobject) }}

In this case the "timerobject" will be passed as a string. In the appropriate route .vue file, in this example "TimerDetails.vue", you can parse back the json string.

export default {
  data() {
    return {
      id: this.$route.params.id,
      org: JSON.parse(this.$route.params.org)
   }
  }
}
  1. Looking at :

a better solution to passing params depending on size/complexity/criticality etc. , VueX comes into view .

This also has the advantage of the Vue-client app being able to dynamically receive updates from the server without having to resort to using api's .

.vue file :


<script>
 export default {
  
   computed: {
    example () {
      return this.$store.state.timers
    }
  }
}
</script>

index.js file :

import Vue from 'vue'
import App from './App.vue'
import VueRouter from 'vue-router'
import router from './router'
import Vuex from 'vuex'

Vue.use(Vuex);
Vue.use(VueRouter)

const store = new Vuex.Store({
  state: {
	  timers:[{name:'s0'}],
  },
  mutations: {
   populate (state,elem) {
	   state.timers = elem
   }
  }
})

new Vue({
    el: '#app',
    router,
    store,
    render: h => h(App),
    mounted: function(){
        const vueApp = this
    }, // --- End of mounted hook --- //
})

and the App.vue file (extract related to Vuex and uibuilder/node-red)

mounted() {

        const app = this;

        uibuilder.onChange('msg', function(msg){
            //console.log('Vue:mounted:UIBUILDER: property msg changed! ', msg)
            app.msgreceived = msg.payload
            app.mytimers = msg.original
            app.timerevents = msg.timer
            app.$store.commit('populate',msg.original)
        }) // ---- End of uibuilder.onChange() watcher function ---- //

    },

So, every message from node-red UIbuilder node will result in populating the $store variables , in this case "timers" .
They will be computed in the various routes where you get them using :
(use the compute section in your area of the vue file)

this.$store.state.timers

Cool, thanks for sharing that. Would you be OK with me adding this to the WIKI? Or indeed, you could add a new page to the WIKI yourself if you like?

I do need to try and find time to play with VueX and VueRouter. Most of my use of Vue though is pretty straight-forward.

Oh , of course. I've got to make it complete though. Feel free to add in a new wiki.
It needs some cleaning up probably.
note that this is currently an example WITH a build step. I am contemplating of converting this to a vanilla uibuilder project without a build step :slight_smile:

This is a project to list/view timers (an array of objects, 1 object per timer), which have an on/off state, and have schedules attached to each of them (i.e. active at certain day(s) of the week ). Each timer runs in its own timezone , if defined, otherwise system time.
UIBuilder gets all properties of the list of timers , in the 'msg.original' object from node-red. (every minute). It also receives a list of "next" events , i.e timers that are going to change state , in the 'msg.timers'. Another requirement of the design is to make it user friendly, viewable on iOS, hence the 'large' amount of styling involved.

Main object received from node red :
msg.original (array of timers). Each timer has a property list , which is an array of schedules, and a property params, which is an array of parameters.
Below is one element of this array as an example of what is received.

[{"name":"s1","params":[{"name":"outputformat","value":0},{"name":"status","value":"1"},{"name":"location","value":""},{"name":"timezone","value":0}],"list":[{"startstop":{"Start":"10:50","Stop":"11:45"},"days":[{"name":"monday","value":true},{"name":"tuesday","value":false},{"name":"wednesday","value":true},{"name":"thursday","value":false},{"name":"friday","value":true},{"name":"saturday","value":false},{"name":"sunday","value":false}]}]},...]

Secondary object received is the list of upcoming events , all that logic in node-red , not in uibuilder.
Below is the object

[{"timer":"s5","period":30,"old_value":0,"new_value":1,"timeperiod":"21:29","state":"success"},{"timer":"s3","period":39,"old_value":1,"new_value":0,"timeperiod":"21:38","state":"danger"},{"timer":"s4","period":443,"old_value":1,"new_value":0,"timeperiod":"04:22","state":"danger"}]

it looks as follows :
image

image

image

File structure : :

project_dir

-- src
  App.vue
  index.js
  index.html

-- src/views/
  Home.vue
  About.vue
  TimeComp.vue
-- src/views/timers
     Timers.vue
     TimerDetails.vue

-- src/router
  index.js

index.js (router) file :

import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import About from '../views/About.vue'
import Timers from '../views/timers/Timers.vue'
import TimerDetails from '../views/timers/TimerDetails.vue'
const routes = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/timers',
    name: 'Timers',
    component: Timers
  },
  {
    path: '/timers/:id',
    name: 'TimerDetails',
    component: TimerDetails
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/:catchAll(.*)',
    redirect: { name: 'Home'}
  }
]
const router = new VueRouter({
  //history: createWebHistory(process.env.BASE_URL),
  mode: 'history',
  base: '/uirouter',
  routes
})

index.js (in src) file

import VueRouter from 'vue-router'
import router from './router'
import Vuex from 'vuex'

import 'bootstrap/dist/css/bootstrap.css'
import 'bootstrap-vue/dist/bootstrap-vue.css'

Vue.use(Vuex);
Vue.use(VueRouter)
Vue.use(BootstrapVue)
Vue.use(IconsPlugin)

const store = new Vuex.Store({
  state: {
          timers:[{name:'s0'}],
    timerevents:[]
  },
  mutations: {
   populate (state,elem) {
           state.timers = elem
   },
   populate_timerevents(state,elem) {
     state.timerevents = elem
   }
  }
})

new Vue({
    el: '#app',
    router,
    store,
    render: h => h(App),
    mounted: function(){
        const vueApp = this
    }, // --- End of mounted hook --- //
})

App.vue file

<template>
    <div>
     <div id="nav">
       <router-link :to="{name:'Home'}">Home</router-link>|
       <router-link :to="{name:'About'}">About</router-link>|
       <router-link :to="{name:'Timers', params: {org:JSON.stringify(mytimers) }}">Timers</router-link>
       <TimeComp/>
     </div>
     <router-view />
   </div>
</template>


<script>

import uibuilder from './../../../node_modules/node-red-contrib-uibuilder/front-end/src/uibuilderfe.js'
import TimeComp from './views/TimeComp.vue'

export default {
data() {
  return {
    msgreceived:"",
    mytimers:[],
  }
},
created() {
  uibuilder.start(this)
},
methods: {
},
mounted() {
        const app = this;
        uibuilder.onChange('msg', function(msg){
            //console.log('Vue:mounted:UIBUILDER: property msg changed! ', msg)
            app.msgreceived = msg.payload
            app.mytimers = msg.original
            app.timerevents = msg.timer
            app.$store.commit('populate',msg.original)
            app.$store.commit('populate_timerevents',msg.timer)
            //console.log(app.$store.state)
        }) // ---- End of uibuilder.onChange() watcher function ---- //

    },
components: {
  TimeComp
}
}


</script>

<style>

#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;
  text-align: center;
  font-family: Roboto;
  font-size: 24px;
  margin: 10px auto;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
  text-decoration: none;
  padding: 10px;
  border-radius: 5px;
}

#nav a.router-link-exact-active {
  color: white;
  background: crimson;
}

</style>

Timer.vue

<template>
<div>
        <h1 class="fade-in-image"> List </h1>
                <div v-for="timer,i in example" :key="i" class="timer">
                        <router-link :to="{ name:'TimerDetails', params: {id:i, org:JSON.stringify(timer) }}">
                        <h2 >{{ timer.name }}</h2>
                        </router-link>
                </div>
</div>
</template>

<script>
 export default {
   data () {
     return {
        mytimers: this.$store.state.timers
     }
   },
   created() {
     },
   mounted() {

   },
   computed: {
    example () {
      return this.$store.state.timers
    }
  }
}
</script>


<style>
h1 {
 margin:10px auto;
 padding:10px;
 text-align: center;
 font-family: Roboto;
}
.timer h2 {
  background: #f4f4f4;
  padding: 20px;
  border-radius: 15px;
  margin:10px auto;
  max-width: 600px;
  cursor: pointer;
  color: #444;
}

.timer h2:hover {
  background: #ddd;
  text-decoration:none!important;
}

.timer a {
  text-decoration:none
}
.timer a:hover {
  text-decoration:none
}

.fade-enter-from {
  opacity: 0;
}
.fade-enter-to {
  opacity: 1
}
.fade-enter-active {
  transition: all 2s ease;
}

.fade-leave-from {
  opacity: 1;
}
.fade-leave-to {
  opacity: 0;
}
.fade-leave-active {
  transition: all 2s ease;
}
.fade-in-image {
    animation: fadein 2s;
    -moz-animation: fadein 2s; /* Firefox */
    -webkit-animation: fadein 2s; /* Safari and Chrome */
    -o-animation: fadein 2s; /* Opera */
}
@keyframes fadein {
    from {
        opacity:0;
    }
    to {
        opacity:1;
    }
}
@-moz-keyframes fadein { /* Firefox */
    from {
        opacity:0;
    }
    to {
        opacity:1;
    }
}
@-webkit-keyframes fadein { /* Safari and Chrome */
    from {
        opacity:0;
    }
    to {
        opacity:1;
    }
}
@-o-keyframes fadein { /* Opera */
    from {
        opacity:0;
    }
    to {
        opacity: 1;
    }
}

</style>

TimerDetails.vue


<template>
  <div>
    <h1>
      Timer <b-badge variant="danger">{{ org.name }}</b-badge>
    </h1>
    <div class="paramcontainer">
      <div class="params">
        <div class="paramitem" v-for="(param, i) in paramtable" :key="i">
          <b-badge variant="light"> {{ param.name }}</b-badge>
          <b-badge variant="primary"> {{ param.value }}</b-badge>
        </div>
      </div>
    </div>
    <div class="schedulecontainer">
      <div class="scheduleheader">
        <span class="schedheaders"
          ><b-badge variant="secondary">Start</b-badge></span
        >
        <span class="schedheaders"
          ><b-badge variant="secondary">Stop</b-badge></span
        >
        <div class="dayheaders">
          <b-badge variant="secondary">Days</b-badge>
        </div>
      </div>
      <div class="schedule" v-for="(schedule, i) in org.list" :key="i">
        <b-badge variant="success"> {{ schedule.startstop.Start }}</b-badge>
        <b-badge variant="danger"> {{ schedule.startstop.Stop }}</b-badge>
        <div class="dayscontainer">
          <div class="weekdays" v-for="(day, j) in schedule.days" :key="j">
            <img
              :class="(day.value && 'dayshow') || 'dayhide'"
              :src="dayicons[j]"
            />
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      id: this.$route.params.id,
      org: JSON.parse(this.$route.params.org),
      ptable: [],
      format: ["String", "Numeric", "Boolean"],
      timerstatus: ["Disabled", "Enabled"],
      tzformat: ["System", "Local"],
      dayicons: [
        "/icons8-monday-40.png",
        "/icons8-tuesday-40.png",
        "/icons8-wednesday-40.png",
        "/icons8-thursday-40.png",
        "/icons8-friday-40.png",
        "/icons8-saturday-40.png",
        "/icons8-sunday-40.png",
      ],
    };
  },
  computed: {
    paramtable() {
      this.ptable.push(
        {
          name: "Format",
          value: this.format[
            this.org.params.find((el) => el.name === "outputformat").value
          ],
        },
        {
          name: "Status",
          value: this.timerstatus[
            this.org.params.find((el) => el.name === "status").value
          ],
        },
        {
          name: "Location",
          value: this.org.params.find((el) => el.name === "location").value,
        },
        {
          name: "Timezone",
          value: this.tzformat[
            this.org.params.find((el) => el.name === "timezone").value
          ],
        }
      );
      return this.ptable;
    },
  },
};
</script>

<style>
.schedtable {
  text-align: center;
  margin: 10px auto;
  max-width: 400px;
}
.schedule {
  width: 25%;
  margin: 0px auto;
}
.schedulecontainer {
  margin-top: 40px;
}
.scheduleheader {
  width: 25%;
  margin: 0px auto;
  font: italic 16px Roboto;
  background: ghostwhite;
}
@media screen and (max-width: 600px) {
  .scheduleheader,
  .schedule {
    width: 100%;
  }
}
.schedheaders {
  display: inline-block;
  margin-left: 3px;
}
.dayheaders {
  display: inline-block;
  margin-left: 25%;
}
.dayscontainer {
  display: inline-block;
}
.dayshow {
  opacity: 1;
  width: 100%;
}

.dayhide {
  opacity: 0;
  width: 100%;
}
.weekdays {
  display: inline-block;
}

@media screen and (max-width: 600px) {
  .weekdays {
    width: 32px;
  }
}

@media screen and (max-width: 600px) {
  .dayshow, .dayhide {
    width: 95%;
  }
}
.paramitem {
  display: flex;
  justify-content: space-between;
  padding: 1px;
}
.paramcontainer {
  display: flex;
  justify-content: center;
}
</style>

index.html

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

    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1">

    <title>NodeRED UI Builder Blank template</title>
    <meta name="description" content="Node-RED UI Builder - Blank template">
    <link rel="icon" href="./images/node-blue.ico">

    <link type="text/css" rel="stylesheet" href="./index.css" media="all">

</head><body>
    <div id="app"></div>

    <!--<script src="../uibuilder/vendor/socket.io/socket.io.js"></script>-->
    <!-- Note no leading / -->
    <script src="./main.js" type="text/javascript"></script>


</body></html>
3 Likes

thanks for the awesome information.

Oh and I forgot.
The icons of the weekdays are in the static httpStatic directory of node-red, as defined in node-red's settings.json, which is in home-dir of node-red (in my case /home/xxx/.node-red)

httpStatic: '/home/xxx/.node-red/static/',

This means references to them in uibuilder are referenced as

"/icons8-monday-40.png"

And in the main project directory (root directory under which all other files/directories are), to enable you to run "npm run build" and "npm run serve"

webpack.config.dev.js (development mode currently)
(manifest.json not needed , as per @TotallyInformation ).

'use strict'

const { VueLoaderPlugin } = require('vue-loader')
const CopyWebpackPlugin = require('copy-webpack-plugin')

module.exports = {
    mode: 'development',
    entry: ['./src/index.js'],
    module: {
        rules: [
            {
                test: /\.vue$/,
                use: 'vue-loader'
            }, {
                test: /\.css$/,
                use: [
                    'vue-style-loader',
                    'css-loader'
                ]
            },
        ]
    },
    // @see https://webpack.js.org/plugins/
    plugins: [
        new VueLoaderPlugin(),
        // Copies from wherever to the dist folder
        new CopyWebpackPlugin({
                patterns: [
            {from: './src/index.html'},
            {from: './src/index.css'},
            {from: './src/manifest.json'},
        ]
        }),
    ],
}

package.json

{
  "name": "uirouter",
  "version": "1.0.0",
  "description": "This is about the simplest template you can get for uibuilder.",
  "main": ".eslintrc.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "webpack --config ./webpack.config.dev.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bootstrap-vue": "^2.21.2",
    "engine.io": "^5.2.0",
    "socket.io-client": "^4.2.0",
    "vue": "^2.6.14",
    "vue-router": "^3.5.2",
    "vuex": "^3.6.2"
  },
  "devDependencies": {
    "copy-webpack-plugin": "^9.0.1",
    "css-loader": "^6.2.0",
    "engine.io-client": "^5.2.0",
    "vue-loader": "^15.9.8",
    "vue-style-loader": "^4.1.3",
    "vue-template-compiler": "^2.6.14",
    "webpack": "^5.53.0",
    "webpack-cli": "^4.8.0"
  }
}

Home.vue file :

<template>
  <div class="home">
    <h3 class="subtitle">Upcoming events </h3>
       <b-list-group>
            <b-list-group-item  class="eventitems" v-for="(timerevent,i) in timerevents" :key="i" href="#" :variant="timerevent.state">
                <div class="d-flex w-100 justify-content-between">
                    <h4 class="mb-1">{{timerevent.timeperiod}} </h4>
                    <h5>{{displaystatus[timerevent.new_value]}}</h5>
                    <h5>{{timerevent.timer}}</h5>
                </div>
            </b-list-group-item>
        </b-list-group>
  </div>
</template>

<script>
// @ is an alias to /src

export default {
  name: 'Home',
  components: {},
  data() {
    return {
      displaystatus: ['Off','On']
    }
  },
  computed: {
    timerevents () {
      return this.$store.state.timerevents
    }
  }

}
</script>
<style scoped>
.subtitle {
  text-align: center;
}
.eventitems {
  width:50%;
  margin:0px auto;
}
</style>

TimeComp.vue (just a clock)

<template>
    <div>
        <div id="clock">
            <p class="date">{{date}}</p>
            <p class="time">{{time}}</p>
        </div>
    </div>
</template>

<script>
    export default {
    name: 'TimeComp',
    data() {
        return {
            time:"",
            date:"",
            week : ['SUN', 'MON', 'TUE', 'WED', 'THU', 'FRI', 'SAT']
        }
    },
    methods: {
        zeroPadding(num, digit) {
            var zero = '';
            for(var i = 0; i < digit; i++) {
                zero += '0';
            }
            return (zero + num).slice(-digit);
        },
        updateTime() {
            var cd = new Date();
            this.time = this.zeroPadding(cd.getHours(), 2) + ':' + this.zeroPadding(cd.getMinutes(), 2) + ':' + this.zeroPadding(cd.getSeconds(), 2);
            this.date = this.zeroPadding(cd.getFullYear(), 4) + '-' + this.zeroPadding(cd.getMonth()+1, 2) + '-' + this.zeroPadding(cd.getDate(), 2) + ' ' + this.week[cd.getDay()];
        }
    },
    mounted() {
        var timerID = setInterval(this.updateTime, 1000);

    }
    }
</script>

<style scoped>

p {
  margin: 0;
  padding: 0;
}

#clock {
  font-family: "Share Tech Mono", monospace;
  text-align: center;
  color: black;
  text-shadow: 0 0 20px crimson, 0 0 20px rgba(10, 175, 230, 0);
}
#clock .time {
  letter-spacing: 0.05em;
  font-size: 18px;
  font-weight: bold;

}
#clock .date {
  letter-spacing: 0.1em;
  font-size: 18px;
  padding: 20px 0 0 0;
  font-weight: bold;
}

Some final comments : Vue2/Vue3

Note that the the router index.js file at the end contains

const router = new VueRouter({
  //history: createWebHistory(process.env.BASE_URL),
  mode: 'history',
  base: '/uirouter',
  routes
})

The base is the name of the URL as in the uibuilder node.
This is Vue2 code. Vue3 (should you use that is slightly different). I kindly refer to
https://next.router.vuejs.org/guide/migration/

How to make sure a vue route is updated dynamically when you change the params of an active (mounted) route (i.e. a list of users where you want to go to the next user .../user/id)

I could find the following solutions

  1. <router-view :key="$route.fullPath"/>

Do this in the vue file where you have your router-view (obviously). This forces a refresh as your fullPath will contain your param.In my case this is done in the main App.vue as I don't have nested views.

  1. watch: {
    $route(to,from) {
    this.id = to.params.id
    this.org = JSON.parse(to.params.org)
    }
    },

The above is in the script section of the route vue file that you want to update

Nice.

Not sure if this helps at all but worth noting that the initial connect msg from Node-RED to your front-end contains a time offset which tells you if the server has an offset from the client (e.g. in a different timezone).

You have the common folder if you want to put that under uibuilder's folder structure instead. It does the same thing. One possible advantage of that is that it should use uibuilder's own security rather than Node-RED's - if I can ever work out how to get security working properly that is! But that might be a future benefit anyway.

Yes, Vue3 does not seem to me to be anywhere near mature enough yet to warrent its use right now.

Thanks @TotallyInformation

On the timezone, definitely interesting , I saw that !
At this stage my server and browser are still in same timezone, but that will change. It's on my todo list to use this once I migrate the data entry code to uibuilder !
The timers (of this project) themselves send info to smart switches and valves that are located outside the server and client timezone though. (e.g. a switch in Europe needs to be turned on from 10:00 AM local time...). So in that case the time entered is UTC or close to it ;-).

On the common folder, definitely , I learnt something !
I must admit one of the big missing pieces so far in the node-red world is indeed built-in security , but thats another story of course . I am following the progress on that topic ! No complaints though, it's not obvious.

In the meantime,I added a sidebar and updated the code, I will edit the above pages very soon.
My experiences :slight_smile: (I could be completely wrong)

  • Bootstrap-vue sidebar : This sidebar is activated with a button that you have to design ,so I am not entirely convinced of the added benefit. And the animation options are very limited (bootstrap 4)
  • Self made sidebar : Biggest learning was how to code : As a separate route,component or just in main Vue file. At this stage , I've added it as it's own component <SideBar/> . I am always showing the component from a component view , so no <v-if="showSideBar"> stuff. This also made it much simpler as no parent-child component communication/events are needed such as this.$event("close")

So, the component is 2 way. The sidebar and a button, the button being used to extract the sidebar. Clicking inside the sidebar makes it go away. There are 3 classes . One for opening with animation, one as opened, and one for closing with oppisite animation. So , I use the :class="sidebarClass"prop to define the class used based on clicking. Here is the new components code

<template>
  <div>
    <button class="sidebarbutton" @click="opensidebar">
      <b-icon font-scale="0.5" icon="list" aria-hidden="true"></b-icon>
    </button>
    <div :class="sideBarClass" @click="closesidebar">
      <h5 class="sidetitle">Timers</h5>
      <div class="sidetimer" v-for="(timer, i) in timers" :key="i">
        <router-link :to="{ name: 'TimerDetails', params: { id: i } }">
          {{ timer.name }}
        </router-link>
      </div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      sideBarClass: "finalsidebarclosed",
    };
  },
  methods: {
    //async closesidebar() {
      closesidebar() {
      this.sideBarClass = "finalsidebarclosing";
      //await new Promise((r) => setTimeout(r, 2000));
      //this.$emit("closebar");
    },
    opensidebar() {
      this.sideBarClass = "finalsidebaropen"
    }
  },
  computed: {
    timers() {
      return this.$store.state.timers;
    },
  },
};
</script>

<style>
/* The side navigation menu */
.sidetitle {
  padding: 50px 0px 20px 0px;
}
.finalsidebaropen {
  text-align: center;
  font-family: Roboto;
  margin: 0;
  padding: 0;
  left: -300px;
  width: 200px;
  background-color: #f1f1f1;
  height: 100%;
  overflow: auto;
  position: absolute;
  top: 0%;
  -webkit-animation: slide 1s forwards;
  animation: slide 1s forwards;
}

.finalsidebarclosed {
  margin: 0;
  padding: 0;
  left: -300px;
  width: 200px;
  background-color: #f1f1f1;
  height: 100%;
  overflow: auto;
  position: absolute;
  top: 0%;
}

.finalsidebarclosing {
  margin: 0;
  padding: 0;
  left: 0px;
  width: 200px;
  background-color: #f1f1f1;
  height: 100%;
  overflow: auto;
  position: absolute;
  top: 0%;
  -webkit-animation: slideclose 0.5s forwards;
  animation: slideclose 1s forwards;
}

/* Sidebar links */
.sidetimer a {
  display: block;
  color: darkslategray;
  padding: 4px;
  text-decoration: none;
}

/* Active/current link */
.sidetimer a.active {
  background-color: #04aa6d;
  color: white;
  text-decoration: none;
}

/* Links on mouse-over */
.sidetimer a:hover:not(.active) {
  background-color: #dc2c3b;
  color: white;
  border-radius: 44px;
  text-decoration: none;
}

@-webkit-keyframes slideclose {
  100% {
    left: -300px;
  }
}

@keyframes slideclose {
  100% {
    left: -300px;
  }
}

@-webkit-keyframes slide {
  100% {
    left: 0;
  }
}

@keyframes slide {
  100% {
    left: 0;
  }
}

.sidebarbutton {
  position: absolute;
  background: lightgray;
  top: 30%;
  left: -4px;
  height: 200px;
  border-radius: 5px;
  border: none;
  box-shadow: 2px 2px #eeeeee;
}

</style>

As can be seen an earlier version contained , the following code:

    async closesidebar() {
      closesidebar() {
      this.sideBarClass = "finalsidebarclosing";
      await new Promise((r) => setTimeout(r, 2000));
      this.$emit("closebar");
    },

This was at the time I was using a v-if on the main App.vue to show/hide the component, but I had to use this async function to make sure the animation of closing the sidebar was shown. You could try to use the <transition></transition> element ,but there I encountered issues with a transition showing a slide . (all the examples are related to an opacity animation. ).

Voila. that's it for today !
App.vue (updated)

<template>
  <div>
    <div id="nav">
      <router-link :to="{ name: 'Home' }">Home</router-link>|
      <router-link :to="{ name: 'About' }">About</router-link>|
      <router-link :to="{ name: 'Timers' }">Timers</router-link>
      <TimeComp />
    </div>
    <router-view />
    <div>
      <SideBar />
    </div>
    <!--<div class="backdropsection2" @click="offsidebar"></div>-->
  </div>
</template>


<script>
import uibuilder from "./../../../node_modules/node-red-contrib-uibuilder/front-end/src/uibuilderfe.js";
import TimeComp from "./views/TimeComp.vue";
import SideBar from "./views/SideBar.vue";

export default {
  data() {
    return {
      msgreceived: "",
      mytimers: [],
      showSideBar: false,
    };
  },
  created() {
    uibuilder.start(this);
  },
  methods: {
    opensidebar() {
      this.showSideBar = true;
    },
    closesidebar() {
      this.showSideBar = false;
    },
  },
  mounted() {
    const app = this;
    uibuilder.onChange("msg", function (msg) {
      app.msgreceived = msg.payload;
      app.mytimers = msg.original;
      app.timerevents = msg.timer;
      app.$store.commit("populate", msg.original);
      app.$store.commit("populate_timerevents", msg.timer);
    }); // ---- End of uibuilder.onChange() watcher function ---- //
  },
  components: {
    TimeComp,
    SideBar,
  },
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
}

#nav {
  padding: 30px;
  text-align: center;
  font-family: Roboto;
  font-size: 24px;
  margin: 10px auto;
}

#nav a {
  font-weight: bold;
  color: #2c3e50;
  text-decoration: none;
  padding: 10px;
  border-radius: 5px;
}

#nav a.router-link-exact-active {
  color: white;
  background: crimson;
}

.backdropsection2 {
  height: 100%;
}

.sidebarbutton {
  position: absolute;
  background: lightgray;
  top: 30%;
  left: -4px;
  height: 200px;
  border-radius: 5px;
  border: none;
  box-shadow: 2px 2px #eeeeee;
}
</style>

Oh, I also added a monitor screen in the about menu (which I should rename). This uses a bootstrap-vue progress bar, to show not progress, but status (green/red animated). found this an elegant way iso designing green/red buttons !

About.vue

<template>
  <div class="fade-in-image about">
    <div class="monitorcontainer">
      <div>
        <div v-for="(timer, i) in timers" :key="i" class="monitoritem">
          <router-link :to="{ name: 'TimerDetails', params: { id: i } }">
            <div class="badgeprocesscontainer">
            <b-badge class="monitorbadge" variant="secondary">{{ timer.name }}</b-badge>
            <b-progress
              class="monitorprogress"
              :variant="isActive(i)"
              value="10"
              max="10"
              animated
            ></b-progress>
            </div>
          </router-link>
        </div>
      </div>
    </div>
    <div class="copyright">
      Written in Vue,Bootstrap Vue , Vuex ,Vue Router, uIbuilder and Node Red
    </div>
  </div>
</template>

<script>
export default {
  methods: {
    isActive(key) {
      if (this.$store.state.timers[key].currentstatus === 0) {
        return "danger";
      } else {
        return "success";
      }
    },
  },
  computed: {
    timers() {
      return this.$store.state.timers;
    },
  },
};
</script>

<style>
.about {
  font-family: Roboto;
}
.monitorcontainer {
  display: flex;
  justify-content: center;
}
.monitorbadge {
  vertical-align: super !important;
}
.monitoritem {
  padding: 10px 2px 0px 0px;
  display: flex;
  justify-content: end;
}
.copyright {
  font-family: Roboto;
  text-align: center;
  padding-top: 10px;
}
.monitorprogress {
  width: 30px;
  margin-left: 10px;
}
.monitoritem a:hover {
  text-decoration:none!important;}
  
.badgeprocesscontainer {
  display: flex;
}
</style>

image

Frankly it is driving me mad! :grinning: I know security from an IT and governance perspective but I am not a security engineer. So getting the process right is proving to be infuriating.

But each itteration of my thinking is bringing me closer and I'm hopeful I'll crack the remaining problems soon. In the meantime, it is forcing me to continue to improve the code-base and the next itteration of uibuilder will be a LOT more modular. I'm also introducing a new "Experimental" flag so that in the future, I can include code in live releases that may not be ready for production use but that you might want to trial.

I included bootstrap-vue originally because it gives people a pretty much zero touch, minimal boilerplate "nice" layout and style. I do wonder though whether bootstrap is keeping up with the times?

Cool, that could be interesting. Not sure if you've seen my experimental components? These were put together to test out some concepts on bridging the gap between the simplicity of Node-RED's Dashboard and the coding requirements of uibuilder.

The idea being that you can add a simple component to your HTML and control it from Node-RED just by sending a message with the right data schema.

Not sure if that is useful for your sidebar but I am definatly looking for ways to continue to bridge that gap.

I'm also starting to prepare the ground for allowing other, 3rd-party nodes to interact with uibuilder.


Definately some great ideas in your code, thanks for sharing.

I have switched to vuetify (in both uibuilder and standalone projects)

It has decent capabilities.

The reason I didn't use that was that it needed a build step in order to use it and I wanted to avoid forcing people who already struggle with the concepts to learn a whole bunch of other concepts just to get started.

It would be interesting to get a quick write-up of your tooling and workflow though Steve and now that uibuilder accepts external templates, it might be really interesting to have a vuetify template!

Templates now cover the whole root folder for a uibuilder instance which means they can include a package.json with a build script and dev dependencies built right in. In a future release, the uibuilder editor panel will recognise common package.json script names and present them in the panel UI to make them easy to run as well.

1 Like