Convert dashboard shopping list to v2

it does exactly what op wants, with 'layout' you mean colors ?

Thanks for that flow @bakman2
One can learn a lot from different solutions to the same requirement.

I think that flow should go on the Share Your Projects subforum too, maybe as "Node-red mobile shopping list without a dashboard component"

As we are starting to look at alternatives - here is the start of how I would do it if starting from scratch. This has taken me about an hour.

I will doubtless move those top buttons as they don't look so good.

In this version, clicking a product in the lower list will add to the upper. Adding a product will show an overlayed dialog.

I'm making use of the fact that uibuilder uses static files and the fact that much of the logic was already in code. Moving most of the code to the static files so that I can use VS Code to do the editing, helped greatly by using GitHub Copilot AI as my co-editor.

All node-red needs to do is provide the web server take the full product list when a browser connection is made and sync the data changes from the browser to node-red. Everything else happens in the browser. This would make it relatively easy to turn the shopping list into an offline capable web app.

Styling CSS is still minimal since we are using uibuilder's default styles which, of course, work for both light and dark colour schemes. We are also using accessible HTML with a well-formed structure, still only 27 lines of HTML. Only around 20 lines of JavaScript (not including the extra function mentioned next).

JavaScript is a bit heavier that anticipated but that is purely because I discovered a weakness in uibuilder's built-in applyTemplate function. So I made a copy. I will actually probably end up doing it differently anyway.

I love getting to play with these challenges as it can really help me improve uibuilder for everyone by looking at real-world problems raised by others.

1 Like

@bakman2
Following your input I connected my db and this is the result (all the elements)

and only selected elements ...

Last element is the join of the two text input at the bottom of dashboard v1 (first post).

Thank you .....

You can replace the html part (excluding the script/header) with this:

<div x-data="load()" class="container mx-auto p-6 max-h-screen flex flex-col">
      <h1 class="text-3xl font-bold mb-4">Shopping List</h1>
      <!-- Cart -->
      <div>
        <template x-if="!cartView">
          <button @click="cartView = true" class="px-3 py-1 rounded mb-4 text-white" x-text="'View Cart ('+cart?.length+')'" :class="cart.length && cart.length>0 ? 'bg-blue-500 font-semibold': 'bg-gray-300 cursor-default'" :disabled="cart.length>0 ? false:'disabled'"></button>
        </template>
        <template x-if="cartView">
          <div>
            <button @click="toggleCartView" class="text-white px-3 py-1 rounded mb-4" :class="cart.length>0 ? 'bg-blue-500 font-semibold': 'bg-gray-300 cursor-default'">View products</button>
            <button @click="cartView = false;cart = [];" class="text-white px-3 py-1 rounded mb-4 bg-red-500 font-semibold">Empty Cart</button>
            <h2 class="text-xl font-semibold mb-2">Cart</h2>
          </div>
        </template>

        <ul x-show="cartView " class="space-y-2">
          <template x-for="(item, index) in cart" :key="index">
            <li class="flex justify-between items-center bg-white py-2.5 px-4 rounded shadow">
              <span x-text="item"></span>
              <button @click="removeFromCart(item)" class="bg-red-500 text-white text-sm px-3 py-1 rounded">Remove</button>
            </li>
          </template>
        </ul>
      </div>
      <!-- Product List -->
      <div class="mb-4 overflow-y-auto max-h-screen" x-show="!cartView || cart?.length === 0">
        <h2 class="text-xl font-semibold mb-2" x-text="'Products ('+products.length+')'"></h2>
        <ul class="space-y-2">
         
          <template x-for="(product, index) in products?.sort((a,b)=>a.localeCompare(b))" :key="index">
            <li class="flex justify-between items-center bg-white py-2.5 px-4 rounded shadow cursor-pointer " :class="cart.includes(product) ? 'bg-blue-500 text-white font-semibold  hover:bg-blue-500': 'bg-white hover:bg-gray-200'" @click="cart.includes(product) ? cart = cart.filter(i => i !== product) : cart.push(product)">
              <span x-text="product"></span>
              <div class="p-1 rounded" @click="removeProduct(product);$event.stopPropagation()">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hover:bg-red-500 p-1 rounded hover:fill-white fill-gray-400 h-6 w-6">
                  <title>remove product</title>
                  <path class="" d="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M7,13H17V11H7" />
                </svg>
              </div>
            </li>
          </template>
        </ul>

      </div>
       <div x-show="!cartView" class="flex justify-between items-center bg-white py-2.5 px-4 rounded shadow">
            <div class="flex justify-between gap-2 items-center w-full" x-data="{product_name:''}">
              <input type="text" placeholder="new product" class="w-full px-2 py-1" x-model="product_name" @keyup.enter="addProduct(product_name);product_name = ''" />
              <button class="text-white text-sm px-3 py-1 rounded" :class="product_name.length>0 ? 'bg-blue-500':'bg-gray-200'" :disabled="product_name.length>0 ? false: 'disabled'" @click="addProduct(product_name);product_name = ''">Save</button>
            </div>
          </div>
    </div>

Done! Thank you ...

It's possible the same behavior in the View Cart?

Do you mean that you want to be able to add it directly to the cart and save it as a new product ?

No, sorry, I got confused ....

In the page with the list of shopping the head is fixed and the body scroll under ...
It's possible the same behavior in the View Cart?

Lates version of my alternate.

The "Filter and add products" input does exactly that, if you enter a new product name, the filtered list is empty and the "Add" button adds the new product. The Node-RED flow is now much simpler. Everything happens in the browser except the initial data load and recording updated/new/deleted products.

I will be using browser retained storage so that the client works even if offline - great for taking the phone to the shops with no connection to Node-RED (though no offline data updates handled yet - they will come).

