# Shopping cart

Source: https://signy.dev/examples/shopping-cart/

Products, a cart and totals that follow every change.

The cart lives in a store. Changing a quantity writes one path, such as `cart.set('lines', 0, 'qty', 2)`, and only the cells that read it update. Computed values add up the subtotal, the discount and the total.

## Preview

[Open the page on its own](https://signy.dev/examples/shopping-cart/demo/index.html.md)

## Code

Save it as an `.html` file and open it in a browser. It loads Signy and Iris CSS from the CDN, so there is nothing to install or build.

**Show the full page**

```html
<!doctype html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta name="color-scheme" content="light dark">
    <title>Shopping cart - Signy example</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/iriscss@1/dist/iris.min.css">
    <script src="https://cdn.jsdelivr.net/npm/signy@1/dist/signy.global.min.js"></script>
  </head>
  <body>
    <main class="container" id="app"></main>

    <script>
      const { Component, Computed, application, html, store, For, Show } = Signy;

      const PRODUCTS = [
        { id: "tea", name: "Green tea", price: 6.5 },
        { id: "honey", name: "Wildflower honey", price: 9 },
        { id: "biscuits", name: "Butter biscuits", price: 4.25 },
      ];

      const COUPONS = { SPRING: 0.1 };

      const euro = (value) => `${value.toFixed(2)} EUR`;

      class Shop extends Component {
        cart = store({ lines: [], coupon: "" });

        subtotal = new Computed(() =>
          this.cart.state.lines.reduce((sum, line) => sum + line.price * line.qty, 0),
        );
        discount = new Computed(() => this.subtotal.value * (COUPONS[this.cart.state.coupon] ?? 0));
        total = new Computed(() => this.subtotal.value - this.discount.value);

        add(product) {
          const index = this.cart.state.lines.findIndex((line) => line.id === product.id);
          if (index === -1) this.cart.set("lines", (lines) => [...lines, { ...product, qty: 1 }]);
          else this.cart.set("lines", index, "qty", (qty) => qty + 1);
        }

        change(id, delta) {
          const index = this.cart.state.lines.findIndex((line) => line.id === id);
          const qty = this.cart.state.lines[index].qty + delta;
          if (qty > 0) this.cart.set("lines", index, "qty", qty);
          else this.cart.set("lines", (lines) => lines.filter((line) => line.id !== id));
        }

        // A line is a plain object from the store, so read its qty through the store to keep it live
        qtyOf(id) {
          return this.cart.state.lines.find((line) => line.id === id)?.qty ?? 0;
        }

        render() {
          return html`
            <h1>Corner shop</h1>
            <div class="grid">
              ${For({
                each: PRODUCTS,
                keyBy: "id",
                children: (product) => html`
                  <article class="card">
                    <h2>${product.name}</h2>
                    <p>${euro(product.price)}</p>
                    <button class="primary" onClick=${() => this.add(product)}>Add to cart</button>
                  </article>
                `,
              })}
            </div>

            <h2>Cart</h2>
            ${Show({
              when: () => this.cart.state.lines.length,
              fallback: html`<p>Your cart is empty.</p>`,
              children: html`
                <table>
                  <thead>
                    <tr><th scope="col">Product</th><th scope="col">Quantity</th><th scope="col">Price</th></tr>
                  </thead>
                  <tbody>
                    ${For({
                      each: () => this.cart.state.lines,
                      keyBy: "id",
                      children: (line) => html`
                        <tr>
                          <td>${line.name}</td>
                          <td>
                            <div class="cluster">
                              <button class="small outlined" aria-label=${`One less ${line.name}`} onClick=${() => this.change(line.id, -1)}>−</button>
                              <span>${() => this.qtyOf(line.id)}</span>
                              <button class="small outlined" aria-label=${`One more ${line.name}`} onClick=${() => this.change(line.id, 1)}>+</button>
                            </div>
                          </td>
                          <td>${() => euro(line.price * this.qtyOf(line.id))}</td>
                        </tr>
                      `,
                    })}
                  </tbody>
                </table>
                <form class="group" onSubmit=${(e) => {
                  e.preventDefault();
                  this.cart.set("coupon", e.currentTarget.elements.coupon.value.trim().toUpperCase());
                }}>
                  <input name="coupon" placeholder="Coupon, try SPRING" aria-label="Coupon" />
                  <button class="secondary">Apply</button>
                </form>
                <p>Subtotal: ${() => euro(this.subtotal.value)}</p>
                <p>Discount: ${() => euro(this.discount.value)}</p>
                <p><strong>Total: ${() => euro(this.total.value)}</strong></p>
              `,
            })}
          `;
        }
      }

      application(Shop).mount("#app");
    </script>
  </body>
</html>

```

## What it uses

- [Store](https://signy.dev/documentation/1/reactivity/store/index.html.md) with path writes
- [Computed](https://signy.dev/documentation/1/reactivity/computed/index.html.md) totals
- [For](https://signy.dev/documentation/1/templates/for-and-index/index.html.md) and [Show](https://signy.dev/documentation/1/templates/show/index.html.md)
