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.

HNY!
Thanks! I've deleted everything old and make a clean installation of the svg example floorplan. Now it works as it should be.
And started to move on.
The next plan is to load this svg floorplan with bulbs as a child page and access to it from a menu page link.

At first I started from complex framework routers like React - and tryed with it. But have some errors that cannot load the HTML element where the template is (the bulbs did not load).
Then I went the other way, started simplifying the code in an attempt to figure it out found a ready router on simple pure js. But the same story happened with it, they were not displayed as they should.
And then I started to study the documentation of UiBuilder deeper and realized that all the tools are already here and in a very convenient form, I'm an idiot =).

So now I took the menu from the example documentation and am trying to display the floorplan on one of the pages. The same error as with other routers.
Apparently I'm confused in the architecture of the page construction and the loaded elements. Moreover as the main index floorplan page - it works, but as a loaded one - there are errors.
I can't figure it out, I've tried a lot of things. I hope for advice.

index.html

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

<head>

    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <link rel="icon" href="../uibuilder/images/node-blue.ico">

    <title>FE Router - Node-RED uibuilder</title>
    <meta name="description" content="Node-RED uibuilder - FE Router">

    <!-- Your own CSS (defaults to loading uibuilders css)-->
    <link type="text/css" rel="stylesheet" href="./index.css" media="all">

    <!-- #region Supporting Scripts. These MUST be in the right order. Note no leading / -->
    <script defer src="../uibuilder/uibuilder.iife.min.js"></script>
    <script defer src="../uibuilder/utils/uibrouter.iife.min.js"></script>
    <script defer src="./js/uibrouter.js"></script>
    <script defer src="./index.js"></script>
    <!-- #endregion -->

<template id="home">
        <h2>This comes from an internal <code class="r01style">&lt;template></code> tag</h2>
        <div>
            Route 1
        </div>
        <script>
            console.log('I was produced by a script in Route 1')
        </script>
        <style> 
            .r01style {
                background-color: yellow;
                color: blue;
                font-weight: 900;
            }
        </style>
</template>

</head>

<body class="uib">
    <script>
        console.log('a script at the start of body')
    </script>

    <header>
        <h1 class="with-subtitle">An example of a framework-less front-end router</h1>
        <div role="doc-subtitle">Using the UIBUILDER IIFE library.</div>

        <h2>Current Route Title: <uib-var variable="uibrouter_CurrentTitle"></uib-var>
        </h2>

        <nav id="main-menu" class="horizontal" aria-labelledby="primary-navigation">
            <h2 id="primary-navigation">Menu:</h2>
            <ul id="routemenu" role="menubar" aria-describedby="main-menu">
                <!-- <li><a href="#route01" onclick="router.doRoute(event)">#1</a></li> -->
                <li data-route="route01" tabindex="0" role="menuitem"><a href="#route01">Home</a></li>
                <li data-route="route02" tabindex="0" role="menuitem"><a href="#route02">SVG1</a></li>
                <li data-route="route03" tabindex="0" role="menuitem"><a href="#route03?doh=rei">SVG2</script></a></li>
            </ul>
        </nav>
    </header>

    <main>
        <div id="more">
            <!-- '#more' is used as a parent for dynamic HTML content in examples -->
        </div>

        <div id="uibroutecontainer">
            <!-- router content will appear here -->
        </div>

        
    </main>



</body>

</html>

floorplan.html