The number input in the list is simply a reminder for how many/how much of something to get. Future enhancements would be for a record of where to buy a product, when it was last purchased and a retained sort order (to allow more common items to be kept at the top of the list).

Ah sorry I see what you mean, I have combined the lists, the interaction will now be the same for the products/cart and both will scroll

    <div x-data="load()" class="container mx-auto p-6 max-h-screen flex flex-col">
      <h1 class="text-3xl font-bold mb-4">Shopping List</h1>
      <div>
        <button x-show="!cartView" @click="cartView = true" class="px-3 py-1 rounded mb-4 text-white" x-text="'View Cart ('+cart?.length+')'" :class="cart.length && cart.length>0 ? 'bg-blue-500 font-semibold': 'bg-gray-300 cursor-default'" :disabled="cart.length>0 ? false:'disabled'"></button>
        <button x-show="cartView" @click="toggleCartView" class="text-white px-3 py-1 rounded mb-4" :class="cart.length>0 ? 'bg-blue-500 font-semibold': 'bg-gray-300 cursor-default'">View products</button>
        <button x-show="cartView" @click="cartView = false;cart = [];" class="text-white px-3 py-1 rounded mb-4 bg-red-500 font-semibold">Empty Cart</button>
      </div>
      <div class="mb-4 overflow-y-auto max-h-screen">
        <h2 class="text-xl font-semibold mb-2" x-text="!cartView ? 'Products ('+products.length+')' : 'Cart ('+cart?.length+')'"></h2>
        <div class="space-y-2">
          <template x-for="(item, index) in cartView ? cart.sort((a,b)=>a.localeCompare(b)) : products?.sort((a,b)=>a.localeCompare(b))" :key="index">
            <div class="flex justify-between items-center py-3 px-4 rounded shadow cursor-pointer h-14" :class="cart.includes(item) ? 'bg-blue-500 text-white font-semibold  hover:bg-blue-500': 'bg-white hover:bg-gray-200'" @click="cart.includes(item) ? cart = cart.filter(i => i !== item) : cart.push(item);if(cart.length==0) cartView = false">
              <span x-text="item"></span>
              <div x-show="!cartView" class="p-1 rounded" @click="removeProduct(item);$event.stopPropagation()">
                <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hover:bg-red-500 p-1 rounded hover:fill-white fill-gray-400 h-6 w-6">
                  <title>remove product</title>
                  <path class="" d="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M7,13H17V11H7" />
                </svg>
              </div>
            </div>
          </template>
        </div>
      </div>
      <div x-show="!cartView" class="flex justify-between items-center bg-white py-2.5 px-4 rounded shadow">
        <div class="flex justify-between gap-2 items-center w-full" x-data="{product_name:''}">
          <input type="text" placeholder="new product" class="w-full px-2 py-1" x-model="product_name" @keyup.enter="addProduct(product_name);product_name = ''" />
          <button class="text-white text-sm px-3 py-1 rounded" :class="product_name.length>0 ? 'bg-blue-500':'bg-gray-200'" :disabled="product_name.length>0 ? false: 'disabled'" @click="addProduct(product_name);product_name = ''">Save</button>
        </div>
      </div>
    </div>

Ok, works fine, thank you.
In view cart I don't see the button remove and when I push an element, it disappears ...
It's correct? (for me is ok also this way)

Indeed it was my lazy way.

If you want the button instead, you can replace the code between the <template ....>...</template> with:

            <div>
              <div x-show="!cartView" class="flex justify-between items-center py-3 px-4 rounded shadow cursor-pointer" :class="cart?.includes(item) ? 'bg-blue-500 text-white font-semibold  hover:bg-blue-500': 'bg-white hover:bg-gray-200'" @click="cart?.includes(item) ? cart = cart?.filter(i => i !== item) : cart.push(item);if(cart?.length==0) cartView = false">
                <span x-text="item"></span>
                <div x-show="!cartView" class="p-1 rounded" @click="removeProduct(item);$event.stopPropagation()">
                  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" class="hover:bg-red-500 p-1 rounded hover:fill-white fill-gray-400 h-5 w-5">
                    <title>remove product</title>
                    <path class="" d="M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M7,13H17V11H7" />
                  </svg>
                </div>
              </div>
              <div x-show="cartView" class="flex justify-between items-center bg-white items-center py-3 px-4 rounded shadow">
                <span x-text="item"></span>
                <button class="bg-red-500 text-white text-sm px-3 py-1 rounded" @click="cart.includes(item) ? cart = cart.filter(i => i !== item) : cart.push(item);if(cart.length==0) cartView = false">Remove</button>
              </div>
            </div>

this may be a little nicer.

Ok, last request,
to update my db I need to know when the buttons are pressed, above all Empty cart ...
I put several debug node but I can't understand if there is this possibility.
Can you help me?

This should output every action, which is an object with both the cart and the products.
if the 'cart' is empty, it will be an empty array.

Yes, I already check the cart but I thought there was another way ....

What should happen if all items are removed manually (ie = empty cart) ?

Nothing .....
until the button Empty cart is pressed the db is not updated ....

EDIT: ah ok, I understand what you mean, but this is not my case, my wife never delete every single product; It would be better to be able to handle this event differently anyway...

You are referring to 'the db' but the data is being saved with every action in flow context.
I can add a specific action that you can capture when the 'empty cart' button is pressed, but that would be the same as an empty array (?)

I think no .....
in case my wife delete one or more single product var flow is update ....
but If I can capture the action 'empty cart' button pressed, only in this moment I can update the db,
but empty cart must continue also to delete the flow shippingList.
It should work fine.

EDIT: with this possibility I stop checking if the cart is empty of course