Convert dashboard shopping list to v2

I'm proceeding with the conversion of my dashboard from v1 to v2 and now I'm in trouble...
I try to explain myself (sorry for my bad english).

In v1 I have a dashboard (for my wife's mobile) with the list for shopping ...

I have a list of context variables .... (generated from the database when the project start)

to get a fast response (when my wife press LOAD LIST) without having to query the database every time because the table has more the 100 rows

This is the function ...

const letGreen = "http://xxxxxxx.it:47xxx/lettere/green.png";
const letGrey = "http://xxxxxx.it:47xxx/lettere/grey.png";


if (stato === true) {
    icona = letGreen
} else {
    icona = letGrey
}

const x = {
    "prodotto": prodotto,
    "stato": stato,
    "icona": icona,
    "nome": nome,
    "id": id
}

const newId = "v" + id
context.set(newId, x)

so I create an array that I send to the ui-list...

let keys = context.keys();
let result = [];

// Loop
for (let i = 0; i < keys.length; i++) {
    let value = context.get(keys[i]);
    result.push(value);
}

msg.payload = result
msg.payload.sort((a, b) => a.nome.localeCompare(b.nome))
let array = msg.payload.map(item => ({
title: item.nome,
isChecked: item.stato,
icon: item.icona,
id: item.id
}))
output[0] = { payload: array }

and this is the result .....

(It might not be the most elegant code in the world, but it’s been working for two years).

Now, I would like to convert this dashboard in v2 but I have no idea how to do it and what component to use (ui-list is not present in v2).

Can you help me?

For speed and lower resource usage on mobile, have you considered switching to UIBUILDER - since you don't appear to need to use any of Dashboards features?

I have no problem to use UIBUILDER for my dashboard on mobile, but I have already many difficulties to use dashboard v2......

My shopping list how can be implement in UIBUILDER without making me cry all the day?

Can you share an example of the array output you are using?

Some questions:

  • Are you completely recreating the full list each time? Rather than amending the list.
  • You have 4 blue boxes - are those buttons? If so, what do they do?
  • Your source data that you cache from the db seems to be in many individual variables - why not use a single array of objects instead which would be much easier to handle I would think?
  • When you click on the checkbox, does it immediately send something back to Node-RED? Or do you have to press a button to update Node-RED? (and which would you prefer).

With uibuilder, we can take your input array and create a list from it. We can then manipulate the list very easily. There is an example in uibuilder's section of the Node-RED example library after you have loaded the package. It is part of the zero-code example.


It would be simple to add a handler to each entry that tracked clicks on the list entry itself that sends an update immediately back to node-red and changes the list icon. Probably a lot easier to use on mobile that using a checkbox.

This is the array output ...

If you want a flow I can try to share it .....

When I deploy NR I create the list of context variables from the db (only once) therefore I send the array with the data of the context variables every time I press the button CARICA LISTA or LISTA SEL.
LISTA SEL apply a filter on that array to show only the isChecked = true

output[1] = { payload: msg.payload.filter(e => e.isChecked === (true)) }

Yes,
SALVA: at the bottom of the dashboard I have two text-input for things that are not included in the list (saved in two variables and in db) - it wouldn't be necessary, but my wife, after writing, always asks me "How do I save?" - Now she knows how to do it :grinning_face:
CARICA LISTA: described above
SPESA CONCLUSA: my wife press this button when the shopping is finish and
one loop change in the db all the value Checked = false;
therefore I delete all the context variables and I create (again) the list of context variables from the db like when I press deploy (so I have all the "isChecked" in the list = false and icon = grey).
LISTA SEL: described above

I'm sure so, but it was the best thing I could do ....

When I click on the checkbox, I immediately send two output:
1 - with the id I update in the db the field checked (true/false) because if I deploy the project I have the list of shopping alway updated

2 - with the id I update the single context variable so if I press CARICA DATI I have the data updated without send a query to the database but only the loop on all the context variables.

I hope I made it clear, I am here all the afternoon ...

Thanks for that. I've been busy at work but I did find a couple of minutes to start creating a demo. Doesn't look very nice yet but that is easily fixed, I wanted to get the basic logic working.

Not all there yet by any means. But hopefully demonstrates that it is all quite straight-forwards.

[{"id":"a0346bf4b7bb7225","type":"group","z":"3badb0a6906eef7f","name":"Example UIBUILDER Shopping List","style":{"fill":"#bfdbef","fill-opacity":"0.29","label":true,"color":"#3f3f3f"},"nodes":["b44745fc1423ceef","6c13b25075aa8740","dc6b78ac7a21a417","9210c72945bbf0c7","2b116aacfe15b942","5a4af508bd9df8ec","86c42d09e04ebcde","37358a4ea308b8d7","4f371f7bd744689a","0f0f214d14299d01","3bc4ade8f86c2ef8","76c929bb2cf2d76b","09169acb7d586cf7"],"x":134,"y":3219,"w":822,"h":482},{"id":"b44745fc1423ceef","type":"link out","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"Out to shopping list","mode":"link","links":["2b116aacfe15b942"],"x":715,"y":3540,"wires":[]},{"id":"6c13b25075aa8740","type":"inject","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"Shopping list initialise","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"initialise-shopping-list","payload":"[{\"item\":\"Bread\",\"selected\":false},{\"item\":\"Milk\",\"selected\":true},{\"item\":\"Honey\",\"selected\":false},{\"item\":\"Flour\",\"selected\":false}]","payloadType":"json","x":280,"y":3540,"wires":[["dc6b78ac7a21a417"]]},{"id":"dc6b78ac7a21a417","type":"function","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"Low-code output","func":"// If the input msg is the initialise msg...\nif (msg.topic === 'initialise-shopping-list') {\n    // ... Initialise/update the context variable\n    context.set('shoppingList', msg.payload)\n}\n\n// Get the shopping list input data\nconst inputData = context.get('shoppingList') ?? []\n\n// Create the shopping list template\n// using UIBUILDER's low-code output json\n// NOTE: Copied from the output of a `uib-element` node\n//       set to output a list element.\nconst lowCode = {\n    \"topic\": \"shopping-list\",\n    \"mode\": \"update\",\n    \"_ui\": [{\n        \"method\": \"replace\",\n        \"components\": [{\n            \"type\": \"div\",\n            \"id\": \"eltest-ul-ol\",\n            \"parent\": \"#more\",\n            \"position\": \"last\",\n            \"attributes\": {\"aria-labelledby\": \"eltest-ul-ol-heading\"},\n            \"components\": [\n                {\n                    // Remove this object if no heading wanted\n                    \"type\": \"h2\",\n                    \"id\": \"eltest-ul-ol-heading\",\n                    // Change the list heading here:\n                    \"slot\": \"Shopping List\",\n                    \"components\": [],\n                },\n                {\n                    // The actual shopping list\n                    \"type\": \"ul\",\n                    // List entries will go here\n                    \"components\": [],\n                },\n            ],\n        }],\n    }],\n}\n\n// Grab a ref to the list entries components array\nconst shopList = lowCode._ui[0].components[0].components[1].components\n\n// Translate the input data to list entries.\n// Assumes the data is an array on msg.payload\ninputData.forEach( entry => {\n    shopList.push({\n        \"type\": \"li\",\n        // We can set any attribute here\n        \"attributes\": {\n            \"style\": `color: ${entry.selected === true ? \"green\": \"red\"}`,\n            \"onclick\": \"uibuilder.eventSend(event)\",\n        },\n        // This is the content of the list entry - can be HTML.\n        \"slot\": entry.item,\n    })\n})\n\n// Send the message\nreturn lowCode\n\n/**\n * input data schema:\n * {\"item\":\"Bread\",\"selected\":false}\n */","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":530,"y":3540,"wires":[["b44745fc1423ceef"]]},{"id":"9210c72945bbf0c7","type":"uibuilder","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"","topic":"","url":"shopping-list","okToGo":true,"fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"templateFolder":"blank","extTemplate":"","showfolder":false,"reload":false,"sourceFolder":"src","deployedVersion":"7.3.0","showMsgUib":false,"title":"","descr":"","editurl":"vscode://file/src/uibRoot/shopping-list/?windowId=_blank","x":520,"y":3380,"wires":[["5a4af508bd9df8ec"],["37358a4ea308b8d7"]]},{"id":"2b116aacfe15b942","type":"link in","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"In to shopping list","links":["b44745fc1423ceef"],"x":365,"y":3380,"wires":[["9210c72945bbf0c7"]]},{"id":"5a4af508bd9df8ec","type":"debug","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"debug 9","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":740,"y":3260,"wires":[]},{"id":"86c42d09e04ebcde","type":"debug","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"debug 11","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":895,"y":3440,"wires":[],"l":false},{"id":"37358a4ea308b8d7","type":"switch","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"Detect client connections","property":"uibuilderCtrl","propertyType":"msg","rules":[{"t":"eq","v":"client connect","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":750,"y":3420,"wires":[["4f371f7bd744689a"],["86c42d09e04ebcde"]]},{"id":"4f371f7bd744689a","type":"link out","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"shopping list client connect","mode":"link","links":["0f0f214d14299d01"],"x":895,"y":3400,"wires":[]},{"id":"0f0f214d14299d01","type":"link in","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"from shopping list client connect","links":["4f371f7bd744689a"],"x":365,"y":3500,"wires":[["dc6b78ac7a21a417"]]},{"id":"3bc4ade8f86c2ef8","type":"switch","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"Process list clicks","property":"_ui.nodeName","propertyType":"msg","rules":[{"t":"eq","v":"LI","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":730,"y":3360,"wires":[["76c929bb2cf2d76b"],[]]},{"id":"76c929bb2cf2d76b","type":"link out","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"link out 34","mode":"link","links":["09169acb7d586cf7"],"x":875,"y":3340,"wires":[]},{"id":"09169acb7d586cf7","type":"link in","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"link in 4","links":["76c929bb2cf2d76b"],"x":285,"y":3660,"wires":[[]]}]

You only need UIBUILDER installed to run this. After importing the flow, you MUST first set the URL in the uibuilder node and then immediately deploy. After that it will all work and you can play with it.

You've peaked my interest so I will try to get some time to expand the example to closer to your current Dashboard.

Ok, meanwhile I will try to recreate the list with my data.....

Great start. :slight_smile:

Did a bit more and got this:

Each entry bigger making it easier to use with touch with the checkmark built into the list.

Is your wife's phone a reasonably up-to-date model? If it is, I can use smarter CSS like this for styling the list:

ul#shoplist { /* ul with id of shoplist */
    padding-left: 0;

    & li {
        /* Turn off normal list entry icons so we can control them */
        list-style-type: none;
        /* top/bottom, left/right - add padding so easier for user to touch on a phone */
        padding: 0.5em 0;
    }

    & li.success { /* list entry with class of success */
        font-weight: bold;
    }

    /* Manually add a list entry icon so we can resize it - this is the default (not selected) version  */
    & li::before {
        content: "✖️";
        font-size: 2rem;
        vertical-align: middle;
        /* line-height: 20px; */
        padding-right: 0.5em;
    }

    /* this is the selected version */
    & li.success::before {
        content: "âś…";
    }
}

Very nice work!

Yes, my wife's phone is an up-to-date model ...

but now I'm in trouble ....
your new code, I have to create a css?
I see uibuilder today for the first time, I'll look for some examples tomorrow ...

Thank you ....

I'm looking forward to seeing the result of this. I think my wife would love this.

How do you sort your list, is it as entered or can your wife sort it?

Would I leave you in trouble! :smile:

I've moved a couple of things into the front-end code. You could, in fact move most of the function node into the front-end (browser) but I'll leave it where it is for now, there are some advantages for each method but I don't think it will make much difference for you.

However, the processing that I've added to the front-end automatically toggles the formatting and the value right in the browser, it would work even with no network - though, of course, the changes would not then be sent back to Node-RED. Though you should note that a future enhancement could indeed allow your wife to work offline and re-sync when reconnecting to your home network.

So here is an updated flow:

[{"id":"a0346bf4b7bb7225","type":"group","z":"3badb0a6906eef7f","name":"Example UIBUILDER Shopping List","style":{"fill":"#bfdbef","fill-opacity":"0.29","label":true,"color":"#3f3f3f"},"nodes":["b44745fc1423ceef","6c13b25075aa8740","dc6b78ac7a21a417","9210c72945bbf0c7","2b116aacfe15b942","5a4af508bd9df8ec","86c42d09e04ebcde","37358a4ea308b8d7","4f371f7bd744689a","0f0f214d14299d01","3bc4ade8f86c2ef8","76c929bb2cf2d76b","09169acb7d586cf7","a4f532159390254e","8f55878c5ad7398a"],"x":134,"y":3279,"w":1088,"h":422},{"id":"b44745fc1423ceef","type":"link out","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"Out to shopping list","mode":"link","links":["2b116aacfe15b942"],"x":715,"y":3540,"wires":[]},{"id":"6c13b25075aa8740","type":"inject","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"Shopping list initialise","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"initialise-shopping-list","payload":"[{\"title\":\"Bread\",\"isChecked\":false,\"icon\":\"\",\"id\":100},{\"title\":\"Milk\",\"isChecked\":true,\"icon\":\"\",\"id\":101},{\"title\":\"Honey\",\"isChecked\":false,\"icon\":\"\",\"id\":102},{\"title\":\"Flour\",\"isChecked\":false,\"icon\":\"\",\"id\":103}]","payloadType":"json","x":280,"y":3540,"wires":[["dc6b78ac7a21a417"]]},{"id":"dc6b78ac7a21a417","type":"function","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"Low-code output","func":"// If the input msg is the initialise msg...\nif (msg.topic === 'initialise-shopping-list') {\n    // ... Initialise/update the context variable\n    context.set('shoppingList', msg.payload)\n}\n\n// Get the shopping list input data\nconst inputData = context.get('shoppingList') ?? []\n\n// Create the shopping list template\n// using UIBUILDER's low-code output json\n// NOTE: Copied from the output of a `uib-element` node\n//       set to output a list element.\nconst lowCode = {\n    \"topic\": \"shopping-list\",\n    \"mode\": \"update\",\n    \"_ui\": [{\n        \"method\": \"replace\",\n        \"components\": [{\n            \"type\": \"div\",\n            \"id\": \"shopping-list\",\n            \"parent\": \"#more\",\n            \"position\": \"last\",\n            \"attributes\": {\"aria-labelledby\": \"eltest-ul-ol-heading\"},\n            \"components\": [\n                {\n                    // Remove this object if no heading wanted\n                    \"type\": \"h2\",\n                    \"id\": \"shop-list-heading\",\n                    // Change the list heading here:\n                    \"slot\": \"Shopping List\",\n                    \"components\": [],\n                },\n                {\n                    // The actual shopping list\n                    \"type\": \"ul\",\n                    \"id\": \"shoplist\",\n                    // List entries will go here\n                    \"components\": [],\n                },\n            ],\n        }],\n    }],\n}\n\n// Grab a ref to the list entries components array\nconst shopList = lowCode._ui[0].components[0].components[1].components\n\n// Translate the input data to list entries.\n// Assumes the data is an array on msg.payload\ninputData.forEach( entry => {\n    const classes = []\n    const styles = {\n        // padding: \"1em 0.5em\"\n    }\n\n    // styles['list-style'] = '\"✖️\"'\n\n    if (entry.isChecked) {\n        classes.push('success') // green background\n        // styles['font-weight'] = \"bold\"\n        // styles['list-style'] = '\"✅\"'\n    } else {\n        classes.push('surface3')\n    }\n\n    shopList.push({\n        \"type\": \"li\",\n        // We can set any attribute here\n        \"attributes\": {\n            // Use the default uib-brand.css classes\n            \"class\": classes.join(' '),\n            \"style\": mergeStyles(styles),\n            // Invert the value and send back to Node-RED\n            \"onclick\": \"handleListItemClick(event)\",\n            // \"ontouchend\": \"handleListItemClick(event)\",\n            // Track as data attributes for easier processing\n            \"data-title\": entry.title,\n            \"data-value\": entry.isChecked,\n            \"data-id\": entry.id,\n        },\n        // This is the content of the list entry - can be HTML.\n        \"slot\": entry.title,\n    })\n})\n\n// Send the message\nreturn lowCode\n\n// stylename: stylevalue; ...\nfunction mergeStyles(styles) {\n    let mergedStyles = ''\n    Object.keys(styles).forEach ( styleName => {\n        mergedStyles += `${styleName}: ${styles[styleName]}; `\n    })\n    return mergedStyles\n}\n\n/**\n * input data schema:\n * {\"title\":\"Bread\",\"isChecked\":false,\"idon\":\"\",\"id\":100}\n */","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":530,"y":3540,"wires":[["b44745fc1423ceef"]]},{"id":"9210c72945bbf0c7","type":"uibuilder","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"","topic":"","url":"shopping-list","okToGo":true,"fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"templateFolder":"blank","extTemplate":"","showfolder":false,"reload":false,"sourceFolder":"src","deployedVersion":"7.3.0","showMsgUib":false,"title":"","descr":"","editurl":"vscode://file/src/uibRoot/shopping-list/?windowId=_blank","x":300,"y":3360,"wires":[["3bc4ade8f86c2ef8"],["37358a4ea308b8d7"]]},{"id":"2b116aacfe15b942","type":"link in","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"In to shopping list","links":["b44745fc1423ceef","ddd1f248655218fc"],"x":185,"y":3360,"wires":[["9210c72945bbf0c7"]]},{"id":"5a4af508bd9df8ec","type":"debug","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"debug 9","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":715,"y":3340,"wires":[],"l":false},{"id":"86c42d09e04ebcde","type":"debug","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"debug 11","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":715,"y":3420,"wires":[],"l":false},{"id":"37358a4ea308b8d7","type":"switch","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"Detect client connections","property":"uibuilderCtrl","propertyType":"msg","rules":[{"t":"eq","v":"client connect","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":530,"y":3400,"wires":[["4f371f7bd744689a"],["86c42d09e04ebcde"]]},{"id":"4f371f7bd744689a","type":"link out","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"shopping list client connect","mode":"link","links":["0f0f214d14299d01"],"x":685,"y":3380,"wires":[]},{"id":"0f0f214d14299d01","type":"link in","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"from shopping list client connect","links":["4f371f7bd744689a"],"x":365,"y":3500,"wires":[["dc6b78ac7a21a417"]]},{"id":"3bc4ade8f86c2ef8","type":"switch","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"Process list clicks","property":"_ui.nodeName","propertyType":"msg","rules":[{"t":"eq","v":"LI","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":510,"y":3340,"wires":[["76c929bb2cf2d76b"],["5a4af508bd9df8ec"]]},{"id":"76c929bb2cf2d76b","type":"link out","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"link out 34","mode":"link","links":["09169acb7d586cf7"],"x":655,"y":3320,"wires":[]},{"id":"09169acb7d586cf7","type":"link in","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"link in 4","links":["76c929bb2cf2d76b"],"x":285,"y":3660,"wires":[["a4f532159390254e"]]},{"id":"a4f532159390254e","type":"debug","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"debug 8","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":445,"y":3660,"wires":[],"l":false},{"id":"8f55878c5ad7398a","type":"group","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["37e4f2c0838f124e","67eb7bc082b134aa","ddd1f248655218fc","0c2799ed68acea4a"],"x":814,"y":3319,"w":382,"h":122},{"id":"37e4f2c0838f124e","type":"inject","z":"3badb0a6906eef7f","g":"8f55878c5ad7398a","name":"Light","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"mark-light","payload":"{\"class\":\"light\"}","payloadType":"json","x":910,"y":3360,"wires":[["67eb7bc082b134aa"]]},{"id":"67eb7bc082b134aa","type":"uib-update","z":"3badb0a6906eef7f","g":"8f55878c5ad7398a","name":"Set mode","topic":"","mode":"update","modeSourceType":"modeType","cssSelector":"html","cssSelectorType":"str","slotSourceProp":"","slotSourcePropType":"msg","attribsSource":"payload","attribsSourceType":"msg","slotPropMarkdown":false,"x":1060,"y":3360,"wires":[["ddd1f248655218fc"]]},{"id":"ddd1f248655218fc","type":"link out","z":"3badb0a6906eef7f","g":"8f55878c5ad7398a","name":"link out 35","mode":"link","links":["2b116aacfe15b942"],"x":1155,"y":3360,"wires":[]},{"id":"0c2799ed68acea4a","type":"inject","z":"3badb0a6906eef7f","g":"8f55878c5ad7398a","name":"Dark","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"mark-dark","payload":"{\"class\":\"dark\"}","payloadType":"json","x":910,"y":3400,"wires":[["67eb7bc082b134aa"]]}]

For this version to fully work, after you have set the URL and deployed, open the uibuilder node and go to the "Files" tab and replace the 3 files with the following:

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>Shopping List - Node-RED uibuilder</title>
    <meta name="description" content="Node-RED uibuilder - Shopping List">

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

    <!-- Load the uibuilder library and do other front-end processing -->
    <script type="module" async src="./index.js"></script>

</head><body class="uib">
    
    <h1 class="with-subtitle">Home Shopping List</h1>
    <div role="doc-subtitle">Using the uibuilder ESM library.</div>

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

</body></html>

index.js

// Using this as an ES6 module 

// Import the uibuilder library first
// @ts-ignore
import uibuilder from '../uibuilder/uibuilder.esm.min.js'

// Handle list item click/touch events
window['handleListItemClick'] = function handleListItemClick(event) {
    const item = event.currentTarget

    item.value = !item.value // Toggle the value (true/false)
    item.classList.toggle('success') // Toggle the active class
    item.classList.toggle('surface3') // Toggle the active class

    console.log('Item clicked:', item)

    const msg = {
        topic: 'listItemClicked',
        payload: {
            title: item.dataset.title,
            value: item.value,
            id: item.dataset.id,
        }
    }

    // Send the clicked item's data to Node-RED
    uibuilder.send(msg)
}

index.css

/* Load defaults from `<userDir>/node_modules/node-red-contrib-uibuilder/front-end/uib-brand.min.css`
 * This is optional but reasonably complete and allows for light/dark mode switching.
 */
@import url("../uibuilder/uib-brand.min.css");

/**
 * NOTE: I'm using nested selectors here - they require fairly new browsers.
 *       You will have to unpack them if using a browser on an older phone.
 */

#shop-list-heading { display: none; } /* hide the default heading */

ul#shoplist { /* ul with id of shoplist */
    padding-left: 0;
    cursor: pointer;

    & li {
        /* Turn off normal list entry icons so we can control them */
        list-style-type: none;
        /* top/bottom, left/right - add padding so easier for user to touch on a phone */
        padding: 0.5em 0;
        border-top: 3px solid var(--surface2);
    }

    & li.success { /* list entry with class of success */
        font-weight: bold;
    }

    /* Manually add a list entry icon so we can resize it - this is the default (not selected) version  */
    & li::before {
        content: "✖️";
        font-size: 2rem;
        vertical-align: middle;
        /* line-height: 20px; */
        padding-right: 0.5em;
    }

    /* this is the selected version */
    & li.success::before {
        content: "âś…";
    }
}

The CSS is already set up to use either light or dark mode. The HTML is simply changing the titles and loading the front-end JavaScript. The JavaScript loads the uibuilder client library and creates a simple event handler function that updates things on click/touch and sends the data back to Node-RED.

Buttons are also easy - maybe tomorrow if you've not been able to work them out.

An enhanced version of this is likely to end up as another example in the library for a future uibuilder release. :wink:

Undoubtedly, this looks a bit overwhelming to start with. However, I've tried to annotate everything so keep reading through things and I think you will soon get a feeling for how everything works and will start to think of improvements. Just take things a step at a time and above all, have fun experimenting, it is very addictive!

Oh, nearly forgot. To do list for the to-do list!

  • Oops, the returned data is not yet updating the context variable.
  • Sort the list?
  • Add the buttons.
    • Toggle filter to only selected or all entries
    • Including one to add new entries
  • More advanced:
    • Add a right-click handler to each list entry that allows the entry to be deleted. :man_mage:
    • Add a drag and drop handler to allow reordering of the list.
  • Ask yourself why you bothered with a database when a retained context variable would do the job.

And a corrected flow - this time with the part that updates the context variable correctly.

[{"id":"a0346bf4b7bb7225","type":"group","z":"3badb0a6906eef7f","name":"Example UIBUILDER Shopping List","style":{"fill":"#bfdbef","fill-opacity":"0.29","label":true,"color":"#3f3f3f"},"nodes":["b44745fc1423ceef","6c13b25075aa8740","dc6b78ac7a21a417","9210c72945bbf0c7","2b116aacfe15b942","5a4af508bd9df8ec","86c42d09e04ebcde","37358a4ea308b8d7","4f371f7bd744689a","0f0f214d14299d01","3bc4ade8f86c2ef8","76c929bb2cf2d76b","09169acb7d586cf7","a4f532159390254e","8f55878c5ad7398a"],"x":134,"y":3279,"w":1088,"h":422},{"id":"b44745fc1423ceef","type":"link out","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"Out to shopping list","mode":"link","links":["2b116aacfe15b942"],"x":715,"y":3540,"wires":[]},{"id":"6c13b25075aa8740","type":"inject","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"Shopping list initialise","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"initialise-shopping-list","payload":"[{\"title\":\"Bread\",\"isChecked\":false,\"icon\":\"\",\"id\":100},{\"title\":\"Milk\",\"isChecked\":true,\"icon\":\"\",\"id\":101},{\"title\":\"Honey\",\"isChecked\":false,\"icon\":\"\",\"id\":102},{\"title\":\"Flour\",\"isChecked\":false,\"icon\":\"\",\"id\":103}]","payloadType":"json","x":280,"y":3540,"wires":[["dc6b78ac7a21a417"]]},{"id":"dc6b78ac7a21a417","type":"function","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"Low-code output","func":"// Get the shopping list input data - gets a live reference so updates will be reflected\nconst inputData = context.get('shoppingList') ?? []\n\n// If the input msg is the initialise msg...\nif (msg.topic === 'initialise-shopping-list') {\n    // ... Initialise/update the context variable\n    context.set('shoppingList', msg.payload)\n} else if (msg.topic === 'listItemClicked') {\n    updateShoppingList(msg.payload)\n    return\n}\n\n// Create the shopping list template\n// using UIBUILDER's low-code output json\n// NOTE: Copied from the output of a `uib-element` node\n//       set to output a list element.\nconst lowCode = {\n    \"topic\": \"shopping-list\",\n    \"mode\": \"update\",\n    \"_ui\": [{\n        \"method\": \"replace\",\n        \"components\": [{\n            \"type\": \"div\",\n            \"id\": \"shopping-list\",\n            \"parent\": \"#more\",\n            \"position\": \"last\",\n            \"attributes\": {\"aria-labelledby\": \"eltest-ul-ol-heading\"},\n            \"components\": [\n                {\n                    // Remove this object if no heading wanted\n                    \"type\": \"h2\",\n                    \"id\": \"shop-list-heading\",\n                    // Change the list heading here:\n                    \"slot\": \"Shopping List\",\n                    \"components\": [],\n                },\n                {\n                    // The actual shopping list\n                    \"type\": \"ul\",\n                    \"id\": \"shoplist\",\n                    // List entries will go here\n                    \"components\": [],\n                },\n            ],\n        }],\n    }],\n}\n\n// Grab a ref to the list entries components array\nconst shopList = lowCode._ui[0].components[0].components[1].components\n\n// Translate the input data to list entries.\n// Assumes the data is an array on msg.payload\ninputData.forEach( entry => {\n    const classes = []\n    const styles = {\n        // padding: \"1em 0.5em\"\n    }\n\n    // styles['list-style'] = '\"✖️\"'\n\n    if (entry.isChecked) {\n        classes.push('success') // green background\n        // styles['font-weight'] = \"bold\"\n        // styles['list-style'] = '\"✅\"'\n    } else {\n        classes.push('surface3')\n    }\n\n    shopList.push({\n        \"type\": \"li\",\n        // We can set any attribute here\n        \"attributes\": {\n            // Use the default uib-brand.css classes\n            \"class\": classes.join(' '),\n            \"style\": mergeStyles(styles),\n            // Invert the value and send back to Node-RED\n            \"onclick\": \"handleListItemClick(event)\",\n            // \"ontouchend\": \"handleListItemClick(event)\",\n            // Track as data attributes for easier processing\n            \"data-title\": entry.title,\n            \"data-value\": entry.isChecked,\n            \"data-id\": entry.id,\n        },\n        // This is the content of the list entry - can be HTML.\n        \"slot\": entry.title,\n    })\n})\n\n// Send the message\nreturn lowCode\n\n// stylename: stylevalue; ...\nfunction mergeStyles(styles) {\n    let mergedStyles = ''\n    Object.keys(styles).forEach ( styleName => {\n        mergedStyles += `${styleName}: ${styles[styleName]}; `\n    })\n    return mergedStyles\n}\n\nfunction updateShoppingList(data) {\n    console.log({data})\n    // Find the entry in x with the id passed back from the client\n    // let entry = inputData.find(item => item.id === data.id)\n    const index = inputData.findIndex(item => item.title === data.title)\n    console.log(index)\n\n    if (index !== -1) {\n        inputData[index].isChecked = data.value === 1 ? true : false\n        node.status({fill:\"green\",shape:\"dot\",text:`Entry ${data.title} updated. Value: ${inputData[index].isChecked}`})\n        context.set('shoppingList', inputData)\n    } else {\n        node.status({ fill: \"red\", shape: \"ring\", text: `Entry ${data.title} not found` })\n        node.warn('Entry not found')\n    }\n}\n\n/**\n * input data schema:\n * {\"title\":\"Bread\",\"isChecked\":false,\"idon\":\"\",\"id\":100}\n */","outputs":1,"timeout":0,"noerr":0,"initialize":"","finalize":"","libs":[],"x":530,"y":3540,"wires":[["b44745fc1423ceef"]]},{"id":"9210c72945bbf0c7","type":"uibuilder","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"","topic":"","url":"shopping-list","okToGo":true,"fwdInMessages":false,"allowScripts":false,"allowStyles":false,"copyIndex":true,"templateFolder":"blank","extTemplate":"","showfolder":false,"reload":false,"sourceFolder":"src","deployedVersion":"7.3.0","showMsgUib":false,"title":"","descr":"","editurl":"vscode://file/src/uibRoot/shopping-list/?windowId=_blank","x":300,"y":3360,"wires":[["3bc4ade8f86c2ef8"],["37358a4ea308b8d7"]]},{"id":"2b116aacfe15b942","type":"link in","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"In to shopping list","links":["b44745fc1423ceef","ddd1f248655218fc"],"x":185,"y":3360,"wires":[["9210c72945bbf0c7"]]},{"id":"5a4af508bd9df8ec","type":"debug","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"debug 9","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":715,"y":3340,"wires":[],"l":false},{"id":"86c42d09e04ebcde","type":"debug","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"debug 11","active":false,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":715,"y":3420,"wires":[],"l":false},{"id":"37358a4ea308b8d7","type":"switch","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"Detect client connections","property":"uibuilderCtrl","propertyType":"msg","rules":[{"t":"eq","v":"client connect","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":530,"y":3400,"wires":[["4f371f7bd744689a"],["86c42d09e04ebcde"]]},{"id":"4f371f7bd744689a","type":"link out","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"shopping list client connect","mode":"link","links":["0f0f214d14299d01"],"x":685,"y":3380,"wires":[]},{"id":"0f0f214d14299d01","type":"link in","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"from shopping list client connect","links":["4f371f7bd744689a"],"x":365,"y":3500,"wires":[["dc6b78ac7a21a417"]]},{"id":"3bc4ade8f86c2ef8","type":"switch","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"Process list clicks","property":"topic","propertyType":"msg","rules":[{"t":"eq","v":"listItemClicked","vt":"str"},{"t":"else"}],"checkall":"false","repair":false,"outputs":2,"x":510,"y":3340,"wires":[["76c929bb2cf2d76b"],["5a4af508bd9df8ec"]]},{"id":"76c929bb2cf2d76b","type":"link out","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"link out 34","mode":"link","links":["09169acb7d586cf7"],"x":655,"y":3320,"wires":[]},{"id":"09169acb7d586cf7","type":"link in","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"link in 4","links":["76c929bb2cf2d76b"],"x":285,"y":3660,"wires":[["a4f532159390254e","dc6b78ac7a21a417"]]},{"id":"a4f532159390254e","type":"debug","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","name":"debug 8","active":true,"tosidebar":true,"console":false,"tostatus":false,"complete":"true","targetType":"full","statusVal":"","statusType":"auto","x":445,"y":3660,"wires":[],"l":false},{"id":"8f55878c5ad7398a","type":"group","z":"3badb0a6906eef7f","g":"a0346bf4b7bb7225","style":{"stroke":"#999999","stroke-opacity":"1","fill":"none","fill-opacity":"1","label":true,"label-position":"nw","color":"#a4a4a4"},"nodes":["37e4f2c0838f124e","67eb7bc082b134aa","ddd1f248655218fc","0c2799ed68acea4a"],"x":814,"y":3319,"w":382,"h":122},{"id":"37e4f2c0838f124e","type":"inject","z":"3badb0a6906eef7f","g":"8f55878c5ad7398a","name":"Light","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"mark-light","payload":"{\"class\":\"light\"}","payloadType":"json","x":910,"y":3360,"wires":[["67eb7bc082b134aa"]]},{"id":"67eb7bc082b134aa","type":"uib-update","z":"3badb0a6906eef7f","g":"8f55878c5ad7398a","name":"Set mode","topic":"","mode":"update","modeSourceType":"modeType","cssSelector":"html","cssSelectorType":"str","slotSourceProp":"","slotSourcePropType":"msg","attribsSource":"payload","attribsSourceType":"msg","slotPropMarkdown":false,"x":1060,"y":3360,"wires":[["ddd1f248655218fc"]]},{"id":"ddd1f248655218fc","type":"link out","z":"3badb0a6906eef7f","g":"8f55878c5ad7398a","name":"link out 35","mode":"link","links":["2b116aacfe15b942"],"x":1155,"y":3360,"wires":[]},{"id":"0c2799ed68acea4a","type":"inject","z":"3badb0a6906eef7f","g":"8f55878c5ad7398a","name":"Dark","props":[{"p":"payload"},{"p":"topic","vt":"str"}],"repeat":"","crontab":"","once":false,"onceDelay":0.1,"topic":"mark-dark","payload":"{\"class\":\"dark\"}","payloadType":"json","x":910,"y":3400,"wires":[["67eb7bc082b134aa"]]}]

List is in alphabetical order and no, my wife don't can sort it.

Hi,
first thing thank you for the work done, result is very nice!

With the four buttons and the two text fields would be ready to give it to my wife.

This is an euphemism :grinning_face: for me of course, but I will study to try to become independent, even if for now your help is indispensable.

I will try to explain my choice ...
in addition to the panel for my wife's phone (visible in my first post) in the kitchen I have a tablet with this dashboard ...

and you can see the duplicate of the shipping list.
This is to update the list quickly when a product must be bought.
In the kitchen I also have an amazon echo and just say "alexa, add bread" that my "hal 9000" :grinning_face: update the list; if the product is not present in the list, it's written in the two fields at bottom of the list (in this case the problem is applestrudel very unstable ...).

For all these interactions, I thought that a db was the best solution.
To manage all data of my db I have another dashboard (for pc)

and in this panel I can add or delete the componet of the list.
After every modification the button LOAD DATI SPESA delete all the context variables and the usual loop recreate the new list.

1 Like

I'm doing some test (with delete and load variables) and I have this problem..
When I push an item the value is 1 color green / 0 color grey...
After some test I have value 1 color grey / 0 color green for the same item, but if I change item value and color are ok ...

Now I test again ....

EDIT:
I have one item grey in the dash...
I push the item and the color change in green and output value = 1 ...
the variable is correct (true) the db is correct (T)...
I send again the list of variable to the function Low-code output and I reload the page
The dash is correct (item color green) but if I push the item, the color change in grey but the output value is 1, not 0

So the on-screen and the Node-RED versions of the list are out-of-step?

Are you using the last version I sent? If so, you might need to change the 0/1 values to false/true before updating your database. While 0/1 is "falsy" and "truthy", they are not always interchangeable. I added that to the update function in the function node but you probably haven't seen that.

It might be possible to improve the data updates coming back from the client but you always hit an issue in translation where some things end up as strings instead of numbers/booleans so not uncommon to have to deal with that.

Which would also be fairly easy to re-create using uibuilder. :grin: Though I might be tempted to use a custom table component or helper library. I do have a full table component on my backlog to get to when time permits (which sadly, right now it doesn't) that would make things easier. I have a component (have a look at the web components library on my GitHub) that converts a simple JSON array or object to a table but it doesn't do edits as yet.

Yes

No, I expressed myself badly ....
After the first push on item (grey) page uibuilder ...
. the item color change gray to green
. context var is correct (true)
. database value is correct (true)
. dashboard nr is correct (green)

I send again the list of variable to the function Low-code output and I reload the page uibuilder

The item color is green (correct because the context var is true)

If now I push again the item in page uibuilder, the color change, from green to grey and I have in output value 1 but I should have 0

OK, I can see the same happening on my test instance as well - a small bug in the client code I think. I did do it very quickly yesterday evening. :slight_smile:

Started looking but rather busy at work today so might not be able to resolve for a few hours.