UIBUILDER + VueJS Dynamic Floorplan - assistance needed

Hi Julian,
and others

Great and very interesting work! I'm new and just getting into nodered and uibuilder.
My apologize if im raising an old topic but im totally lossed how to move further.

Im thying to do a HMI panel with light control (mostly) based on uib and SVG floorplan.
I don't know frontend very well.. but whats done at this moment:

Structure:


builded on vue

also
Im using sidebar menu with router to load pages.
And thats how it looks like now

Next point i want to place some bulbs in rooms and have an option to control them from front and backend.

But something im doing wrong.. idk where's problem.
I can't get the bulbs to show properly.
Maybe i should somehow declare bulbs and call it in different way in vue...

Thats how appvue js and light page looks like now:



Hi, moved this to a new topic because the old one was very long. I need to remind myself of the old example.

Can I ask to start though, are you on current versions of node.js, Node-RED and UIBUILDER? Also, did you base your attempt on the latest example?

https://flows.nodered.org/flow/02eb3716157db586f3f5b8a85c241009

You also need to share your code rather than images please so that I can examine it and try it out if needed.

nodejs - 20.5.1
nodered - 4.0.5
uib - 7.0.4

Yes, ive tryed latest example and old one. Also trying different ways to combine old and one.
100% I'm missing something in front.

app.vue

<script setup lang="ts">
import { RouterView } from 'vue-router'
import SidebarMenu from '@/components/SidebarMenu.vue';
</script>

<template>
  <div id="root">
    <SidebarMenu class="sidebar" />
    <RouterView class="router-view" />
  </div>
</template>

<style scoped lang="scss">
#root {
  display: grid;
  grid-template-areas: 'sidebar router-view';
  grid-template-columns: auto 1fr;
  font-family: sans-serif;
}

.sidebar {
  grid-area: 'sidebar';
}

.router-view {
  grid-area: 'router-view';
  padding: 1rem;
}
</style>

index.js

