Is this an edge case?
How do users normally deal with click events. Have I approached this differently to others (I couldn't find any examples anywhere, but read your docs, and a little help from AI to get this far)
The timing issue is an edge-case, not clicking, that is quite normal.
Honest, I only got back a couple of hours ago after a long couple of days so I don’t think I’ve fully gone through your original posts in enough detail. After all, Marcus was dealing with it
Ah, penny begins to drop - you have a button as a drop-down, instead of using a <select>
with <options>
.
I'm pretty sure.
The dynamic HTML is trying to resolve (via onclick) the callback, before DomLoaded
but its DomLoaded
that creates the handler
Can you try
document.addEventListener('DOMContentLoaded', () => {
setTimeout(()=>{
Setup()
},1000)
})
and Setup
is your normal routines
The code for handling the pseudo dropdown needs to go in the partial html file for that route, not in the main index file. That should fix the timing issue.
Remember that the router library dynamically loads the HTML, Scripts and Styles from the partial file when you first trigger that route - it doesn’t exist before then.
There ARE ways to get the routes pre-loaded but you mostly don’t want to bother with that.
I assume that you mean add it to index.js.
But I don't understand what you mean by;
Give me a few minutes....
I think I have fixed it all...
All will be revealed in a few minutes.
(im doing this, to get my head around UIB)
I found some UIRouter events, that are key
I've noticed that I don't have uibuilder.start()
anywhere in the index.js.
Could that be key?
Nah!
Im using an event of uibrouter:route-changed
to setup the various hooks you need
document.addEventListener("uibrouter:route-changed", function (event) {
switch (event.detail.newRouteId) {
case "charger": prepChargerEvents(); break;
// case "energy": prepEnergyEvents(); break;
// case "server": prepServerEvents(); break;
// etc etc
}
});
Seems to work well, almost done
Ok....
The Flow (Sorry, I added some test injects)
[{"id":"225a84aba29a2f46","type":"tab","label":"Flow 1","disabled":false,"info":"","env":[]},{"id":"4e89298095d3c94a","type":"group","z":"225a84aba29a2f46","name":"Test","style":{"label":true,"color":"#000000"},"nodes":["1e745a44e02acae9","48a6f2b65714e462","57c788c0464727d3","0888e682908588bf","183943637333a122","fd41a662cc0395f1","ccc4aa6a384fca3e","6f2df71294415bd1","94c788cd902197cd","fbb8e6cc6950047b","a6a8a9c197055da7","1a2cb50341f4446d","ce98ec87b72697f8","03ae810409001b2d","dd3f7168b4d3ef07","c0bcb4f92f0b39aa","ca5517d421bf6639","fc407ab71390b3ec","d48876ab93472d54","c7ae5bb8ee5235f9","0733ee8c52f72432"],"x":114,"y":19,"w":972,"h":422},{"id":"d48876ab93472d54","type":"junction","z":"225a84aba29a2f46","g":"4e89298095d3c94a","x":380,"y":100,"wires":[["c7ae5bb8ee5235f9"]]},{"id":"1e745a44e02acae9","type":"inject","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"","props":[],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":465,"y":360,"wires":[["57c788c0464727d3","c0bcb4f92f0b39aa","03ae810409001b2d"]],"l":false},{"id":"48a6f2b65714e462","type":"uib-save","z":"225a84aba29a2f46","g":"4e89298095d3c94a","url":"test","uibId":"183943637333a122","folder":"src","fname":"","createFolder":false,"reload":false,"usePageName":false,"encoding":"utf8","mode":438,"name":"","topic":"","x":1010,"y":360,"wires":[]},{"id":"57c788c0464727d3","type":"change","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"index.js","rules":[{"t":"set","p":"fname","pt":"msg","to":"index.js","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":620,"y":360,"wires":[["0888e682908588bf"]]},{"id":"0888e682908588bf","type":"template","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"","field":"payload","fieldType":"msg","format":"javascript","syntax":"mustache","template":"const routerConfig = {\n defaultRoute: \"energy\",\n hide: true,\n routes: [\n {\n id: \"energy\",\n src: \"./views/energy.html\",\n type: \"url\",\n title: \"Energy\",\n description: \"Home energy\",\n },\n {\n id: \"charger\",\n src: \"./views/charger.html\",\n type: \"url\",\n title: \"Charger\",\n description: \"EV Charger\",\n },\n {\n id: \"server\",\n src: \"./views/server.html\",\n type: \"url\",\n title: \"Server\",\n description: \"Server performance\",\n },\n ],\n routeMenus: [\n {\n id: \"menu1\",\n menuType: \"horizontal\",\n label: \"Main Menu\",\n },\n ],\n};\n\nconst router = new UibRouter(routerConfig);\ndocument.addEventListener(\"uibrouter:route-changed\", function (event) {\n switch (event.detail.newRouteId) {\n case \"charger\":\n prepChargerEvents();\n break;\n // case 'energy': prepEnergyEvents(); break;\n // case 'server': prepServerEvents(); break;\n }\n});\n\nfunction prepChargerEvents() {\n const dropdown = document.querySelector(\".dropdown\");\n const dropdownBtn = document.getElementById(\"dropdown-btn\");\n const dropdownContent = document.getElementById(\"dropdown-content\");\n\n document.addEventListener(\"click\", (e) => {\n if (!dropdown.contains(e.target)) {\n dropdown.classList.remove(\"show\");\n }\n });\n\n window.startCharge = () => {\n const label = document.querySelector(\"#start-charge-btn span\");\n uibuilder.send({\n topic: \"chargeButton\",\n payload: label.textContent === \"Start Charge\" ? true : false,\n });\n };\n\n window.toggleDropdown = () => {\n dropdown.classList.toggle(\"show\");\n };\n\n dropdownContent.querySelectorAll(\"a\").forEach((item) => {\n item.addEventListener(\"click\", (e) => {\n e.preventDefault();\n e.stopPropagation();\n\n const rawValue = item.getAttribute(\"data-value\");\n const value = isNaN(rawValue) ? rawValue : Number(rawValue);\n const label = item.textContent.trim();\n\n // Update button label\n dropdownBtn.textContent = `Rate: ${label}`;\n\n // Close dropdown\n dropdown.classList.remove(\"show\");\n\n // Send to Node-RED\n uibuilder.send({\n topic: \"chargingRate\",\n payload: value,\n });\n });\n });\n}\n\n// --- Setup: Centralised Message Handling for SPA ---\nuibuilder.onChange(\"msg\", handleUibMsg);\n\nfunction handleUibMsg(msg) {\n if (!msg || typeof msg.payload === \"undefined\") return;\n\n const { topic, payload } = msg;\n console.log(\"[uibuilder msg]\", msg);\n\n switch (topic) {\n case \"chargeButton\":\n updateChargeButtonUI(payload);\n break;\n\n case \"chargingRate\":\n updateChargingRateUI(payload);\n break;\n\n // Future routes:\n // case 'energyData': updateEnergyUI(payload); break\n // case 'serverStatus': updateServerUI(payload); break\n\n default:\n console.warn(`Unhandled topic: ${topic}`, msg);\n }\n}\n\n// --- CHARGER UI LOGIC ---\nfunction updateChargeButtonUI(state) {\n const indicator = document.getElementById(\"start-indicator\");\n const label = document.querySelector(\"#start-charge-btn span\");\n\n if (indicator) {\n indicator.style.backgroundColor =\n state === true || state === \"on\" ? \"red\" : \"green\";\n }\n\n if (label) {\n label.textContent =\n state === true || state === \"on\" ? \"Stop Charge\" : \"Start Charge\";\n }\n}\n\n// --- CHARGING RATE DROPDOWN ---\nfunction updateChargingRateUI(value) {\n const dropdownContent = document.getElementById(\"dropdown-content\");\n const dropdownBtn = document.getElementById(\"dropdown-btn\");\n\n if (!dropdownContent || !dropdownBtn) return;\n\n const match = dropdownContent.querySelector(`[data-value=\"${value}\"]`);\n const label = match?.textContent.trim() || value;\n\n dropdownBtn.textContent = `Rate: ${label}`;\n}","output":"str","x":800,"y":360,"wires":[["48a6f2b65714e462"]]},{"id":"fd41a662cc0395f1","type":"inject","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"Status","props":[{"p":"_uib","v":"{\"command\":\"showStatus\"}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":220,"y":260,"wires":[["183943637333a122"]]},{"id":"ccc4aa6a384fca3e","type":"inject","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"SET Client Log Level 5","props":[{"p":"_uib","v":"{\"command\":\"set\",\"prop\":\"logLevel\",\"value\":5}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":270,"y":300,"wires":[["183943637333a122"]]},{"id":"6f2df71294415bd1","type":"inject","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"SET Client Log Level 0","props":[{"p":"_uib","v":"{\"command\":\"set\",\"prop\":\"logLevel\",\"value\":0}","vt":"json"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":270,"y":340,"wires":[["183943637333a122"]]},{"id":"94c788cd902197cd","type":"switch","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"Filter connects","property":"uibuilderCtrl","propertyType":"msg","rules":[{"t":"eq","v":"client connect","vt":"str"},{"t":"else"}],"checkall":"true","repair":false,"outputs":2,"x":720,"y":260,"wires":[["fbb8e6cc6950047b"],[]]},{"id":"fbb8e6cc6950047b","type":"function","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"REPLAY","func":"return { \n \"uibuilderCtrl\": \"replay\", \n \"cacheControl\": \"REPLAY\", \n}","outputs":1,"timeout":"","noerr":0,"initialize":"","finalize":"","libs":[],"x":900,"y":260,"wires":[["a6a8a9c197055da7"]]},{"id":"a6a8a9c197055da7","type":"link out","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"link out 149","mode":"link","links":[],"x":1005,"y":260,"wires":[]},{"id":"1a2cb50341f4446d","type":"link in","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"server uibuilder node","links":[],"x":355,"y":380,"wires":[["183943637333a122"]]},{"id":"ce98ec87b72697f8","type":"template","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"","field":"payload","fieldType":"msg","format":"css","syntax":"mustache","template":"/* Load defaults from `<userDir>/node_modules/node-red-contrib-uibuilder/front-end/uib-brand.min.css`\n * This is optional but reasonably complete and allows for light/dark mode switching.\n */\n@import url(\"../uibuilder/uib-brand.min.css\");\n\n/* Simple horizontal navigation main menu (in the header) */\n.nav-main {\n background-color: var(--surface3);\n}\n\n.nav-main ul {\n /* Remove bullet points */\n list-style-type: none;\n /* Remove default padding */\n padding: 0;\n /* Remove default margin */\n margin: 0;\n /* Use Flexbox to align items horizontally */\n display: flex;\n}\n\n/* Add space between menu items */\n.nav-main li {\n margin-right: 1rem;\n}\n\n/* Remove margin on the last item */\n.nav-main li:last-child {\n margin-right: 0;\n}\n\n.nav-main a {\n /* Remove underline from links */\n text-decoration: none;\n /* Add padding for better click area */\n padding: var(--border-pad);\n /* Ensure the entire area is clickable */\n display: block;\n}\n\n/* Highlight on hover */\n.nav-main a:hover {\n background-color: var(--surface5);\n /* Optional: Add rounded corners */\n border-radius: var(--border-radius);\n}\n/* end of Navigation bar */\n\n/* Charger CSS */\n/* Standardize height */\n:root {\n --control-height: 3em;\n}\n\n/* Status button styling */\n.status-button {\n display: flex;\n align-items: center;\n background-color: var(--surface4);\n border: 1px solid var(--text3);\n border-radius: var(--border-radius);\n padding: 6px 10px;\n margin-right: 0.3em;\n font-family: var(--font-family);\n color: inherit;\n height: var(--control-height);\n width: fit-content;\n cursor: pointer;\n transition: box-shadow 0.4s, transform 0.2s;\n}\n\n.status-button:active {\n transform: scale(0.98);\n box-shadow: 0 0 5px rgba(0, 0, 0, 0.2);\n}\n\n.indicator {\n width: 0.5em;\n height: 2em;\n margin-right: 0.5em;\n border-radius: 0.5em;\n background-color: green;\n transition: background-color 0.3s;\n}\n\n/* Text box layout */\nbody>main {\n padding: var(--spacing-xs);\n}\n\n.wrap-row {\n display: flex;\n flex-wrap: wrap;\n gap: 0.2em;\n align-items: flex-start;\n justify-content: flex-start;\n}\n\n/* Dropdown styling */\n.dropdown {\n position: relative;\n display: inline-block;\n font-family: var(--font-family);\n}\n\n.dropbtn {\n background-color: var(--surface4);\n color: var(--text-color, inherit);\n padding: 10px 16px;\n font-size: var(--font-size-base, 1rem);\n border: 1px solid var(--text3);\n cursor: pointer;\n border-radius: var(--border-radius);\n height: var(--control-height);\n width: 10.5em;\n display: flex;\n align-items: center;\n transition: background-color 0.2s ease;\n}\n\n.dropbtn:hover {\n background-color: var(--surface5, #ddd);\n}\n\n.dropdown-content {\n display: none;\n position: absolute;\n background-color: var(--surface4);\n min-width: 10.5em;\n box-shadow: 0px 8px 16px rgba(0, 0, 0, 0.2);\n z-index: 1;\n border-radius: var(--radius-s, 0.3em);\n overflow: hidden;\n}\n\n.dropdown-content a {\n color: var(--text-color, inherit);\n padding: 10px 16px;\n text-decoration: none;\n display: block;\n}\n\n.dropdown-content a:hover {\n background-color: var(--surface5, #ddd);\n}\n\n.dropdown.show .dropdown-content {\n display: block;\n}\n\n/* charger Status text*/\n.status-text {\n color: var(--text-color, inherit);\n font-size: var(--font-size-base);\n font-family: var(--font-family);\n margin: 0.3em 0;\n /* Tight vertical spacing */\n line-height: 1.2em;\n /* Compact line spacing */\n}\n\n.status-block {\n margin-top: 0.5em;\n margin-bottom: 0.5em;\n}\n\n","output":"str","x":800,"y":400,"wires":[["48a6f2b65714e462"]]},{"id":"03ae810409001b2d","type":"change","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"index.css","rules":[{"t":"set","p":"fname","pt":"msg","to":"index.css","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":620,"y":400,"wires":[["ce98ec87b72697f8"]]},{"id":"dd3f7168b4d3ef07","type":"template","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"","field":"payload","fieldType":"msg","format":"handlebars","syntax":"mustache","template":"<!doctype html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <title>Home Panel</title>\n <link type=\"text/css\" rel=\"stylesheet\" href=\"./index.css\" media=\"all\" />\n <script defer src=\"../uibuilder/uibuilder.iife.min.js\"></script>\n <script defer src=\"../uibuilder/utils/uibrouter.iife.min.js\"></script>\n <script defer src=\"./index.js\"></script>\n </head>\n <body>\n <header>\n <nav class=\"nav-main\">\n <ul>\n <li>\n <a href=\"#energy\">Home Energy</a>\n </li>\n <li>\n <a href=\"#charger\">EV Charger</a>\n </li>\n <li>\n <a href=\"#server\">Server</a>\n </li>\n </ul>\n </nav>\n </header>\n <main id=\"view-container\">\n <!-- SPA view content will be loaded here -->\n </main>\n </body>\n</html>","output":"str","x":800,"y":320,"wires":[["48a6f2b65714e462"]]},{"id":"c0bcb4f92f0b39aa","type":"change","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"index.html","rules":[{"t":"set","p":"fname","pt":"msg","to":"index.html","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":630,"y":320,"wires":[["dd3f7168b4d3ef07"]]},{"id":"183943637333a122","type":"uibuilder","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"page1","topic":"mytopic","url":"test","okToGo":true,"fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"templateFolder":"blank","extTemplate":"","showfolder":false,"reload":false,"sourceFolder":"src","deployedVersion":"7.4.3","showMsgUib":false,"title":"","descr":"","editurl":"vscode://file/Users/marcusdavies/.node-red/uibuilder/test/?windowId=_blank","x":500,"y":260,"wires":[["0733ee8c52f72432"],["94c788cd902197cd"]]},{"id":"ca5517d421bf6639","type":"inject","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"Set Charge On","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"true","payloadType":"bool","x":240,"y":60,"wires":[["d48876ab93472d54"]]},{"id":"fc407ab71390b3ec","type":"inject","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"Set Charge Off","props":[{"p":"payload"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","payload":"false","payloadType":"bool","x":240,"y":100,"wires":[["d48876ab93472d54"]]},{"id":"c7ae5bb8ee5235f9","type":"change","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"","rules":[{"t":"set","p":"topic","pt":"msg","to":"chargeButton","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":240,"y":220,"wires":[["183943637333a122"]]},{"id":"0733ee8c52f72432","type":"switch","z":"225a84aba29a2f46","g":"4e89298095d3c94a","name":"","property":"payload","propertyType":"msg","rules":[{"t":"istype","v":"boolean","vt":"boolean"}],"checkall":"true","repair":false,"outputs":1,"x":690,"y":200,"wires":[["d48876ab93472d54"]]}]
And the only partial file I modified was charger.html
(to set the onclick
events)
<section>
<h1>EV Charger</h1>
<main>
<!-- Buttons & dropdown -->
<section class="wrap-row">
<div class="dropdown">
<button onclick="toggleDropdown()" class="dropbtn" id="dropdown-btn">Select charging rate</button>
<div id="dropdown-content" class="dropdown-content">
<a href="#" data-value="6.75">1.5kW (6A)</a>
<a href="#" data-value="13.5">3kW (12A)</a>
<a href="#" data-value="19.9">4.5kW (18A)</a>
<a href="#" data-value="26.7">6kW (24A)</a>
<a href="#" data-value="32">7.4kW (32A)</a>
<a href="#" data-value="Auto">Auto</a>
</div>
</div>
<button id="start-charge-btn" class="status-button" onClick="startCharge()">
<div class="indicator" id="start-indicator"></div>
<span>Start Charge</span>
</button>
</section>
<!-- Information text -->
<section class="status-block">
<p class="status-text">Charger Status: <span id="chargerstate"></span>
</p>
<p class="status-text">Grid status: <span id="gridpwr"></span>
</p>
</section>
</main>
</section>
This is what I have done.
when uibrouter:route-changed
is fired, I'm checking the route id that is loaded, and running a function based on that, to setup the button functions.
document.addEventListener("uibrouter:route-changed", function (event) {
switch (event.detail.newRouteId) {
case "charger":
prepChargerEvents();
break;
// case 'energy': prepEnergyEvents(); break;
// case 'server': prepServerEvents(); break;
}
});
function prepChargerEvents(){
...
}
I have looped back into UIB the button change, so it toggles in the UI
you dont have to use this of course, but it does now work
just re-inject the files (at minimum the js file)
it seems to all work, no matter if the charger page is first loaded or later via navigation
I have removed the need to listen for DOMContentLoaded
I didn't wire up all the UI to update the text or anything, but the foundations of connecting it all - is there I believe
Wow! Just tried it, and success!
I need to spend some time later, to fully appreciate the changes that you have made, but meanwhile it works, and it appears that you are quickly getting to grips with UIbuilder.
Thank you
PS AI has also sent you a thank you!
Anytime!
And thanks for the certificate
I think uibrouter:route-changed
was the magic ingredient!
was looking at the source code of the router, and found this gem, which seem to trigger, before the HTML is injected (I think). meaning, you can setup function dependencies before.
further more, it stops bloating the window object, when they are not needed, so also aids in End User browser performance, may be a - but still
Doesn’t putting the script into the html fragment do this anyway? Maybe I’ve misunderstood. But the reason that event hasn’t (yet) been documented is that I don’t believe it should be needed.
Don’t take this away from me Julian, this is my win, mine only - certificate is the proof
But yes, JS in the partials does work (I tested).
But having it in 1 file (and pulled in from index.html) feels cleaner? Instead of having Snippets of JS in various files?
Depends on the use case/preferred method entirely i suppose, and there isn’t one right answer.
Both will work.
The event I used here, is actually quite powerful, as it allows post setup, after a route change, really handy IMO (document it )
Apologies! I realised after I walked away from my PC that I should have said well done for finding it. It is a good call.
I understand. However, the purpose of the route files is to keep them together. You will see this in front-end frameworks as well.
If you want to go further, you can pull the route contents from an in-page <template>
element if you prefer. But except for really simple things, the separate file is generally best.
Anyway, you’ve spotted a gap in the documentation so that goes onto the backlog as well for fixing.
Yup, I always try to include useful events throughout. So that uibuilder is as useful for front-end coding as it is for Node-RED coding.
Based on our previous chat, I am going to spend some time and create a network chat app demo, using UIB only (hopefully this weekend, but have a 40th to attend today - )
Will PM you, once I have something to show off.
Nice, I look forward to it.