Improve the commenting in todomvc
This commit is contained in:
parent
4b3da7e4ff
commit
61ee2f18ac
|
@ -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.
|
||||
|
|
|
@ -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}))))
|
||||
|
|
|
@ -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 ?
|
||||
|
|
Loading…
Reference in New Issue