Skip to content
The front of the store
Clojure JavaScript HTML CSS Shell Dockerfile
Branch: master
Clone or download
Latest commit 2a0b192 Oct 16, 2019
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
container_files Update to hosted Graphite Aug 19, 2019
dev Handle infinite recursion on reset. Jan 31, 2019
externs Adding extern for fbq (facebook analytics) Oct 9, 2019
resources Removing structured data (excluding product data) from non-about-us Oct 7, 2019
src-cljc api is a cljs file. reference it that way Oct 17, 2019
src-cljs Fetch cms data client-side only when we don't have it in state Oct 16, 2019
src/storefront Fetch homepage hero on nav (classic) Oct 16, 2019
test/storefront Implement client side cms fetching and refactor server side cms fetch… Oct 16, 2019
.dir-locals.el WIP: Upgrade to figwheel-main Jun 12, 2019
.gitignore WIP: Upgrade to figwheel-main Jun 12, 2019
.projectile Projectile now indexes, let's ignore some files! Jan 7, 2019
Dockerfile Remove superfluous jvm arg Jun 24, 2019
Dockerfile.test Support build-args and fix docker image to java8 Mar 5, 2019
LICENSE Add License to project.clj Aug 7, 2015
Procfile.dev Fix up procfile Jul 11, 2019
README.md Remove leads from storefront Dec 12, 2018
assert_no_duplicate_event_handlers.sh Ensure no duplicate event handlers in any source file Jul 25, 2018
dev.cljs.edn Renamed Google Places [Autocomplete] library to Google Maps Jul 29, 2019
figwheel-main.edn Do not open a browser when figwheel starts Aug 27, 2019
gulpfile.js Restore sourcemaps Jun 28, 2019
package.json Another attempt and fixing up sourcemaps Jun 21, 2019
project.clj Revert "Increase JSON->EDN processing times for API calls by ~23x" Oct 8, 2019

README.md

Mayvenn Storefront

The front of shop.mayvenn.com.

NOTE: This document represents our goals, not necessarily our current state.

Overview

Storefront is built primarily using ClojureScript and Om, with a smidgen of Clojure.

Directory Structure

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).

Modules

There are 9 modules: core, account, catalog, checkout, dashboard, gallery, home, login.

Core

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

Account

This module contains the code involved in editing a user or stylist's information

Catalog

This module contains the code involved in shopping.

Checkout

This module contains the code involved in checking out.

Dashboard

This module contains the code involved in the stylist dashboard and the cash out now code.

Gallery

This module contains the code involved in stylist gallery

Home

This module contains the code involved in home page and static content

Login

This module contains the code involved in logging in.

Architecture

Storefront's is built around a single application state and 5 event systems. An event is a vector of keywords.

State

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.

Example

(def app-state
 {:order {:number "W123456"}
  :ui    {:other :stuff}})

(get-in app-state keypaths/order-number)

Events

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].

Multimethods

There are many multimethods for which events can have implementations. They are listed here in order of execution.

Transitions
(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

Effects

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.

Query

All components should implement a query defmethod. Query is responsible for transforming data in app-state into the shape that the component needs.

Display

All components should implement a display defmethod. Display is responsible for building the component.

Trackings

Executing Events

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:

  1. Transition
    1. [:navigate]
    2. [:navigate :checkout]
    3. [:navigate :checkout :returning]
    4. [:navigate :checkout :returning :or]
    5. [:navigate :checkout :returning :or :guest]
  2. Effect
    1. [:navigate]
    2. [:navigate :checkout]
    3. [:navigate :checkout :returning]
    4. [:navigate :checkout :returning :or]
    5. [:navigate :checkout :returning :or :guest]
  3. Query
    1. [:navigate :checkout :returning :or :guest]
  4. Display
    1. [:navigate :checkout :returning :or :guest]
  5. Trackings
    1. [:navigate]
    2. [:navigate :checkout]
    3. [:navigate :checkout :returning]
    4. [:navigate :checkout :returning :or]
    5. [: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].

Examples:

Fetching data asynchronously over an API
(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.

Configuring a component to update application state when interacted with
(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.

Server Side Rendering

Questions

  • 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.

License

Copyright (C) Mayvenn, Inc. - All Rights Reserved

You can’t perform that action at this time.