Skip to content

calmm-js/karet-shopping-cart

Repository files navigation

Build Status

See also prebuilt site and forkable version in StackBlitz.

To try locally:

git clone https://github.com/calmm-js/karet-shopping-cart.git
cd karet-shopping-cart
npm install

Then open docs/index.html file in your browser.

If you want to edit the code, you can also run npm run watch to auto build when sources are changed. You will need to manually refresh the browser.

Tutorial

Here is how to write the very beginnings of a Shopping Cart UI using

Karet is simple library that allows one to embed Kefir observables into React VDOM. If this tutorial advances at a too fast a pace, then you might want to read a longer introduction to the approach.

Counters are not toys!

So, how does one create a Shopping Cart UI?

Well, of course, the first thing is to write the classic counter component:

const Counter = ({count}) =>
  <span>
    <button onClick={() => count.modify(R.add(-1))}>-</button>
    {count}
    <button onClick={() => count.modify(R.add(+1))}>+</button>
  </span>

The Counter component displays a count, which is supposed to refer to state that contains an integer, and buttons labeled - and + that decrement and increment the count using modify.

As you probably know, a counter component such as the above is a typical first example that the documentation of any respectable front-end framework will give you. Until now you may have mistakenly thought that those are just toys.

Component, remove thyself!

The next thing is to write a component that can remove itself:

const Remove = ({removable}) =>
  <button onClick={() => removable.remove()}>x</button>

The Remove component gives you a button labeled x that calls remove on the removable state given to it.

Lists are simple data structures

Then we write a higher-order component that can display a list of items:

const Items = ({items, Item}) =>
  <div>
    {U.seq(items,
           U.mapElems((item, i) => <Item key={i} item={item}/>))}
  </div>

The Items component is given state named items that is supposed to refer to an array of objects. From that array it then produces an unordered list of Item components, passing them an item that corresponds to an element of the items state array.

Items in a cart

We haven't actually written anything shopping cart specific yet. Let's change that by writing a component for cart items:

const cartCount =
  [L.removable("count"),
   "count",
   L.defaults(0)]

const CartItem = ({item}) =>
  <div>
    <Remove removable={item}/>
    <Counter count={U.view(cartCount, item)}/>
    {U.view("name", item)}
  </div>

The CartItem component is designed to work as Item for the previous Items component. It is a simple component that is given state named item that is supposed to refer to an object containing name and count fields. CartItem uses the previously defined Remove and Counter components. The Remove component is simply passed the item as the removable. The Counter component is given a lensed view of the count. The cartCount lens makes it so that when the count property reaches 0 the whole item is removed.

This is important: By using a simple lens as an adapter, we could plug the previously defined Counter component into the shopping cart state.

If this is the first time you encounter partial lenses, then the definition of cartCount may be difficult to understand, but it is not very complex at all. It works like this:

  • The L.removable("count") part specifies that if the count property is removed, then so should the whole item.
  • The "count" simply selects the count property from the item.
  • The L.defaults(0) part specifies that the value is to be 0 in case there is none and that in case the value 0 is written it should be removed.

This way, when the count reaches 0, the whole item gets removed. After working with partial lenses for some time you will be able to write far more interesting lenses.

Items to put into the cart

We are nearly done! We just need one more component for products:

const productCount = U.lift(item =>
  [L.find(R.whereEq({id: item.id})),
   L.defaults(item),
   "count",
   L.defaults(0),
   L.normalize(R.max(0))])

const ProductItem = cart => ({item}) =>
  <div>
    <Counter count={U.view(productCount(item), cart)}/>
    {U.view("name", item)}
  </div>

The ProductItem component is also designed to work as an Item for the previous Items component. Note that ProductItem actually takes two curried arguments. The first argument cart is supposed to refer to cart state. ProductItem also reuses the Counter component. This time we give it another non-trivial lens. The productCount lens is a parameterized lens that is given an item to put into the cart. We furthermore lift the productCount function to allow the parameter to be a time varying value.

Putting it all together

We now have all the components to put together our shopping cart application. Here is a list of some Finnish delicacies:

const productsData = [
  {id: 1, name: "Sinertävä lenkki 500g"},
  {id: 2, name: "Maksainen laatikko 400g"},
  {id: 3, name: "Maitoa etäisesti muistuttava juoma 0.9l"},
  {id: 4, name: "Festi moka kaffe 500g"},
  {id: 5, name: "Niin hyvä voffeli ettei saa 55g"},
  {id: 6, name: "Suklainen Japanilainen viihdyttäjä 37g"}
]

Then we define an observable that produces the products one-by-one:

const products =
  U.seq(productsData,
        U.map(U.later(1000)),
        U.serially,
        U.foldPast((xs, x) => U.append(x, xs), []))

And, finally, here is our App:

const App = ({state, cart = U.view(["cart", L.define([])], state)}) =>
  <div>
    <h1>Karet (toy) Shopping Cart example</h1>
    <a href="https://github.com/calmm-js/karet-shopping-cart">GitHub</a>
    <div className="panels">
      <div className="panel">
        <h2>Products</h2>
        <Items Item={ProductItem(cart)} items={products}/>
      </div>
      <div className="panel">
        <h2>Shopping Cart</h2>
        <Items Item={CartItem} items={cart}/>
      </div>
    </div>
  </div>

The App above lenses the cart state out of the whole app state and then instantiates the components. Note that we use the higher-order Items component twice with different Item components and different lists of items.

Summary

For the purposes of this example we are done. Here is a summary:

  • We wrote several components such as Counter, Removable and Items that are not specific to the application in any way.

  • Each component is just one function that takes (possibly reactive variables as) parameters and returns VDOM.

  • We composed components together as VDOM expressions.

  • We used Counter and Items twice in different contexts.

  • When using Counter we used lenses to decompose application specific state to match the interface of the component.