<template id="bulb-template">
    <svg id="mybulb" class="bulb" height="3rem" viewBox="0 0 1024 1024" version="1.1"
        xmlns="http://www.w3.org/2000/svg">
        <defs>
            <filter id="shadow">
                <feDropShadow dx="1" dy="1" stdDeviation="5" flood-opacity="50%" />
            </filter>
            <filter id="glow" filterUnits="userSpaceOnUse" x="-50%" y="-50%" width="200%" height="200%">
                <!-- blur the text at different levels-->
                <feGaussianBlur in="SourceGraphic" stdDeviation="5" result="blur5" />
                <feGaussianBlur in="SourceGraphic" stdDeviation="10" result="blur10" />
                <feGaussianBlur in="SourceGraphic" stdDeviation="20" result="blur20" />
                <feGaussianBlur in="SourceGraphic" stdDeviation="30" result="blur30" />
                <feGaussianBlur in="SourceGraphic" stdDeviation="50" result="blur50" />
                <!-- merge all the blurs except for the first one -->
                <feMerge result="blur-merged">
                    <feMergeNode in="blur10" />
                    <feMergeNode in="blur20" />
                    <feMergeNode in="blur30" />
                    <feMergeNode in="blur50" />
                </feMerge>
                <!-- recolour the merged blurs red-->
                <feColorMatrix result="red-blur" in="blur-merged" type="matrix" values="1 0 0 0 0
                                            0 0.06 0 0 0
                                            0 0 0.44 0 0
                                            0 0 0 1 0" />
                <feMerge>
                    <!--<feMergeNode in="red-blur"/>        largest blurs coloured red -->
                    <feMergeNode in="blur-merged" />
                    <feMergeNode in="blur5" /> <!-- smallest blur left white -->
                    <feMergeNode in="SourceGraphic" /> <!-- original -->
                </feMerge>
            </filter>
        </defs>
        <title>TITLE</title>
        <path name="icon"
            d="M511.549861 803.293331H408.419043a73.232959 73.232959 0 0 1-67.1862-41.991375 59.795719 59.795719 0 0 1-6.71862-30.569722 207.60536 207.60536 0 0 0-33.593101-113.88061 196.519637 196.519637 0 0 0-27.882273-33.5931A463.248853 463.248853 0 0 1 217.274302 504.314738a399.086031 399.086031 0 0 1-36.95241-75.248544 242.542184 242.542184 0 0 1-15.116895-77.264131 349.032312 349.032312 0 0 1 8.062344-84.990544 314.76735 314.76735 0 0 1 51.733375-114.888403A367.172586 367.172586 0 0 1 361.724634 34.011334 327.532728 327.532728 0 0 1 433.949799 8.144647 369.524103 369.524103 0 0 1 528.682342 0.418234a333.579486 333.579486 0 0 1 126.310057 29.225997 326.860866 326.860866 0 0 1 70.881442 44.678824A382.625412 382.625412 0 0 1 808.848799 168.383736a314.095488 314.095488 0 0 1 41.991375 105.146403 312.751764 312.751764 0 0 1 6.382689 92.045095 275.799353 275.799353 0 0 1-20.15586 76.256338 449.139751 449.139751 0 0 1-61.139443 107.16199 497.513815 497.513815 0 0 1-33.5931 39.639858 160.575019 160.575019 0 0 0-31.241583 48.038134 215.331773 215.331773 0 0 0-18.812136 55.428615c-1.679655 11.757585 0 23.179239-2.687448 33.5931a171.660742 171.660742 0 0 1-3.695241 25.194826 69.873649 69.873649 0 0 1-33.593101 40.647651 74.576683 74.576683 0 0 1-39.639858 10.07793zM490.050277 88.768088c-11.085723 0-22.171446 2.351517-33.5931 4.031172a210.96467 210.96467 0 0 0-74.240752 26.538549 244.221839 244.221839 0 0 0-55.428616 44.342893 222.386324 222.386324 0 0 0-43.335099 63.82689 230.784599 230.784599 0 0 0-19.483998 94.732543 28.218204 28.218204 0 0 0 33.5931 28.890066 28.890066 28.890066 0 0 0 22.171446-26.202618v-13.773171a167.965501 167.965501 0 0 1 9.406068-49.045927 184.762052 184.762052 0 0 1 64.834684-83.98275 167.965501 167.965501 0 0 1 93.72475-33.593101 142.770676 142.770676 0 0 0 18.140274 0 23.851101 23.851101 0 0 0 19.148067-15.452826 33.5931 33.5931 0 0 0 0-19.483998 23.51517 23.51517 0 0 0-20.491791-18.140274 122.950747 122.950747 0 0 0-15.116895 0zM647.601917 943.040628a15.788757 15.788757 0 0 1-13.773171 15.116895H400.356699a17.468412 17.468412 0 0 1-16.460619-8.734206 18.812136 18.812136 0 0 1 0-20.15586 16.124688 16.124688 0 0 1 16.460619-4.703034h227.089358a19.148067 19.148067 0 0 1 19.148067 20.827722zM405.731595 899.369598a18.140274 18.140274 0 0 1-16.460619-12.765378 17.804343 17.804343 0 0 1 15.452826-23.851101H635.508401a18.812136 18.812136 0 0 1 17.804343 13.773171 19.819929 19.819929 0 0 1-10.749792 21.499584 24.187032 24.187032 0 0 1-8.734206 0H423.535938zM437.64504 1022.992207a17.132481 17.132481 0 0 1-15.452826-9.406068 18.140274 18.140274 0 0 1 15.116895-26.202618h139.411367a19.819929 19.819929 0 0 1 19.483998 17.804343 16.124688 16.124688 0 0 1-8.734206 15.788757 19.148067 19.148067 0 0 1-9.741999 3.023379H442.348074z" />
        <!-- <circle name="default" cx="50" cy="50" r="50"></circle> -->
    </svg>
