UIBUILDER Front-end routing example

Here is a complete flow that tests out the front-end router introduced in v6.7. It mostly uses front-end code but it uses the recently introduced uib-save node to provide a small flow that sets up all of the front-end files.

Instructions:

  1. Import the flow
  2. Set the uibuilder node url
  3. deploy
  4. Change the selection of uibuilder instance in the uib-save node - you might have to close and re-open to get the actual list - I've already noted that as a bug for a future fix.
  5. Re-deploy.
  6. Open the page and play with the routes.

Expect this to appear as a new example in the library in the next release.

[{"id":"858a25ec633e15aa","type":"tab","label":"router test","disabled":false,"info":"","env":[]},{"id":"f21209a191164954","type":"group","z":"858a25ec633e15aa","name":"Base setup","style":{"fill":"#ffffbf","fill-opacity":"0.12","label":true,"color":"#000000"},"nodes":["91d0333d46f8e7f4","980cfa1b7c1b3a5f","6cc5ccb9c91f801d","13bb356399138c24","59925a9e660a9e95","43e63c95d634a048","eb268d3e884b4b1c","09151d9dbab7fce8","e610deb0d3b69094"],"x":54,"y":119,"w":852,"h":162},{"id":"877f1fc905614aab","type":"group","z":"858a25ec633e15aa","name":"Front-end Code - Run after the base node has been deployed. Sets up FE code and routes. \\n ","style":{"label":true,"stroke":"#a4a4a4","fill-opacity":"0.33","color":"#000000","fill":"#ffffff"},"nodes":["8fe9cca36746933e","b90be49846f3f6ba","48f120153d22462f","95521b5e3f43c779","458a4019ee805475","0b263d2d25010d6d","8af6244dc82f99f2","631bd40ef94fd12c","d975894b5c20ed71","5d7e4ff16bfbb57f"],"x":54,"y":323,"w":852,"h":218},{"id":"e610deb0d3b69094","type":"junction","z":"858a25ec633e15aa","g":"f21209a191164954","x":440,"y":160,"wires":[["980cfa1b7c1b3a5f"]]},{"id":"91d0333d46f8e7f4","type":"debug","z":"858a25ec633e15aa","g":"f21209a191164954","name":"uib Std Output","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"","statusType":"counter","x":755,"y":160,"wires":[],"l":false},{"id":"980cfa1b7c1b3a5f","type":"uibuilder","z":"858a25ec633e15aa","g":"f21209a191164954","name":"","topic":"","url":"uib-router-eg","fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"templateFolder":"blank","extTemplate":"","showfolder":false,"reload":true,"sourceFolder":"src","deployedVersion":"6.6.0","showMsgUib":true,"title":"","descr":"","x":600,"y":200,"wires":[["91d0333d46f8e7f4"],["6cc5ccb9c91f801d","eb268d3e884b4b1c"]]},{"id":"6cc5ccb9c91f801d","type":"debug","z":"858a25ec633e15aa","g":"f21209a191164954","name":"uib Control Output","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"","statusType":"counter","x":845,"y":220,"wires":[],"l":false},{"id":"13bb356399138c24","type":"link in","z":"858a25ec633e15aa","g":"f21209a191164954","name":"uib-upd-egs - no cache","links":["226aef3812fcb00b"],"x":315,"y":160,"wires":[["e610deb0d3b69094"]]},{"id":"59925a9e660a9e95","type":"link in","z":"858a25ec633e15aa","g":"f21209a191164954","name":"uib-upd-egs - cached","links":["eb268d3e884b4b1c"],"x":225,"y":240,"wires":[["43e63c95d634a048"]]},{"id":"43e63c95d634a048","type":"uib-cache","z":"858a25ec633e15aa","g":"f21209a191164954","cacheall":false,"cacheKey":"topic","newcache":true,"num":1,"storeName":"default","name":"Cache (by topic)","storeContext":"context","varName":"uib_cache","x":360,"y":200,"wires":[["980cfa1b7c1b3a5f"]]},{"id":"eb268d3e884b4b1c","type":"link out","z":"858a25ec633e15aa","g":"f21209a191164954","name":"link out 68","mode":"link","links":["59925a9e660a9e95"],"x":755,"y":240,"wires":[]},{"id":"09151d9dbab7fce8","type":"inject","z":"858a25ec633e15aa","g":"f21209a191164954","name":"Clear Cache","props":[{"p":"uibuilderCtrl","v":"clear cache","vt":"str"},{"p":"cacheControl","v":"CLEAR","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":170,"y":200,"wires":[["43e63c95d634a048"]]},{"id":"8fe9cca36746933e","type":"inject","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"setup all FE files","x":115,"y":380,"wires":[["8af6244dc82f99f2","631bd40ef94fd12c","d975894b5c20ed71","5d7e4ff16bfbb57f"]],"l":false},{"id":"b90be49846f3f6ba","type":"template","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"index.html","field":"payload","fieldType":"msg","format":"html","syntax":"plain","template":"<!doctype html>\n<html lang=\"en\">\n\n<head>\n\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link rel=\"icon\" href=\"../uibuilder/images/node-blue.ico\">\n\n    <title>FE Router - Node-RED uibuilder</title>\n    <meta name=\"description\" content=\"Node-RED uibuilder - FE Router\">\n\n    <!-- Your own CSS (defaults to loading uibuilders css)-->\n    <link type=\"text/css\" rel=\"stylesheet\" href=\"./index.css\" media=\"all\">\n\n    <!-- #region Supporting Scripts. These MUST be in the right order. Note no leading / -->\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\">\n        /* OPTIONAL: Put your custom code in that */\n    </script>\n    <!-- #endregion -->\n\n    <template id=\"route01\">\n        <h2>This comes from an internal <code class=\"r01style\">&lt;template></code> tag</h2>\n        <div>\n            Route 1\n        </div>\n        <script>\n            console.log('I was produced by a script in Route 1')\n        </script>\n        <style>\n            .r01style {\n                background-color: yellow;\n                color: blue;\n                font-weight: 900;\n            }\n        </style>\n    </template>\n    <template id=\"route02\">\n        <h2>This also comes from an internal <code>&lt;template></code> tag</h2>\n        <div class=\"extraclass\">\n            Route 2\n        </div>\n    </template>\n    <template id=\"route06\">\n        <h2>This also comes from an internal <code>&lt;template></code> tag</h2>\n        <div class=\"extraclass\">\n            Route 6\n        </div>\n    </template>\n\n</head>\n\n<body class=\"uib\">\n    <script>\n        console.log('a script at the start of body')\n    </script>\n\n    <h1 class=\"with-subtitle\">An example of a framework-less front-end router</h1>\n    <div role=\"doc-subtitle\">Using the UIBUILDER IIFE library.</div>\n\n    <div id=\"more\">\n        <!-- '#more' is used as a parent for dynamic HTML content in examples -->\n    </div>\n\n    <ul id=\"routemenu\">Route Menu\n        <!-- <li><a href=\"#route01\" onclick=\"router.doRoute(event)\">#1</a></li> -->\n        <li><a href=\"#route01\">#1 (Internal template)</a></li>\n        <li><a href=\"#route02\">#2 (Internal template)</a></li>\n        <li><a href=\"#route03?doh=rei\">#3 (External template)</a></li>\n        <li><a href=\"#route04\">#4 (fails as the external route template doesn't exist)</a></li>\n        <li><a href=\"#route05\">#5 (External template)</a></li>\n        <li><a href=\"#route06\">#6 (Internal template)</a></li>\n    </ul>\n    <div href=\"#route01\" onclick=\"router.doRoute(event)\">#1 via event handler</div>\n\n</body>\n\n</html>","output":"str","x":550,"y":380,"wires":[["48f120153d22462f"]]},{"id":"48f120153d22462f","type":"uib-save","z":"858a25ec633e15aa","g":"877f1fc905614aab","url":"uib-router-eg","uibId":"980cfa1b7c1b3a5f","folder":"src","fname":"","createFolder":true,"reload":true,"usePageName":false,"encoding":"utf8","mode":438,"name":"","topic":"","x":810,"y":380,"wires":[]},{"id":"95521b5e3f43c779","type":"template","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"index.js","field":"payload","fieldType":"msg","format":"javascript","syntax":"plain","template":"/*globals UibRouter, uibuilder */\n\nconst routerConfig = {\n    // Router templates created inside the routeContainer, specify an CSS selector\n    // If not provided, default div with ID uibroutecontainer is added as the last element of the body\n    routeContainer: '#routecontainer',\n\n    // Optionally, chose a default route id to be displayed on load\n    defaultRoute: 'route03',\n\n    hide: true,\n    // unload: true,\n\n    // Define the possible routes type=url for externals\n    // Can be an object or an array but each entry must be an object containing {id,src,type}\n    //   type can be anything but only `url` will be treated as an external template file.\n    //   src is either a CSS selector for a <template> or a URL of an HTML file.\n    //   id must match the href=\"#routeid\" in any menu/link. and `<template id=\"routeid\">` on any loaded template\n    //      must be unique on the page\n    routes: [\n        {id: 'route01', src: '#route01'},\n        {id: 'route02', src: '#route02'},\n        {id: 'route03', src: './fe-routes/route03.html', type: 'url'},\n        // Doesn't exist. Tests load error\n        {id: 'route04', src: './fe-routes/dummy.html', type: 'url'},\n    ],\n}\n// @ts-ignore\nconst router = new UibRouter(routerConfig)\n\n// Example of dynamically adding additional routes\nconst extraRoutes = [\n    { id: 'route05', src: './fe-routes/route05.html', type: 'url' },\n    { id: 'route06', src: '#route06' },\n]\nrouter.addRoutes(extraRoutes)\n\n// @ts-ignore - Optionally send a msg back to Node-RED when the route changes\nuibuilder.watchUrlHash()\n\n// Example of changing route from code (after 5 seconds):\nsetTimeout(() => {\n    router.doRoute('route01')\n}, 5000)\n\n/** If you need to be certain that all external route templates\n *  have loaded before doing something, this is how.\n */\n//\n// uibuilder.onChange('uibrouter', uibrouter => {\n//     if (uibrouter === 'loaded') {\n//         // Do stuff\n//     }\n// })\n/** Alternatively, use this custom event handler (no dependency on uibuilder) */\n// document.addEventListener('uibrouter:loaded', () => {\n//     console.log('The router is fully loaded - including all external templates')\n//     // Do stuff\n// })\n","output":"str","x":560,"y":420,"wires":[["48f120153d22462f"]]},{"id":"458a4019ee805475","type":"template","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"route03.html","field":"payload","fieldType":"msg","format":"html","syntax":"plain","template":"<style>\n    /* Note that these only exist when this route is showing */\n    .extraclass {\n        color: green;\n        border: 2px solid var(--warning);\n        padding: 1em;\n    }\n    .coolclass {\n        background-color: var(--surface4);\n        border: 2px solid var(--info-intense);\n        padding: 1em;\n        margin: .5em 0;\n    }\n</style>\n<script>\n    // ! WARNING: This will be re-run EVERY time the route is shown - use with caution.\n    // Here we use a simple global variable to ensure we only ever run once.\n    // Also note that deleting a route that has previously displayed will not \n    //   remove any global values set here including even handlers.\n    \n    console.log('route03 external: built-in script running ...')\n\n    if (!window['mysensor']) window['mysensor'] = {temperature: 'N/A', humdity: 'N/A' }\n    else {\n        let e =  $('#mytemp') // make sure the element exists\n        if (e) e.innerText = window['mysensor'].temperature // update if it exists\n        e =  $('#myhumid') // make sure the element exists\n        if (e) e.innerText = window['mysensor'].humidity // update if it exists\n    }\n\n    if (!window['route3Run']) { // make sure this is unique across routes\n\n        // A function to use in <a href=\"#routeid\" onclick=\"doClick1(event)\">\n        // function doClick1(event) {\n        //     console.log('You did a click1 (onclick=\"router.doRoute(event)\")', event)\n        // }\n\n        // Alternative to using onclick is to add the handler via code\n        // const r03d02 = $('#r03d02')\n        // if (r03d02) r03d02.addEventListener('click', (event) => {\n        // $('#r03d02').addEventListener('click', (event) => {\n        //     console.log('You did a different click (addEventListener)', event)\n        // })\n        // or even:\n        // r03d02.addEventListener('click', doClick1)\n\n        // Example of updating the UI direct from a Node-RED message\n        uibuilder.onChange('msg', (msg) => {\n            console.log('msg from Node-RED handled in a route')\n            if(msg.topic === 'mysensor') {\n                let e =  $('#mytemp') // make sure the element exists\n                if (e) e.innerText = msg.payload.temperature // update if it exists\n                window['mysensor'].temperature = msg.payload.temperature\n                e =  $('#myhumid') // make sure the element exists\n                if (e) e.innerText = msg.payload.humidity // update if it exists\n                window['mysensor'].humidity = msg.payload.humidity\n            }\n        })\n        // You could instead use the `uib-update` node to avoid this code.\n    }\n    // Make sure we only run the above once.\n    route3Run = true\n</script>\n\n<h2>This comes from an external file</h2>\n<div  id=\"r03d01\" class=\"extraclass\">\n    Route 3 part 1\n</div>\n<div id=\"r03d02\" class=\"extraclass\">\n    Route 3 part 2\n</div>\n<div class=\"coolclass\">\n    Temperature: <span id=\"mytemp\">...</span>℃<br>\n    Humidity: <span id=\"myhumid\">...</span>%\n</div>\n","output":"str","x":550,"y":460,"wires":[["48f120153d22462f"]]},{"id":"0b263d2d25010d6d","type":"template","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"route05.html","field":"payload","fieldType":"msg","format":"html","syntax":"plain","template":"<h2>This comes from an external file - <code>./fe-routes/route05.html</code></h2>\n<div  id=\"r04d01\" class=\"extraclass\">\n    Route 5 part 1\n</div>\n<div id=\"r04d02\" class=\"extraclass\">\n    Route 5 part 2\n</div>\n","output":"str","x":550,"y":500,"wires":[["48f120153d22462f"]]},{"id":"8af6244dc82f99f2","type":"change","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"index.html","rules":[{"t":"set","p":"fname","pt":"msg","to":"index.html","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":310,"y":380,"wires":[["b90be49846f3f6ba"]]},{"id":"631bd40ef94fd12c","type":"change","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"index.js","rules":[{"t":"set","p":"fname","pt":"msg","to":"index.js","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":300,"y":420,"wires":[["95521b5e3f43c779"]]},{"id":"d975894b5c20ed71","type":"change","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"fe-routes/route03.html","rules":[{"t":"set","p":"fname","pt":"msg","to":"fe-routes/route03.html","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":460,"wires":[["458a4019ee805475"]]},{"id":"5d7e4ff16bfbb57f","type":"change","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"fe-routes/route05.html","rules":[{"t":"set","p":"fname","pt":"msg","to":"fe-routes/route05.html","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":500,"wires":[["0b263d2d25010d6d"]]}]
1 Like

Thank you, Julian, for another great feature :slight_smile:
I'm trying to use that for my current project, but I think I need some help.
Before, I "changed sites" by a button with attributes like

id="ButtonToHome" onclick="uibuilder.eventSend(event)" data-type="eventSend"

At uibuilders first output I had a switch "_ui.id" == "ButtonToHome" and with the following flow I were able to replace the content of the website.
I now try to do the same with the new routing, but as some content is created in node-red and send to the client, I use "watchUrlHash" and a switch "newHash"=="#route03". That works fine at changing a route, but as soon as someone reloads the website or use the direct link like "https://example.com/uib-router-eg/#route03, the content of the flow won't be shown.

Here is your example, extended with an uib-element to show what I'm trying to explain

[{"id":"f21209a191164954","type":"group","z":"858a25ec633e15aa","name":"Base setup","style":{"fill":"#ffffbf","fill-opacity":"0.12","label":true,"color":"#000000"},"nodes":["91d0333d46f8e7f4","980cfa1b7c1b3a5f","6cc5ccb9c91f801d","13bb356399138c24","59925a9e660a9e95","43e63c95d634a048","eb268d3e884b4b1c","09151d9dbab7fce8","e610deb0d3b69094","3fa32aa6f8051b48","096bf0d77f8c66a8","b07b9a8f7c91b978"],"x":54,"y":119,"w":1292,"h":162},{"id":"91d0333d46f8e7f4","type":"debug","z":"858a25ec633e15aa","g":"f21209a191164954","name":"uib Std Output","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"","statusType":"counter","x":755,"y":160,"wires":[],"l":false},{"id":"980cfa1b7c1b3a5f","type":"uibuilder","z":"858a25ec633e15aa","g":"f21209a191164954","name":"","topic":"","url":"uib-router-eg","fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"templateFolder":"blank","extTemplate":"","showfolder":false,"reload":true,"sourceFolder":"src","deployedVersion":"6.6.0","showMsgUib":true,"title":"","descr":"","x":600,"y":200,"wires":[["91d0333d46f8e7f4","3fa32aa6f8051b48"],["6cc5ccb9c91f801d","eb268d3e884b4b1c"]]},{"id":"6cc5ccb9c91f801d","type":"debug","z":"858a25ec633e15aa","g":"f21209a191164954","name":"uib Control Output","active":true,"tosidebar":true,"console":false,"tostatus":true,"complete":"true","targetType":"full","statusVal":"","statusType":"counter","x":845,"y":220,"wires":[],"l":false},{"id":"13bb356399138c24","type":"link in","z":"858a25ec633e15aa","g":"f21209a191164954","name":"uib-upd-egs - no cache","links":["226aef3812fcb00b","b07b9a8f7c91b978"],"x":315,"y":160,"wires":[["e610deb0d3b69094"]]},{"id":"59925a9e660a9e95","type":"link in","z":"858a25ec633e15aa","g":"f21209a191164954","name":"uib-upd-egs - cached","links":["eb268d3e884b4b1c"],"x":225,"y":240,"wires":[["43e63c95d634a048"]]},{"id":"43e63c95d634a048","type":"uib-cache","z":"858a25ec633e15aa","g":"f21209a191164954","cacheall":false,"cacheKey":"topic","newcache":true,"num":1,"storeName":"default","name":"Cache (by topic)","storeContext":"context","varName":"uib_cache","x":360,"y":200,"wires":[["980cfa1b7c1b3a5f"]]},{"id":"eb268d3e884b4b1c","type":"link out","z":"858a25ec633e15aa","g":"f21209a191164954","name":"link out 68","mode":"link","links":["59925a9e660a9e95"],"x":755,"y":240,"wires":[]},{"id":"09151d9dbab7fce8","type":"inject","z":"858a25ec633e15aa","g":"f21209a191164954","name":"Clear Cache","props":[{"p":"uibuilderCtrl","v":"clear cache","vt":"str"},{"p":"cacheControl","v":"CLEAR","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"","x":170,"y":200,"wires":[["43e63c95d634a048"]]},{"id":"e610deb0d3b69094","type":"junction","z":"858a25ec633e15aa","g":"f21209a191164954","x":440,"y":160,"wires":[["980cfa1b7c1b3a5f"]]},{"id":"3fa32aa6f8051b48","type":"switch","z":"858a25ec633e15aa","g":"f21209a191164954","name":"newHash=#route03","property":"newHash","propertyType":"msg","rules":[{"t":"eq","v":"#route03","vt":"str"}],"checkall":"true","repair":false,"outputs":1,"x":930,"y":180,"wires":[["096bf0d77f8c66a8"]]},{"id":"096bf0d77f8c66a8","type":"uib-element","z":"858a25ec633e15aa","g":"f21209a191164954","name":"","topic":"","elementtype":"article","parent":"#r03d02","parentSource":"","parentSourceType":"str","elementid":"r03d03","elementId":"","elementIdSourceType":"str","heading":"","headingSourceType":"str","headingLevel":"h2","data":"\"Your IP is: \" & _uib.ip","dataSourceType":"jsonata","position":"last","positionSourceType":"str","passthrough":false,"confData":{},"x":1150,"y":180,"wires":[["b07b9a8f7c91b978"]]},{"id":"b07b9a8f7c91b978","type":"link out","z":"858a25ec633e15aa","g":"f21209a191164954","name":"link out 120","mode":"link","links":["13bb356399138c24"],"x":1305,"y":180,"wires":[]},{"id":"877f1fc905614aab","type":"group","z":"858a25ec633e15aa","name":"Front-end Code - Run after the base node has been deployed. Sets up FE code and routes. \\n ","style":{"label":true,"stroke":"#a4a4a4","fill-opacity":"0.33","color":"#000000","fill":"#ffffff"},"nodes":["8fe9cca36746933e","b90be49846f3f6ba","48f120153d22462f","95521b5e3f43c779","458a4019ee805475","0b263d2d25010d6d","8af6244dc82f99f2","631bd40ef94fd12c","d975894b5c20ed71","5d7e4ff16bfbb57f"],"x":54,"y":323,"w":852,"h":218},{"id":"8fe9cca36746933e","type":"inject","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"","props":[{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"setup all FE files","x":115,"y":380,"wires":[["8af6244dc82f99f2","631bd40ef94fd12c","d975894b5c20ed71","5d7e4ff16bfbb57f"]],"l":false},{"id":"b90be49846f3f6ba","type":"template","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"index.html","field":"payload","fieldType":"msg","format":"html","syntax":"plain","template":"<!doctype html>\n<html lang=\"en\">\n\n<head>\n\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">\n    <link rel=\"icon\" href=\"../uibuilder/images/node-blue.ico\">\n\n    <title>FE Router - Node-RED uibuilder</title>\n    <meta name=\"description\" content=\"Node-RED uibuilder - FE Router\">\n\n    <!-- Your own CSS (defaults to loading uibuilders css)-->\n    <link type=\"text/css\" rel=\"stylesheet\" href=\"./index.css\" media=\"all\">\n\n    <!-- #region Supporting Scripts. These MUST be in the right order. Note no leading / -->\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\">\n        /* OPTIONAL: Put your custom code in that */\n    </script>\n    <!-- #endregion -->\n\n    <template id=\"route01\">\n        <h2>This comes from an internal <code class=\"r01style\">&lt;template></code> tag</h2>\n        <div>\n            Route 1\n        </div>\n        <script>\n            console.log('I was produced by a script in Route 1')\n        </script>\n        <style>\n            .r01style {\n                background-color: yellow;\n                color: blue;\n                font-weight: 900;\n            }\n        </style>\n    </template>\n    <template id=\"route02\">\n        <h2>This also comes from an internal <code>&lt;template></code> tag</h2>\n        <div class=\"extraclass\">\n            Route 2\n        </div>\n    </template>\n    <template id=\"route06\">\n        <h2>This also comes from an internal <code>&lt;template></code> tag</h2>\n        <div class=\"extraclass\">\n            Route 6\n        </div>\n    </template>\n\n</head>\n\n<body class=\"uib\">\n    <script>\n        console.log('a script at the start of body')\n    </script>\n\n    <h1 class=\"with-subtitle\">An example of a framework-less front-end router</h1>\n    <div role=\"doc-subtitle\">Using the UIBUILDER IIFE library.</div>\n\n    <div id=\"more\">\n        <!-- '#more' is used as a parent for dynamic HTML content in examples -->\n    </div>\n\n    <ul id=\"routemenu\">Route Menu\n        <!-- <li><a href=\"#route01\" onclick=\"router.doRoute(event)\">#1</a></li> -->\n        <li><a href=\"#route01\">#1 (Internal template)</a></li>\n        <li><a href=\"#route02\">#2 (Internal template)</a></li>\n        <li><a href=\"#route03?doh=rei\">#3 (External template)</a></li>\n        <li><a href=\"#route04\">#4 (fails as the external route template doesn't exist)</a></li>\n        <li><a href=\"#route05\">#5 (External template)</a></li>\n        <li><a href=\"#route06\">#6 (Internal template)</a></li>\n    </ul>\n    <div href=\"#route01\" onclick=\"router.doRoute(event)\">#1 via event handler</div>\n\n</body>\n\n</html>","output":"str","x":550,"y":380,"wires":[["48f120153d22462f"]]},{"id":"48f120153d22462f","type":"uib-save","z":"858a25ec633e15aa","g":"877f1fc905614aab","url":"uib-router-eg","uibId":"980cfa1b7c1b3a5f","folder":"src","fname":"","createFolder":true,"reload":true,"usePageName":false,"encoding":"utf8","mode":438,"name":"","topic":"","x":810,"y":380,"wires":[]},{"id":"95521b5e3f43c779","type":"template","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"index.js","field":"payload","fieldType":"msg","format":"javascript","syntax":"plain","template":"/*globals UibRouter, uibuilder */\n\nconst routerConfig = {\n    // Router templates created inside the routeContainer, specify an CSS selector\n    // If not provided, default div with ID uibroutecontainer is added as the last element of the body\n    routeContainer: '#routecontainer',\n\n    // Optionally, chose a default route id to be displayed on load\n    defaultRoute: 'route03',\n\n    hide: true,\n    // unload: true,\n\n    // Define the possible routes type=url for externals\n    // Can be an object or an array but each entry must be an object containing {id,src,type}\n    //   type can be anything but only `url` will be treated as an external template file.\n    //   src is either a CSS selector for a <template> or a URL of an HTML file.\n    //   id must match the href=\"#routeid\" in any menu/link. and `<template id=\"routeid\">` on any loaded template\n    //      must be unique on the page\n    routes: [\n        {id: 'route01', src: '#route01'},\n        {id: 'route02', src: '#route02'},\n        {id: 'route03', src: './fe-routes/route03.html', type: 'url'},\n        // Doesn't exist. Tests load error\n        {id: 'route04', src: './fe-routes/dummy.html', type: 'url'},\n    ],\n}\n// @ts-ignore\nconst router = new UibRouter(routerConfig)\n\n// Example of dynamically adding additional routes\nconst extraRoutes = [\n    { id: 'route05', src: './fe-routes/route05.html', type: 'url' },\n    { id: 'route06', src: '#route06' },\n]\nrouter.addRoutes(extraRoutes)\n\n// @ts-ignore - Optionally send a msg back to Node-RED when the route changes\nuibuilder.watchUrlHash()\n\n// Example of changing route from code (after 5 seconds):\nsetTimeout(() => {\n    router.doRoute('route01')\n}, 5000)\n\n/** If you need to be certain that all external route templates\n *  have loaded before doing something, this is how.\n */\n//\n// uibuilder.onChange('uibrouter', uibrouter => {\n//     if (uibrouter === 'loaded') {\n//         // Do stuff\n//     }\n// })\n/** Alternatively, use this custom event handler (no dependency on uibuilder) */\n// document.addEventListener('uibrouter:loaded', () => {\n//     console.log('The router is fully loaded - including all external templates')\n//     // Do stuff\n// })\n","output":"str","x":560,"y":420,"wires":[["48f120153d22462f"]]},{"id":"458a4019ee805475","type":"template","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"route03.html","field":"payload","fieldType":"msg","format":"html","syntax":"plain","template":"<style>\n    /* Note that these only exist when this route is showing */\n    .extraclass {\n        color: green;\n        border: 2px solid var(--warning);\n        padding: 1em;\n    }\n    .coolclass {\n        background-color: var(--surface4);\n        border: 2px solid var(--info-intense);\n        padding: 1em;\n        margin: .5em 0;\n    }\n</style>\n<script>\n    // ! WARNING: This will be re-run EVERY time the route is shown - use with caution.\n    // Here we use a simple global variable to ensure we only ever run once.\n    // Also note that deleting a route that has previously displayed will not \n    //   remove any global values set here including even handlers.\n    \n    console.log('route03 external: built-in script running ...')\n\n    if (!window['mysensor']) window['mysensor'] = {temperature: 'N/A', humdity: 'N/A' }\n    else {\n        let e =  $('#mytemp') // make sure the element exists\n        if (e) e.innerText = window['mysensor'].temperature // update if it exists\n        e =  $('#myhumid') // make sure the element exists\n        if (e) e.innerText = window['mysensor'].humidity // update if it exists\n    }\n\n    if (!window['route3Run']) { // make sure this is unique across routes\n\n        // A function to use in <a href=\"#routeid\" onclick=\"doClick1(event)\">\n        // function doClick1(event) {\n        //     console.log('You did a click1 (onclick=\"router.doRoute(event)\")', event)\n        // }\n\n        // Alternative to using onclick is to add the handler via code\n        // const r03d02 = $('#r03d02')\n        // if (r03d02) r03d02.addEventListener('click', (event) => {\n        // $('#r03d02').addEventListener('click', (event) => {\n        //     console.log('You did a different click (addEventListener)', event)\n        // })\n        // or even:\n        // r03d02.addEventListener('click', doClick1)\n\n        // Example of updating the UI direct from a Node-RED message\n        uibuilder.onChange('msg', (msg) => {\n            console.log('msg from Node-RED handled in a route')\n            if(msg.topic === 'mysensor') {\n                let e =  $('#mytemp') // make sure the element exists\n                if (e) e.innerText = msg.payload.temperature // update if it exists\n                window['mysensor'].temperature = msg.payload.temperature\n                e =  $('#myhumid') // make sure the element exists\n                if (e) e.innerText = msg.payload.humidity // update if it exists\n                window['mysensor'].humidity = msg.payload.humidity\n            }\n        })\n        // You could instead use the `uib-update` node to avoid this code.\n    }\n    // Make sure we only run the above once.\n    route3Run = true\n</script>\n\n<h2>This comes from an external file</h2>\n<div  id=\"r03d01\" class=\"extraclass\">\n    Route 3 part 1\n</div>\n<div id=\"r03d02\" class=\"extraclass\">\n    Route 3 part 2\n</div>\n<div class=\"coolclass\">\n    Temperature: <span id=\"mytemp\">...</span>℃<br>\n    Humidity: <span id=\"myhumid\">...</span>%\n</div>\n","output":"str","x":550,"y":460,"wires":[["48f120153d22462f"]]},{"id":"0b263d2d25010d6d","type":"template","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"route05.html","field":"payload","fieldType":"msg","format":"html","syntax":"plain","template":"<h2>This comes from an external file - <code>./fe-routes/route05.html</code></h2>\n<div  id=\"r04d01\" class=\"extraclass\">\n    Route 5 part 1\n</div>\n<div id=\"r04d02\" class=\"extraclass\">\n    Route 5 part 2\n</div>\n","output":"str","x":550,"y":500,"wires":[["48f120153d22462f"]]},{"id":"8af6244dc82f99f2","type":"change","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"index.html","rules":[{"t":"set","p":"fname","pt":"msg","to":"index.html","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":310,"y":380,"wires":[["b90be49846f3f6ba"]]},{"id":"631bd40ef94fd12c","type":"change","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"index.js","rules":[{"t":"set","p":"fname","pt":"msg","to":"index.js","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":300,"y":420,"wires":[["95521b5e3f43c779"]]},{"id":"d975894b5c20ed71","type":"change","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"fe-routes/route03.html","rules":[{"t":"set","p":"fname","pt":"msg","to":"fe-routes/route03.html","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":460,"wires":[["458a4019ee805475"]]},{"id":"5d7e4ff16bfbb57f","type":"change","z":"858a25ec633e15aa","g":"877f1fc905614aab","name":"fe-routes/route05.html","rules":[{"t":"set","p":"fname","pt":"msg","to":"fe-routes/route05.html","tot":"str"}],"action":"","property":"","from":"","to":"","reg":false,"x":340,"y":500,"wires":[["0b263d2d25010d6d"]]}]

Do you have a recommendation on how to solve that?

Hi, you've spotted what I did as well. I've been creating a new home automation site, combining a number of separate pages I had before and some new stuff.

As you can see, I use the uib-cache node - but of course, the router lazy-loads. Which means that, on-load, not all routes are loaded and so the cache is mostly wasted. This is the issue you have found I think?

In the next release, there will be an option to pre-load all routes which will avoid the issue. I will also hopefully build-in a feature to add a route id into outputs (you will see in the image above that I've added them manually at the moment) along with a matching filter option in the client (not sure if that will make it into v6.8).

I'm also thinking about some other ways of approaching the issue.

As a short term work-around, you probably want to use the built-in uibuilder.watchUrlHash feature to get the front-end client library to report back to node-red when the route changes. Then use that to send out the data for the route.

I've spotted a few other short-comings of the first release of the router and I'm aiming to fix those in the next release as well. Such as setting some uibuilder managed variables to allow the uib-var component to show things like optional titles.

There is also a to-do list:

  • For convenience, add current route details and curr/prev route id to data when route changes.
  • Add 1st show marker to route change to allow client to ask for cache update
  • Update docs to allow for description and other props on route array.
  • if no routerConfig.defaultRoute set, use the first (zeroth) defined route.
  • Add preload option so that updates to ui can happen before display.
  • Document standards for navigation menus.
  • Add optional nav menu list css selector (css class) allowing auto-mark of current page & add aria-current="page".

image

Pls keep it as an option (and not default), as I am relying on the fact that the routes are NOT loaded...
I don't want the routes all loaded at startup. It will delay the startup and uselessly load NR.

Absolutely - I wouldn't do anything else. Both options are useful for different use-cases. As I say, I'm also looking at some alternative options as well. I suspect that the uib-cache will get extended to handle initial route requests as well as all route requests (for those times when you want to unload the route after use).

Also, the router will continue to be usable stand-alone. Not just with uibuilder.

Thank you for the quick answer.
I'm not (yet) using the uib-cache node, so I'm not sure if it is the same issue.

That is what I'm already doing, I tried to describe it with

...I use "watchUrlHash" and a switch "newHash"=="#route03".... <

A reload of the page (pressing F5) or a new connection using a direct link like "https://example.com/uib-router-eg/#route03" did not trigger the uibuilder.watchUrlHash and therefore the textbox wouldn't load.

For me, that is solved in v6.8.1 as both cases now route to the default route which is in my case "Home" where no dynamic content needs to be loaded.
Is that how it should be? I'd expect to stay on the same route at a reload.

No, that is wrong and will be fixed in the next release unless you want me to push out a point release.

So I think that leaves us with a gap still.

I think that I need to add something so that it better reports the load and change more consistently. The watchUrlHash function was really only part of the solution anyway - it was largely there for people not using uibrouter but some other router for Vue/REACT, etc.

I think that the router library probably needs to send uibuilder control messages on route change - this would be more in line with uibuilder's page load anyway. I know that I also need to change the uib-cache node to enable it to be optionally aware of route/page/client ids.


However, for your current issue as a work-around, I think that all you need to do is replace the watchUrlHash with some front-end javascript. The following will report on route changes - including the initial route on page load:

// note that, unlike uibuilder.watchUrlHash(), these also report the initial load route as well.
/** 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()
    // To manually notify Node-RED: uibuilder.send({topic: 'route-change', payload: {newRoute: routeId, oldRoute: router.previousRouteId}})
    uibuilder.send({topic: 'route-change', payload: { newRoute: routeId, oldRoute: router.previousRouteId }})
})
/** Monitor route changes in code and get the new route config */
uibuilder.onChange('uibrouter_CurrentDetails', (routeConfig) => {
    console.log('ROUTE CHANGED. New Route Details: ', routeConfig)
    // To manually notify Node-RED: uibuilder.send(topic: 'route-change-details', payload: routeConfig)
    uibuilder.send({ topic: 'route-change', payload: routeConfig })
})

The two different examples give different data but of course, they can be combined to get just the info you need. It doesn't really matter which of the changes you monitor.

I can show you how to trigger a cache replay from the uib-cache node as well if you need that - though currently the whole cache is sent - that shouldn't matter as long as your cache isn't absolutely enormous.

Thank you very much, that work-around is indeed what I was missing.
No need for a point release, there's enough to do in the meantime :smiley:

I receive the data from several devices only on request (from a database or by xmpp request ) and they should be as current as possible, so I think the uib-cache node is not needed at the moment. But I played already with your example flows and I'm sure there will be a usecase at another project :slight_smile:
Uibuilder and of course node-red itself are awesome tools for people like me who are not really able to write code but still want to realise an webapp idea, Thank you so much for that.

1 Like

:+1:

Unfortunately, some other bugs have cropped up and I'm going to have to do one tomorrow anyway. So the fix is included. :slight_smile: Watch out for v6.8.2 tomorrow, I'll update the release note in the forum when I've done it.

Its a pleasure to be sure.

I've lots more ideas even for the router.

Next release (v6.9) will have the ability to lazy-load the route templates as well as automatically unloading them. (And manually).

This will facilitate some interesting use-cases such as a building-wide collection of Kiosks all controlled from a single Node-RED instance. Where Node-RED determines what routes are shown, in what order and for how long.

It should also make it possible to have thousands (or more!) external templates such as for a WIKI or other Knowledge Base type use-case. Coupled with uibuilder's capability of displaying Markdown, that could be really useful.

I'm also thinking about ways to have >1 route display area on-page so that even your menu's could be routes. That would be yet another path to automating different menus and keeping the core HTML small.

Of course, with the uib-save node, it is now very easy to get Node-RED to update route template files. So menu automation could be done via a simple flow.

2 Likes