Dynamic UI creation?

Is there, within one of the various UI frameworks for NR, that would allow dynamic UI creation? Here is the use case, I have a JSON, that changes from time to time, I want to create a button for each top level property in the JSON.

The JSON is rather simplistic...

[
    {
        "Name": "One",
        "Key": "1"
    },
    {
        "Name": "Two",
        "Key": "2"
    }
]

Would love to create a follow on when the JSON is loaded... creates two buttons (per the number in the JSON file), where the button label set to name, and payload to key, for example. Anyone do something similar?

Certainly, UIBUILDER can do this for you - you may need to transform the input JSON slightly to fit in with the uib-element node's "Form" setting. There is a built-in example that shows all of the different uib-element settings including forms. The output is a low-code JSON format that the uibuilder client library can convert direct to an HTML Form structure. You also get automatic handling of things like button clicks. You can even do the HTML conversion server-side if you prefer.

There are other ways to do this with UIBUILDER as well, but that may be the simplest.

Pretty simple stuff with dashboard-2 (@flowfuse/node-red-dashboard)

chrome_FW5I09MHGD

Here is the flow

[{"id":"28853be7be79a556","type":"ui-template","z":"b6818da1a76c1147","group":"638a7463e2421854","page":"","ui":"","name":"","order":1,"width":"6","height":"2","head":"","format":"<template>\n  <v-row class=\"\" dense>\n    <v-col cols=\"auto\" v-for=\"item in msg?.payload || []\" :key=\"item.Key\">\n      <v-btn @click=\"sendPayload(item)\" color=\"primary\" variant=\"outlined\">\n        {{ item.Name }}\n      </v-btn>\n    </v-col>\n  </v-row>\n</template>\n\n<script>\nexport default {\n  methods: {\n    sendPayload(item) {\n      this.send({ payload: item.Key, topic: item.Name });\n    }\n  }\n}\n</script>\n\n\n<style>\n  div.dynamic-buttons.nrdb-ui-widget.nrdb-ui-template {\n    overflow-y: unset;\n  }\n</style>","storeOutMessages":true,"passthru":false,"resendOnRefresh":true,"templateScope":"local","className":"dynamic-buttons","x":400,"y":520,"wires":[["c72543bdea5d14e2"]]},{"id":"e90416a364eadebd","type":"ui-button","z":"b6818da1a76c1147","group":"638a7463e2421854","name":"","label":"Random Buttons","order":2,"width":0,"height":0,"emulateClick":false,"tooltip":"","color":"","bgcolor":"","className":"","icon":"","iconPosition":"left","payload":"","payloadType":"date","topic":"topic","topicType":"msg","buttonColor":"","textColor":"","iconColor":"","enableClick":true,"enablePointerdown":false,"pointerdownPayload":"","pointerdownPayloadType":"str","enablePointerup":false,"pointerupPayload":"","pointerupPayloadType":"str","x":350,"y":400,"wires":[["773952792e35e55e"]]},{"id":"773952792e35e55e","type":"function","z":"b6818da1a76c1147","name":"random array of items","func":"const randomCount = Math.floor(Math.random() * 10) + 1\n\nconst payload = []\n\nfor (let i = 1; i <= randomCount; i++) {\n    payload.push({\n        Name: i === 1 ? 'One' : i === 2 ? 'Two' : i === 3 ? 'Three' : i === 4 ? 'Four' : i === 5 ? 'Five' : i === 6 ? 'Six' : i === 7 ? 'Seven' : i === 8 ? 'Eight' : i === 9 ? 'Nine' : 'Ten',\n        Key: i.toString()\n    })\n}\n\nmsg.payload = payload\nreturn msg\n","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":360,"y":460,"wires":[["28853be7be79a556"]]},{"id":"c72543bdea5d14e2","type":"ui-notification","z":"b6818da1a76c1147","ui":"f1633f89193de4bc","position":"top right","colorDefault":false,"color":"#df9a9a","displayTime":"1","showCountdown":true,"outputs":1,"allowDismiss":true,"dismissText":"Close","allowConfirm":false,"confirmText":"Confirm","raw":false,"className":"","name":"","x":600,"y":520,"wires":[[]]},{"id":"638a7463e2421854","type":"ui-group","name":"Group Name","page":"2ccae6b36a109b4b","width":6,"height":1,"order":1,"showTitle":false,"className":"","visible":"true","disabled":"false","groupType":"default"},{"id":"f1633f89193de4bc","type":"ui-base","name":"My Dashboard","path":"/dashboard","appIcon":"","includeClientData":true,"acceptsClientConfig":["ui-notification","ui-control"],"showPathInSidebar":false,"headerContent":"page","navigationStyle":"default","titleBarStyle":"default","showReconnectNotification":true,"notificationDisplayTime":1,"showDisconnectNotification":true,"allowInstall":true},{"id":"2ccae6b36a109b4b","type":"ui-page","name":"dynamic elements","ui":"f1633f89193de4bc","path":"/dyn-elements","icon":"home","layout":"grid","theme":"ad84029195b8d5f1","breakpoints":[{"name":"Default","px":"0","cols":"3"},{"name":"Tablet","px":"576","cols":"6"},{"name":"Small Desktop","px":"768","cols":"9"},{"name":"Desktop","px":"1024","cols":"12"}],"order":1,"className":"","visible":"true","disabled":"false"},{"id":"ad84029195b8d5f1","type":"ui-theme","name":"Default Theme","colors":{"surface":"#ffffff","primary":"#0094CE","bgPage":"#eeeeee","groupBg":"#ffffff","groupOutline":"#cccccc"},"sizes":{"density":"default","pagePadding":"12px","groupGap":"12px","groupBorderRadius":"4px","widgetGap":"12px"}},{"id":"c56db59f1c10671d","type":"global-config","env":[],"modules":{"@flowfuse/node-red-dashboard":"1.30.2"}}]
1 Like