</template>

<h1 class="with-subtitle">Dynamic SVG Example</h1>
<div role="doc-subtitle">Using the uibuilder IIFE library.</div>
<p>
    This is a uibuilder example looking at how easy it is to create a dynamic view of IoT devices in a building
    using SVG images both for the background (floor-plan) and device indicators.
</p>

<div id="more">
    <!-- '#more' is used as a parent for dynamic HTML content in examples -->
</div>

<h2>My House, Ground Floor</h2>
<div id="floorplan" class="plan">
    <!-- Bulb icons dynamically inserted here -->
</div>

uibrouter.js

// @ts-nocheck
/*globals UibRouter, uibuilder */

const routerConfig = {
    // Router templates created inside the routeContainer, specify an CSS selector
    // If not provided, default div with ID uibroutecontainer is added as the last element of the body
    routeContainer: '#routecontainer',

    // Optionally, chose a default route id to be displayed on load
    // If not given, the first defined route is used.
    // defaultRoute: 'route03',

    hide: true,
    // unload: true,

    // Define the possible routes type=url for externals
    // Can be an object or an array but each entry must be an object containing {id,src,type}
    //   type can be anything but only `url` will be treated as an external template file.
    //   src is either a CSS selector for a <template> or a URL of an HTML file.
    //   id must match the href="#routeid" in any menu/link. and `<template id="routeid">` on any loaded template
    //      must be unique on the page
    routes: [
        {
            id: 'route01', src: 'part/home.html', type: 'url',
            title: 'Route 1 home', description: 'home'
        }, {
            id: 'route02', src: 'part/floorplan.html', type: 'url',
            title: 'Route 2 svg1', description: 'svg1'
        }, {
            id: 'route03', src: 'part/route03.html', type: 'url',
            title: 'Route 3 svg2', description: 'svg2'
        },
        // Doesn't exist. Tests load error
        { id: 'route04', src: './fe-routes/dummy.html', type: 'url' },
    ],
}
const router = new UibRouter(routerConfig)

// Example of dynamically adding additional routes -> must be external or have existing templates
const extraRoutes = [
    { id: 'route03', src: 'partial/route03.html', type: 'url', title: 'Route 5' },
    { id: 'route06', src: '#route06' /* NB: No title specified */ },
]
router.addRoutes(extraRoutes)

// Currently no way to dynamically add new routes from Node-RED
// So we need to do it here
uibuilder.onTopic('addRoute', (msg) => {
    router.addRoutes(msg.payload)
    console.log('Route added from Node-RED', msg.payload.id)
})

// - Optionally send a msg back to Node-RED when the route changes
// uibuilder.watchUrlHash()

// Example of changing route from code (after 5 seconds):
// setTimeout(() => {
//     router.doRoute('route01')
// }, 5000)

/** If you need to be certain that all external route templates
 *  have loaded before doing something, this is how. */
