Improve the commenting in todomvc

This commit is contained in:
Mike Thompson 2017-07-17 17:09:02 +10:00
parent 4b3da7e4ff
commit 61ee2f18ac
3 changed files with 122 additions and 43 deletions

View File

@ -38,7 +38,9 @@
;;
;; When the application first starts, this will be the value put in app-db
;; Unless, of course, there are todos in the LocalStore (see further below)
;; Look in `core.cljs` for "(dispatch-sync [:initialise-db])"
;; Look in:
;; 1. `core.cljs` for "(dispatch-sync [:initialise-db])"
;; 2. `events.cljs` for the registration of :initialise-db handler
;;
(def default-value ;; what gets put into app-db by default.
@ -54,20 +56,30 @@
;; filter. Just the todos.
;;
(def ls-key "todos-reframe") ;; localstore key
(def ls-key "todos-reframe") ;; localstore key
(defn todos->local-store
"Puts todos into localStorage"
[todos]
(.setItem js/localStorage ls-key (str todos))) ;; sorted-map writen as an EDN map
;; register a coeffect handler which will load a value from localstore
;; To see it used look in events.clj at the event handler for `:initialise-db`
;; -- cofx Registrations -----------------------------------------------------
;; Use `reg-cofx` to register a "coeffect handler" which will inject the todos
;; stored in localstore.
;;
;; To see it used, look in `events.clj` at the event handler for `:initialise-db`.
;; That event handler has the interceptor `(inject-cofx :local-store-todos)`
;; The function registered below will be used to fulfill that request.
;;
;; We must supply a `sorted-map` but in LocalStore it is stored as a `map`.
;;
(re-frame/reg-cofx
:local-store-todos
(fn [cofx _]
"Read in todos from localstore, and process into a map we can merge into app-db."
(assoc cofx :local-store-todos
;; put the localstore todos into the coeffect, under key :local-store-todos
(assoc cofx :local-store-todos ;; read in todos from localstore, and process into a sorted map
(into (sorted-map)
(some->> (.getItem js/localStorage ls-key)
(cljs.reader/read-string) ;; stored as an EDN map.

View File

@ -7,32 +7,62 @@
;; -- Interceptors --------------------------------------------------------------
;; Interceptors are an advanced topic. So, we're plunging into the deep end.
;;
;; There are full tutorials on Interceptors. But I'll try here to get you
;; going enough information so that you can proceed without reading those
;; docs for the moment.
;;
;; Every event handler can be "wrapped" in a chain of interceptors. Each of these
;; interceptors can do things "before" and/or "after" the event handler itself.
;; Think of them like the "middleware" that is often used in web servers.
;; Interceptors are a useful way of handling crosscutting concerns like
;; logging, or debugging, and factoring out commonality.
;;
;; They are also used to "inject" values into the `coeffects` parameter of
;; an event handler, when that handler needs access to certain resources.
;;
;; Yeah, so that's just enough information to get you going. But read the
;; /docs for full information. This is an advanced topic.
;;
;;
(defn check-and-throw
"throw an exception if db doesn't match the spec"
"Throws an exception if `db` doesn't match the Spec `a-spec`."
[a-spec db]
(when-not (s/valid? a-spec db)
(throw (ex-info (str "spec check failed: " (s/explain-str a-spec db)) {}))))
;; Event handlers change state, that's their job. But what happens if there's
;; a bug which corrupts app state in some subtle way? This interceptor is run after
;; each event handler has finished, and it checks app-db against a spec. This
;; a bug in the event handler which corrupts application state in some subtle way?
;; Next, we create an interceptor `check-spec-interceptor`.
;; Later, we use this interceptor in the interceptor chain of all event handlers.
;; When included in the interceptor chain of an event handler, this interceptor
;; runs `check-and-throw` `after` the event handler has finished, checking
;; the contents of `app-db` against a spec.
;; If the event handler messed up `app-db` an exception will be thrown. This
;; helps us detect event handler bugs early.
;; Because all state is held in `app-db`, we are effectively checking the
;; ENTIRE state of the application after each event handler runs.
(def check-spec-interceptor (after (partial check-and-throw :todomvc.db/db)))
;; this interceptor stores todos into local storage
;; we attach it to each event handler which could update todos
;; Part of the TodoMVC challenge is to remember todos in Local Storage.
;; Next, we define an interceptor to help with this challenge.
;; This interceptor runs `after` an event handler, and it stores the
;; current todos into local storage.
;; Later, we include this interceptor into the interceptor chain
;; of all event handlers which modify todos.
(def ->local-store (after todos->local-store))
;; Each event handler can have its own set of interceptors (middleware)
;; But we use the same set of interceptors for all event handlers related
;; to manipulating todos.
;; Each event handler can have its own chain of interceptors.
;; Below we create the interceptor chain shared by all event handlers
;; which manipulate todos.
;; A chain of interceptors is a vector.
(def todo-interceptors [check-spec-interceptor ;; ensure the spec is still valid
(path :todos) ;; 1st param to handler will be the value from this path
->local-store ;; write todos to localstore
(when ^boolean js/goog.DEBUG debug) ;; look in your browser console for debug logs
;; Explanation of `path` and `trimv` is given further below.
(def todo-interceptors [check-spec-interceptor ;; ensure the spec is still valid (after)
(path :todos) ;; 1st param to handler will be the value from this path within db
->local-store ;; write todos to localstore (after)
(when ^boolean js/goog.DEBUG debug) ;; look at the js browser console for debug logs
trim-v]) ;; removes first (event id) element from the event vec
@ -49,42 +79,69 @@
;; -- Event Handlers ----------------------------------------------------------
;; usage: (dispatch [:initialise-db])
(reg-event-fx ;; on app startup, create initial state
;;
;; You'll see this event dispatched in the app's `main` (core.cljs)
;; It's job is to establish initial application state in `app-db`.
;; That means merging:
;; 1. Any todos stored in LocalStore (from the last session of this app)
;; 2. The default initial value
;;
;; Advanced topic: we inject the todos currently stored in LocalStore
;; into the first, coeffect parameter via `(inject-cofx :local-store-todos)`
;; To fully understand how that works, you'll have to review the tutorials.
;; But, if you are interested, look at the bottom of `db.cljs` to see how this is done.
;;
(reg-event-fx ;; part of the re-frame API
:initialise-db ;; event id being handled
[(inject-cofx :local-store-todos) ;; obtain todos from localstore
[(inject-cofx :local-store-todos) ;; <-- advanced: obtain todos from localstore
check-spec-interceptor] ;; after the event handler runs, check that app-db matches the spec
(fn [{:keys [db local-store-todos]} _] ;; the handler being registered
{:db (assoc default-value :todos local-store-todos)})) ;; all hail the new state
;; usage: (dispatch [:set-showing :active])
(reg-event-db ;; this handler changes the todo filter
;; This event is dispatched when the user clicks on the various
;; filter buttons at the bottom of the panel. All, showing, done.
(reg-event-db ;; part of the re-frame API
:set-showing ;; event-id
[check-spec-interceptor]
(fn [db [_ new-filter-kw]]
(assoc db :showing new-filter-kw)))
;; NOTE: here is a rewrite of the event handler above using `path` or `trimv`
;; These interceptors can be interesting and useful, but they are a little advanced
#_(reg-event-db
:set-showing ;; event-id
;; this chain of two interceptors wrap the handler
;; this chain of 3 interceptors wrap the handler. Note use of path and trimv
[check-spec-interceptor (path :showing) trim-v]
;; The event handler
;; Because of the path interceptor above, the 1st parameter to
;; Because of the `path` interceptor above, the 1st parameter to
;; the handler below won't be the entire 'db', and instead will
;; be the value at a certain path within db, namely :showing.
;; Also, the use of the 'trim-v' interceptor means we can omit
;; be the value at the path `[:showing]` within db.
;; Also, the use of the `trim-v` interceptor means we can omit
;; the leading underscore from the 2nd parameter (event vector).
(fn [old-keyword [new-filter-kw]] ;; handler
new-filter-kw)) ;; return new state for the path
;; usage: (dispatch [:add-todo "Finish comments"])
;; usage: (dispatch [:add-todo "a string"])
(reg-event-db ;; given the text, create a new todo
:add-todo
;; The standard set of interceptors, defined above, which we
;; apply to all todos-modifiing event handlers. Looks after
;; apply to all todos-modifying event handlers. Looks after
;; writing todos to local store, etc.
;; NOTE: the interceptors include `path` and `trimv`
todo-interceptors
;; The event handler function.
;; The "path" interceptor in `todo-interceptors` means 1st parameter is :todos
;; The "path" interceptor in `todo-interceptors` means 1st parameter is the
;; value at `:todos` within `db`, rather than the full `db`.
;; And, further, it means the event handler returns just the value to be
;; put into `:todos` and not the entire `db`.
;; So, a path interceptor makea the event handler act more like clojure's `update-in`
(fn [todos [text]]
(let [id (allocate-next-id todos)]
(assoc todos id {:id id :title text :done false}))))

View File

@ -2,25 +2,35 @@
(:require [re-frame.core :refer [reg-sub subscribe]]))
;; -------------------------------------------------------------------------------------
;; Layer 2 (see the Subscriptions Infographic for meaning)
;; Layer 2
;;
;; https://github.com/Day8/re-frame/blob/master/docs/SubscriptionInfographic.md
;;
;; Layer 2 query functions, are "extractors". They simply take from `app-db`
;; and don't do any further computation on the extracted values. That further
;; computation happens in Layer 3.
;; Why? Well Layer 2 subscriptions will rerun every time that `app-db` changes.
;; So for efficiency reasons, we want them to be trivial extractors.
;;
(reg-sub
:showing
(fn [db _] ;; db is the (map) value in app-db
(:showing db))) ;; I repeat: db is a value. Not a ratom. And this fn does not return a reaction, just a value.
:showing ;; usage: (subscribe [:showing])
(fn [db _] ;; db is the (map) value stored in the app-db atom
(:showing db))) ;; extract a value from the application state
;; that `fn` is a pure function
;; Next, the registration of a similar handler is done in two steps.
;; First, we `defn` a pure handler function. Then, we use `reg-sub` to register it.
;; Two steps. This is different to that first registration, above, which was done in one step.
;; Two steps. This is different to that first registration, above, which was done
;; in one step using an anonymous function.
(defn sorted-todos
[db _]
(:todos db))
(reg-sub :sorted-todos sorted-todos)
(reg-sub :sorted-todos sorted-todos) ;; usage: (subscribe [:sorted-todos])
;; -------------------------------------------------------------------------------------
;; Layer 3 (see the infographic for meaning)
;; Layer 3
;;
;; https://github.com/Day8/re-frame/blob/master/docs/SubscriptionInfographic.md
;;
;; A subscription handler is a function which is re-run when its input signals
;; change. Each time it is rerun, it produces a new output (return value).
@ -39,16 +49,16 @@
;; reg-sub allows you to supply:
;;
;; 1. a function which returns the input signals. It can return either a single signal or
;; a vector of signals, or a map of where the values are the signals.
;; a vector of signals, or a map where the values are the signals.
;;
;; 2. a function which does the computation. It takes input values and produces a new
;; derived value.
;;
;; In the two simple examples at the top, we only supplied the 2nd of these functions.
;; But now we are dealing with intermediate nodes, we'll need to provide both fns.
;; But now we are dealing with intermediate (layer 3) nodes, we'll need to provide both fns.
;;
(reg-sub
:todos
:todos ;; usage: (subscribe [:todos])
;; This function returns the input signals.
;; In this case, it returns a single signal.
@ -75,12 +85,12 @@
;; So here we define the handler for another intermediate node.
;; This time the computation involves two input signals.
;; As a result note:
;; - the first function (which returns the signals, returns a 2-vector)
;; - the second function (which is the computation, destructures this 2-vector as its first parameter)
;; - the first function (which returns the signals) returns a 2-vector
;; - the second function (which is the computation) destructures this 2-vector as its first parameter
(reg-sub
:visible-todos
;; signal function
;; signal function - tells us what inputs flow into this node
;; returns a vector of two input signals
(fn [query-v _]
[(subscribe [:todos])
@ -106,7 +116,7 @@
;; It returns one signal, and that signal is app-db itself.
;;
;; So the two simple registrations at the top didn't need to provide a signal-fn,
;; because they operated only on the value in app-db, supplied as 'db' in the 1st arguement.
;; because they operated only on the value in app-db, supplied as 'db' in the 1st argument.
;; -------------------------------------------------------------------------------------
;; SUGAR ?