The front of shop.mayvenn.com.
Storefront is built primarily using ClojureScript and Om, with a smidgen of Clojure.
Storefront's source is first split into three src directories: src-clj
, src-cljs
, src-cljc
.
Naturally, these dirs correspond to the language files contained within them.
The differences between the three files will be talked about more in a later section,
for now let's refer to the top level as src*
.
The next layer of directories corelates to modules. A module generally corresponds to a single feature or logical area of storefront. More on those in a bit.
Inside each module, there are components (representing a single react component of the site, both appearance and behavior), hooks (direct integration with javascript), accessors (transformation functions for commonly used datastructure), a single routes file (which defines that module's routes) and a core file (which simply imports all of the components for that module to make importing all cljs components easier).
There are 9 modules: core, account, catalog, checkout, dashboard, gallery, home, login.
This is the frame of storefront. It is the 'glue' which ties all of the modules together and it contains some utilities which are necessary across all modules. If code needs to be in more than one module, it probably goes into this module
This module contains the code involved in editing a user or stylist's information
This module contains the code involved in shopping.
This module contains the code involved in checking out.
This module contains the code involved in the stylist dashboard and the cash out now code.
This module contains the code involved in stylist gallery
This module contains the code involved in home page and static content
This module contains the code involved in logging in.
Storefront's is built around a single application state and 5 event systems. An event is a vector of keywords.
Storefront's application state (let's call it app-state) is a single om atom containing nested maps.
It is dereferenced before it is in a usuable context, ie event handling code described below.
We set and access values through keypaths
which are vectors of keywords.
For example, say you want to get the ID number of an order.
That information is stored in the order, naturally.
You can use the keypaths/order-number along with get-in to fetch that value.
(def app-state
{:order {:number "W123456"}
:ui {:other :stuff}})
(get-in app-state keypaths/order-number)
Events are evaluated in a cascading manner, for example, examine the event navigate-checkout-returning-or-guest
.
It evaluates to [:navigate :checkout :returning :or :guest]
.
There are many multimethods for which events can have implementations. They are listed here in order of execution.
(defmulti transition-state
(fn [dispatch event arguments app-state]
dispatch))
If a given event has a transition defmethod registered, then that code will execute and update application state. It should not be side-effectful and it must always return the application's state
If a given event has a effect defmethod registered, then that code will execute perform side-effectful operations such as API calls, external library operations or interactions with cookies. It should not have any side effects and it must always return the application's state. Effects can also dispatch further events.
All components should implement a query defmethod. Query is responsible for transforming data in app-state into the shape that the component needs.
All components should implement a display defmethod. Display is responsible for building the component.
As the function handle-message processes the event for most event multimethods,
it executes left to right, building as it goes.
The exceptions are the Query and Display steps (we can only render one component for a given place at a time!).
handle-message
finishes all event steps for a given defmethod before continuing.
So, it would execute the following events:
- Transition
[:navigate]
[:navigate :checkout]
[:navigate :checkout :returning]
[:navigate :checkout :returning :or]
[:navigate :checkout :returning :or :guest]
- Effect
[:navigate]
[:navigate :checkout]
[:navigate :checkout :returning]
[:navigate :checkout :returning :or]
[:navigate :checkout :returning :or :guest]
- Query
[:navigate :checkout :returning :or :guest]
- Display
[:navigate :checkout :returning :or :guest]
- Trackings
[:navigate]
[:navigate :checkout]
[:navigate :checkout :returning]
[:navigate :checkout :returning :or]
[:navigate :checkout :returning :or :guest]
Note that not all intermediate events need to have defmultis implemented.
Some examples would be [:navigate :checkout :returning]
and [:navigate :checkout :returning :or]
.
(defn get-promotions
[cache]
(cache-req
cache
GET
"/promotions"
request-keys/get-promotions
{:handler #(messages/handle-message events/api-success-promotions %)}))
(defmethod transition-state events/api-success-promotion
[dispatch event {:keys [promotions]} app-state]
(assoc-in app-state keypaths/promotions promotions))
Above we have defined the function get-promotions
to fetch the current
promotions from a backend service.
Attached is a handler that will run once a response is received. The handler places
api-success-promotions
into the event queue along with the response from the
api request.
The transition-state defmethod registered for the api-success-promotion
event
receives the response and places the returned promotions into the appropriate place in app-state.
(defn text-field [app-state keypath]
[:input
{:type "text"
:value (get-in app-state keypath)
:on-change (fn [^js/Event e]
(handle-message events/control-change-state
{:keypath keypath
:value (.. e -target -value)}))}])
(defmethod transition-state events/control-change-state
[dispatch event {:keys [keypath value] :as arguments} app-state]
(assoc-in app-state keypath (if (fn? value) (value) value)))
In the above example, we can see a simple function which takes the state of the application and a keypath that points to a value in app state and returns a html data structure. Before it can be used, it must used in an om component.
The value of the text field is not directly manipulated by the text field.
Instead, the on-change fn dispatches a control-change-state
event.
The transition multimethod for the control-change-state
event places the value
into app-state causing the text-field to re-render with the newly inserted value in app-state.
- Where are the tests?
- The vast majority of Storefront's tests are in a different project. This was done because they are integration tests and test more than just storefront. There are some tests for the server side handler.
- Can I run this?
- Sure, but you will need to setup up some external dependencies, such as an API server. We hope this project serves as a reference project moreso than generic ecommerce solution.
Copyright (C) Mayvenn, Inc. - All Rights Reserved