// uibuilder.onChange('uibrouter', uibrouter => {
//     if (uibrouter === 'loaded') {
//         // Do stuff
//     }
// })
/** Monitor route changes in code */
// uibuilder.onChange('uibrouter_CurrentRoute', (routeId) => {
//     console.log(`ROUTE CHANGED. New Route: ${routeId}`)
//     // To get the previous route, use: router.previousRouteId
//     // To get the current route's config, use: router.currentRoute()
// })
/** Monitor route changes in code and get the new route config */
// uibuilder.onChange('uibrouter_CurrentDetails', (routeConfig) => {
//     console.log(`ROUTE CHANGED. New Route Details: ${routeConfig}`)
// })

index.js and css are same as in floorplan template

pic1

pic2

OK, a bit hard to pick apart what is happening.

1st thing. Your routes are defined as being in the part sub-folder but I can't see that in the pics?

2nd thing. you have a number of errors showing on the console. You've fist tried to use $() to select an element that doesn't yet exist on the page. Then you have some issues with your index.js file. Is there a reason you have both a js/router.js and an index.js? Probably easier just to have the index.js at least to begin with.

I use the same code taken from the examples svg floorplan and router.
The only i changed is the folders and paths to the files. Pages with simple html just for test and svg.

If I output the floorplan as in example to the index.html, everything works.
Bulbs appear, all good.

As soon as I place the floorplan as html page loaded through the router - that provides index.js error "Template HTMLElement not provided..".
In one way it works, in second not and I don't understand why "bulb-template" didnt loads to clone.

Maybe the way how the bulb template is called incorrect..

also pics of root structure


You need to share code rather than images.

Sorry, my bad

So root structure:

/src
   - index.css
   - index.html (Copyed from example of a router)
   - index.js (Copyed from svg floorplan example)
/src/js
   - uibrouter.js (Copyed from router example. Separated for test with index.js)
/src/part
   - floorplan.html (html to load by router with <template id="bulb-template">)
   - home.html (simple test html page)
   - route03.html (simple test html page)
   - background.svg (svg floorplan bg)

uibrouter.js
floorplan.html
index.html

I've posted above

Forgot about index.js
// @ts-nocheck
/** Dynamic SVG example */

uibuilder.logLevel = 4

/** A clone of the uibuilder $ function so you don't have to install v6.2-dev
 * Simplistic jQuery-like document CSS query selector, returns an HTML Element
 * If the selected element is a <template>, returns the first child element.
 * type {HTMLElement}
 * @param {string} cssSelector A CSS Selector that identifies the element to return
 * @returns {HTMLElement|HTMLTemplateElement|null}
 */
function mySelector(cssSelector) {
  let el = document.querySelector(cssSelector)

  if (!el) {
    log(1, 'mySelector', `No element found for CSS selector ${cssSelector}`)()
    return null
  }

  if (el.nodeName === 'TEMPLATE') {
    el = el.content.firstElementChild
    if (!el) {
      log(0, 'mySelector', `Template selected for CSS selector ${cssSelector} but it is empty`)()
      return null
    }
  }

  return el
}

function doMe(event) {
  uibuilder.eventSend(event)
}

/** Insert a clone of a template tag
 * NB: Template should have only a single direct child tag, nothing other than that tag and its contents will be cloned.
 * @param {HTMLTemplateElement} template The template to clone and insert
 * @param {HTMLElement} parent The parent within which to insert the clone
 * @param {object} [ui] Optional uib UI object that will apply changes to the cloned element (e.g. attribs, slot)
 */