var app1 = new Vue({
    el: '#app',
    data: {

        isonb: false,
        isona: false,
        isonc: false,
        isond: false,

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

    methods: {

        // Called if one of the bulb icons is clicked
        myClick: function (returnedData) {

            // returnedData.srcId contains the element id, returnedData.event contains the click event object
            console.log('Somebody clicked the ' + returnedData.srcId + ' icon!', returnedData)

            var out

            if (returnedData.srcId === 'a') {
                out = this.isona = !this.isona
            }
            if (returnedData.srcId === 'b') {
                out = this.isonb = !this.isonb
            }
            if (returnedData.srcId === 'c') {
                out = this.isonc = !this.isonc
            }
            if (returnedData.srcId === 'd') {
                out = this.isond = !this.isond
            }

            // Lets tell Node-RED
            uibuilder.send({
                'topic': returnedData.srcId.toUpperCase(),
                'payload': out,
            })

        }, // -- End of myClick -- //

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

    // Get things started
    mounted: function () {

        /** **REQUIRED** Start uibuilder comms with Node-RED */
        uibuilder.start()

        // msg is updated when a standard msg is received from Node-RED
        uibuilder.onChange('msg', function (msg) {

            if (msg.payload.hasOwnProperty('A')) {
                app1.isona = msg.payload.A
            }
            if (msg.payload.hasOwnProperty('B')) {
                app1.isonb = msg.payload.B
            }
            if (msg.payload.hasOwnProperty('C')) {
                app1.isonc = msg.payload.C
            }
            if (msg.payload.hasOwnProperty('D')) {
                app1.isond = msg.payload.D
            }

            console.log('incoming msg', app1.isona, app1.isonb, app1.isonc, app1.isond, msg)
        })

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

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

routes.ts

import { RouteRecordRaw } from "vue-router"
import Home from "@/views/Home.vue"

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    component: Home,
  },
  {
    path: '/light1',
    component: () => import('@/views/Light1.vue'),
  },
  {
    path: '/light2',
    component: () => import('@/views/Light2.vue'),
  },
  {
    path: '/vent',
    component: () => import('@/views/Vent.vue'),
  },
  {
    path: '/entertainment',
    component: () => import('@/views/Entertainment.vue'),
  },
]

export default routes

Light1.vue

<script setup lang="ts"> 
</script>

<template id="bulb-template">
<div id="floorplan" class="light1">
    <bulb id="a" :color="isona ? 'red' : 'grey'" :glow="isona" :clickable="false" x="100" y="100" @bulbclicked="myClick"
        title="A: This one does not respond to clicks">
        <desc>Here we use a component slot to insert some more custom svg</desc>
    </bulb>
    <bulb id="b" :color="isonb ? 'red' : 'grey'" :glow="isonb" :clickable="true" x="270" y="120" @bulbclicked="myClick"
        title="B">
        <circle cx="50" cy="50" r="50" />
    </bulb>
    <bulb id="c" :ison="isonc" :clickable="true" x="650" y="120" @bulbclicked="myClick" title="C"></bulb>
    <bulb id="d" :ison="isond" :clickable="true" x="250" y="270" @bulbclicked="myClick"
        :title="'D: ' + (isond ? 'ON' : 'off')"></bulb>
</div>
</template>


<style scoped lang="scss">
    .example-one {
        background-color: #3a3451;
    }

.light1 {
position: relative;
background: url("./1fl.svg") no-repeat;
background-size: 100% 100%;
background-color: #3a3451;
background-position: center center;
background-origin: content-box;
padding: 20px;
}

:root {
--warning-intense: hsl(
var(--warning-hue) 100% 50%
);
--failure-intense: hsl(
var(--failure-hue) 100% 50%
);
--surface5: hsl( /* additional background shade */
var(--brand-hue)
calc(100% * var(--surfaces-saturation))
calc(
100% * (var(--surfaces-lightness)
- (var(--surfaces-factor) * .20)
+ (var(--surfaces-factor) * var(--surfaces-bias)))
)
);
}

/* Bulb classes control look, colour and position */

.bulb { /* Default "off" class plus standard style */
z-index: 9999 !important; /* Bulbs HAVE to be in the top z-layer */
cursor: pointer;
position: absolute; /* allows exact positioning within the parent div */
transition: filter 2s ease-in-out 0s;
background-color: rgba(0, 0, 0, 0.001); /* transparent background */
filter: url("#shadow"); /* selects the shadow filter */
}
.bulb path {
fill: grey;
}

.bulb-warn { /* Standard "on" class */
filter: url('#glow'); /* selects the glow filter instead of shadow */
}
.bulb-warn path {
fill: var(--warning-intense);
}

.bulb-fail { /* Alternative "on" class with different colour */
filter: url('#glow');
}
.bulb-fail path {
fill: var(--failure-intense);
}

/* Bulb position classes, change as needed
* Positions are relative to the parent floorplan div
*/
.posn1 {
top: 100px; left: 100px;
}
.posn2 {
top: 120px; left: 270px;
}
.posn3 {
top: 120px; left: 650px;
}
.posn4 {
top: 270px; left: 250px;
}

</style>

main.ts

import { createApp } from 'vue'

import 'the-new-css-reset/css/reset.css'

import App from '@/App.vue'
import router from '@/router/index'

const app = createApp(App)

app.use(router)

app.mount('#app')

structure looks like

this

src
-App.vue
-env.d.ts
-index.css
-index.html
-index.js
-main.ts
src/components
-SidebarMenu.vue
src/router
-index.ts
-routes.ts
src/views
-1fl.svg
-Home.vue
-Light1.vue
-Light2.vue
-Vent.vue
-1fl.svg

Is that right? Doesn't look like you are using Typescript.

If those really are .ts files and you haven't added a typescript compile step, then I doubt it will work. I can't help with Typescript I'm afraid as I don't use it. I certainly don't know how to integrate it with VueJS.

Just a future feature thought: Node 22 supports type stripping for executing TS code directly :thinking:

Which makes me think node-red functions could also support TS code in some future :thinking::thinking::thinking:

It is only an experimental feature so it needs an extra parameter when calling Node.js. Fairly unlikely to be the case here - but yes, not impossible. But if in use, I don't know if there would be any impact on the build process for Vue.

Yes, this is an aim of Node.js and is already possible in Bun? Deno? can't quite remember which. It may well make its way into browsers as well.

I couldn't make a sidebar menu from scratch, so Ive took as template this sidebar

After that I added pages so that they could be accessed separately from a side menu.
And on each page include some usable UI elements like buttons, svg with light, maybe graphs etc.

I'm not sure how much I can help as I don't understand the interplay between your Typescript and Vue.

However:

That is generally no longer needed in the more recent versions of UIBUILDER.

Add a console.log statement so you can see if that is working and what is being received as data.

Also, I would temporarily remove the router to simplify the code. Get a single image page working first and then once that is working, you can apply to your routes.

Where is <bulb> defined? Maybe I missed it but I can't see it.

Thanks for the advices! Will try step by step.

I'll probably try it again from scratch with a better understanding of how vue and uib work.
Anyway.. i don't want to give up, maybe it could take some time.
I will update post on any successes

Seems also ts are not needed here

1 Like

Do you even need Vue? Modern HTML/CSS/JS reduces the need for Vue or similar complex frameworks. If you really need the help of a framework, you might want to look at Svelte or HTMx which are much closer to native capabilities.