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.
Here is how to write the very beginnings of a Shopping Cart UI using
- atoms, and
- lenses, with
- the
karet
and via - the
karet.util
libraries.
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.
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.
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.
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.
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 thecount
property is removed, then so should the whole item. - The
"count"
simply selects thecount
property from the item. - The
L.defaults(0)
part specifies that the value is to be0
in case there is none and that in case the value0
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.
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.
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
.
For the purposes of this example we are done. Here is a summary:
-
We wrote several components such as
Counter
,Removable
andItems
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
andItems
twice in different contexts. -
When using
Counter
we used lenses to decompose application specific state to match the interface of the component.