function htmlClone(template, parent, ui) {
  console.log(ui)
  if (!template || !(template instanceof Element)) {
    console.error('Template HTMLElement not provided or is not an HTML Element')
    return
  }
  if (!parent || !(parent instanceof Element)) {
    console.error('Parent HTMLElement not provided or is not an HTML Element')
    return
  }
  if (!ui) ui = {}
  if (!ui.position) ui.position = 'last'

  const clone = template.cloneNode(true)

  // Oops! Fns starting with `_` should not have been used - sorry. This fn no longer available directly.
  // Will add an equivalent in a future release (post v6.4.1) probably called `uiEnhanceElement`
  // uibuilder._uiComposeComponent(clone, ui)
  clone.id = ui.id
  clone.classList.add(ui.attributes['data-posn'])
  clone.dataset.state = ui.attributes['data-state']
  clone.dataset.posn = ui.attributes['data-posn']
  clone.onclick = doMe //uibuilder.eventSend

  if (ui.position === 'first') {
    // Insert new el before the first child of the parent. Ref: https://developer.mozilla.org/en-US/docs/Web/API/Node/insertBefore#example_3
    parent.insertBefore(clone, parent.firstChild)
  } else if (Number.isInteger(Number(ui.position))) {
    elParent.insertBefore(clone, parent.children[ui.position])
  } else {
    // Append to the required parent
    parent.appendChild(clone)
  }

}
const x = {
  "id": "bulb1",
  "attributes": {
    "class": "bulb posn1",
    "data-state": "off",
    "data-posn": "posn1"
  },
  "events": {
    "click": "uibuilder.eventSend"
  },
  "position": "last"
}

// The Template tag in index.html contains a template "bulb" SVG image
// Here, we clone that multiple times and set some properties.
// Note that `htmlClone` is a function that will land in the uibuilder client in v6.2
// Also, the $ function is improved in v6.2 so a copy of that is included here for convenience.
//
// We track state and position class on data-* attributes so that it is much easier to process
// click events in Node-RED without having to create a custom click handler, we can just use the standard eventSend.
// CSS classes do all the clever stuff 😁

htmlClone($('#bulb-template'), $('#floorplan'), {
  // As always, we set a unique ID for every created element so it can be updated easily later
  id: 'bulb1',
  // You only need this if you want to choose 'first' or a position number,
  // the clone will be added at the specified child position of the parent.
  // position: 'last', 
  attributes: {
    // Apply base bulb class and a positioning class
    class: 'bulb posn1',
    // Track the on/off state separately - makes processing in Node-RED easier
    'data-state': 'off',
    // Track the position class separately - makes processing in Node-RED easier
    'data-posn': 'posn1'
  },
  // We have to add event handlers after a clone, they cannot be included in the template
  events: {
    click: 'uibuilder.eventSend'
  }
})

htmlClone(mySelector('#bulb-template'), mySelector('#floorplan'), {
  id: 'bulb2',
  attributes: {
    class: 'bulb posn2',
    'data-state': 'off',
    'data-posn': 'posn2'
  },
  events: {
    click: 'uibuilder.eventSend'
  }
})

htmlClone(mySelector('#bulb-template'), mySelector('#floorplan'), {
  id: 'bulb3',
  attributes: {
    class: 'bulb posn3',
    'data-state': 'off',
    'data-posn': 'posn3'
  },
  events: {
    click: 'uibuilder.eventSend'
  }
})

htmlClone(mySelector('#bulb-template'), mySelector('#floorplan'), {
  id: 'bulb4',
  attributes: {
    class: 'bulb posn4',
    'data-state': 'off',
    'data-posn': 'posn4'
  },
  events: {
    click: 'uibuilder.eventSend'
  },
})

I still think you need to simplify things. Combine the /js/uibrouter.js and index.js but first move all of the floorplan code out of index.js and put it into the floorplan.html for now - you might need to wrap it in something to prevent it being re-executed when changing routes.

Remember that the page's DOM may not always contain the floorplan elements if you are telling the router to unload routes from the DOM. And may not yet exist if you haven't yet gone to that route.

Thanks!
So.. what i've done. Tryed it in a different ways.

  • Moved uibrouter to index.js. Only uibrouter code there now.
  • Added all floorplans functional js code straight to the floorplan.html page.
  • Added two test svg plans on two different pages.

Including js file with floorplans code like <script defer src="path/myfloorplan.js</script> with right path ofc - do nothing. Like as if this line is ignored.
Only after i added full code straight in script tag in html page - began to load as I planned.

BUT
Despite of working im feeling that this is a bad idea and could be much better.

And seems im failed with it.
Still having whitespases for now how everything interacts together and in what sequence compiles.

Svg page pics


Sorry, I'm struggling to see the big picture here so it is hard to see what might be going wrong. Are you able to put your src folder onto GitHub or somewhere and share it with me. I can take a clearer look then.