The input JSON:

[
    {
        "id": "b1",
        "type": "button",
        "label": "Button 1",
        "value": "B1"
    },
    {
        "id": "b2",
        "type": "button",
        "label": "Button 2",
        "value": "B2"
    }
]

The output to Node-RED on button 1 press:

{
  "payload":{"b1":"B1","b2":"B2","value":"B1"},
  "_ui":{
    "type":"eventSend","id":"b1","name":"b1","slotText":"Button 1","dataset":{},
    "form":{
      "id":"form-sform1","valid":true,
      "b1":{"id":"b1","name":"b1","valid":true,"type":"button","value":"B1"},
      "b2":{"id":"b2","name":"b2","valid":true,"type":"button","value":"B2"}
    },
    "props":{},"attribs":{"type":"button","label":"Button 1","value":"B1","onclick":"uibuilder.eventSend(event)","title":"Type: button"},"classes":[],"event":"click","altKey":false,"ctrlKey":false,"shiftKey":false,"metaKey":false,"pointerType":"mouse","nodeName":"BUTTON","clientId":"G9Y0IOGJgI01e9e8Hr03z","pageName":"index.html","tabId":"t783683","from":"client"},"_socketId":"ZPV2OPctCPEtKtNAAAAa","_uib":{"url":"uib-element-test","_socketId":"ZPV2OPctCPEtKtNAAAAa","recovered":false,"ip":"::1","referer":"https://localhost:3001/uib-element-test/","version":"7.6.0-src","clientId":"G9Y0IOGJgI01e9e8Hr03z","tabId":"t783683","pageName":"index.html","urlParams":{},"connections":1,"tls":true,"connectedTimestamp":"2026-01-30T21:47:33.667Z","connectHeaders":{"host":"localhost:3001","connection":"keep-alive","sec-ch-ua-platform":"\"Windows\"","user-agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36","accept":"*/*","x-clientid":"uibuilder; module; 7.6.0-src;","sec-ch-ua":"\"Not(A:Brand\";v=\"8\", \"Chromium\";v=\"144\", \"Google Chrome\";v=\"144\"","sec-ch-ua-mobile":"?0","dnt":"1","sec-fetch-site":"same-origin","sec-fetch-mode":"cors","sec-fetch-dest":"empty","referer":"https://localhost:3001/uib-element-test/","accept-encoding":"gzip, deflate, br, zstd","accept-language":"en-GB,en-US;q=0.9,en;q=0.8"}
  },
  "_msgid":"f77e9d0be61945c1"}

I simply tweaked the element example page to send the data:

Thanks everyone, will give these a test run see what works for my use case.