Skip to content

Cart with Server & Client State

This example fetches product data from an API using query. This also defines a CartStore, which has a cartItems array and two functions: add and remove (for adding and removing items from the cart). When you add or remove items from the cart, CartStore.add or CartStore.remove is called, which updates the cartItems array.

The cartItems array is then connected to the CartElement using connect, which updates the cartItems property on the CartElement whenever CartStore.cartItems is updated. This allows you to keep the cart in sync.

Lastly, the CartStore, by default, is persisted to localStorage. This means that if you refresh the page, the cart will still be there. You can also set the expiry option to set an expiry time for the store. For example, if you set expiry to 1000 * 60 * 60 * 24 * 3, then the store will expire after 3 days.

HTML:

 <article>
  <h2>Products</h2>
  <cami-product-list></cami-product-list>
</article>
<article>
  <h2>Cart</h2>
  <cami-cart></cami-cart>
</article>

<!-- <script src="./build/cami.cdn.js"></script> -->
<!-- CDN version below -->
<script src="https://unpkg.com/cami@latest/build/cami.cdn.js"></script>
<script type="module">
  const { html, ReactiveElement } = cami;

  const CartStore = cami.store({
    cartItems: [],
  },
  {
    name: 'CartStore',
    expiry: 1000 * 60 * 60 * 24 * 3
  }); // 3 days
  // CartStore.reset() // if for some reason, you want to reset the store

  CartStore.register('add', (state, payload) => {
    const newItem = {...payload, id: Date.now()}; // lame way to generate a unique id
    state.cartItems.push(newItem);
  });

  CartStore.register('remove', (state, payload) => {
    state.cartItems = state.cartItems.filter(item => item.id !== payload.id);
  });

  cami.debug.enable();

  // Define a middleware function
  const loggerMiddleware = (context) => {
    console.log(`Action ${context.action} was dispatched with payload:`, context.payload);
  };

  // Use the middleware function with the initialState
  CartStore.use(loggerMiddleware);

  class ProductListElement extends ReactiveElement {
    cartItems = [];
    products = [];

    onConnect() {
      CartStore.subscribe(state => {
        this.cartItems = state.cartItems;
      });
      this.products = this.query({
        queryKey: ['products'],
        queryFn: () => {
          return fetch("https://api.camijs.com/products?_limit=3").then(res => res.json())
        },
        staleTime: 1000 * 60 * 5 // 5 minutes
      });
    }

    isProductInCart(product) {
      return this.cartItems ? this.cartItems.some(item => item.id === product.id) : false;
    }

    isOutOfStock(product) {
      return product.stock === 0;
    }

    template() {
      if (this.products.status === "pending") {
        return html`<div>Loading...</div>`;
      }

      if (this.products.status === "error") {
        return html`<div>Error: ${this.products.errorDetails.message}</div>`;
      }

      if (this.products && this.products.data) {
        return html`
          <ul>
            ${this.products.data.map(product => html`<li>
              ${product.name} - ${(product.price / 100).toFixed(2)}
              <button @click=${() => CartStore.dispatch('add', product)} ?disabled=${this.isOutOfStock(product)}>
                Add to cart
              </button>
            </li>`)}
          </ul>
        `;
      }
    }
  }

  customElements.define('product-list-component', ProductListElement);

  class CartElement extends ReactiveElement {
    cartItems = [];
    onConnect() {
      CartStore.subscribe(state => {
        this.cartItems = state.cartItems;
      });
    }

    template() {
      return html`
        ${this.cartItems.length > 0 ? html`
          <p>Cart value: ${(this.cartItems.reduce((acc, item) => acc + item.price, 0) / 100).toFixed(2)}</p>
          <ul>
            ${this.cartItems.map(item => html`
              <li>${item.name} - ${(item.price / 100).toFixed(2)} <button @click=${() => CartStore.dispatch('remove', item)}>Remove</button></li>
            `)}
          </ul>
        ` : html`
          <p>Cart is empty</p>
        `}
      `
    }
  }

  customElements.define('cart-component', CartElement);
</script>