Improvements in newbie documentation

This commit is contained in:
Mike Thompson 2017-07-20 15:10:18 +10:00
parent ed79d8e4ac
commit 3beb7cfaf3
3 changed files with 67 additions and 62 deletions

View File

@ -7,25 +7,20 @@
;; -- Interceptors --------------------------------------------------------------
;; Interceptors are an advanced topic. So, we're plunging into the deep end.
;; Interceptors are an advanced topic. So, we're plunging into the deep end here.
;;
;; 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.
;; There are full tutorials on Interceptors in re-frame's /docs. But to get
;; you going, here's a very high level description of them ...
;;
;; 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.
;; interceptors can do things "before" and/or "after" the event handler is executed.
;; They are like the "middleware" of web servers, wrapping around the "handler".
;; Interceptors are a useful way of factoring out commonality (across event
;; handlers) and looking after cross-cutting concerns like logging or validation.
;;
;; 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
"Throws an exception if `db` doesn't match the Spec `a-spec`."
@ -34,19 +29,19 @@
(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 in the event handler which corrupts application state in some subtle way?
;; Next, we create an interceptor `check-spec-interceptor`.
;; a bug in the event handler and it corrupts application state in some subtle way?
;; Next, we create an interceptor called `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.
;; the value in `app-db` against a spec.
;; If the event handler corrupted tha value in `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 validating the
;; ENTIRE state of the application after each event handler runs. All of it.
(def check-spec-interceptor (after (partial check-and-throw :todomvc.db/db)))
;; Part of the TodoMVC challenge is to remember todos in Local Storage.
;; Part of the TodoMVC challenge is to store 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.
@ -87,34 +82,40 @@
;; 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.
;; If you are interested, look at the bottom of `db.cljs` to see how this is done.
;; into the first, coeffect parameter via use of the interceptor
;; `(inject-cofx :local-store-todos)`
;;
(reg-event-fx ;; part of the re-frame API
:initialise-db ;; event id being handled
[(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
;; To fully understand how this works, you'll have to review the tutorials
;; and look at the bottom of `db.cljs` for the `:local-store-todos` cofx
;; registration.
(reg-event-fx ;; part of the re-frame API
:initialise-db ;; event id being handled
;; the interceptor chain (a vector of interceptors)
[(inject-cofx :local-store-todos) ;; get todos from localstore, and put into coeffects arg
check-spec-interceptor] ;; after the event handler runs, check app-db matches Spec
;; the event handler (function) being registered
(fn [{:keys [db local-store-todos]} _] ;; take 2 vals from coeffects. Ignore event vector itself.
{:db (assoc default-value :todos local-store-todos)})) ;; all hail the new state
;; usage: (dispatch [:set-showing :active])
;; This event is dispatched when the user clicks on the various
;; filter buttons at the bottom of the panel. All, active or done.
;; This event is dispatched when the user clicks on one of the 3
;; filter buttons at the bottom of the display.
(reg-event-db ;; part of the re-frame API
:set-showing ;; event-id
[check-spec-interceptor]
(fn [db [_ new-filter-kw]]
(fn [db [_ new-filter-kw]] ;; new-filter-kw is one of :all, :active or :done
(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 an advanced topic.
;; It will be illuminating to compare the version below with the one above.
;; These interceptors are useful, but they are an advanced topic.
;; It will be illuminating if you compare this rewrite with the original above.
#_(reg-event-db
:set-showing ;; event-id
;; this chain of 3 interceptors wrap the handler. Note use of path and trimv
;; this chain of 3 interceptors wrap the handler. Note use of `path` and `trimv`
[check-spec-interceptor (path :showing) trim-v]
;; The event handler
@ -127,21 +128,21 @@
new-filter-kw)) ;; return new state for the path
;; usage: (dispatch [:add-todo "a string"])
;; usage: (dispatch [:add-todo "a description 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-modifying event handlers. Looks after
;; writing todos to local store, etc.
;; NOTE: the interceptors include `path` and `trimv`
;; use for all todos-modifying event handlers. Looks after
;; writing todos to LocalStore, etc.
;; NOTE: this chain includes `path` and `trimv`
todo-interceptors
;; The event handler function.
;; The "path" interceptor in `todo-interceptors` means 1st parameter is the
;; value at `:todos` within `db`, rather than the full `db`.
;; value at `:todos` path 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`.
;; put into `:todos` path, and not the entire `db`.
;; So, a path interceptor makes the event handler act more like clojure's `update-in`
(fn [todos [text]] ;; because of trimv, the 2nd parameter is NOT [_ text]
(let [id (allocate-next-id todos)]

View File

@ -4,13 +4,13 @@
;; -------------------------------------------------------------------------------------
;; Layer 2
;;
;; https://github.com/Day8/re-frame/blob/master/docs/SubscriptionInfographic.md
;; See 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.
;; Layer 2 query functions, are "extractors". They take from `app-db`
;; and don't do any further computation on the extracted values. Any further
;; computation should happen in Layer 3.
;; Why? It is an efficiency thing. Every Layer 2 subscriptions will rerun anytime
;; that `app-db` changes (in any way). As a result, we want Layer 2 to be trivial.
;;
(reg-sub
:showing ;; usage: (subscribe [:showing])
@ -30,7 +30,7 @@
;; -------------------------------------------------------------------------------------
;; Layer 3
;;
;; https://github.com/Day8/re-frame/blob/master/docs/SubscriptionInfographic.md
;; See 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).
@ -90,8 +90,9 @@
(reg-sub
:visible-todos
;; signal function - tells us what inputs flow into this node
;; returns a vector of two input signals
;; Signal Function
;; Tells us what inputs flow into this node.
;; Returns a vector of two input signals (in this case)
(fn [query-v _]
[(subscribe [:todos])
(subscribe [:showing])])
@ -107,16 +108,20 @@
;; -------------------------------------------------------------------------------------
;; Hey, wait on!!
;;
;; How did those two simple registrations at the top work?
;; How did those two simple Layer 2 registrations at the top work?
;; We only supplied one function in those registrations, not two?
;; Very observant of you, I'm glad you asked.
;; When the signal-returning-fn is omitted, re-sub provides a default,
;; When the signal-returning-fn is omitted, reg-sub provides a default,
;; and it looks like this:
;; (fn [_ _] re-frame.db/app-db)
;; (fn [_ _]
;; re-frame.db/app-db)
;; 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 argument.
;;
;; So that, by the way, is why Layer 2 subscriptions always re-calculate when `app-db`
;; changes - `app-db` is literally their input signal.
;; -------------------------------------------------------------------------------------
;; SUGAR ?

View File

@ -16,27 +16,26 @@
[clojure.set :as set]))
;; This namespace defines the re-frame API
;; When originally writing this re-frame API namespace, we used
;; this technique:
;; (def api-name (deeper.namespace/where-the-defn-is))
;; -- API ---------------------------------------------------------------------
;;
;; Turns out this technique makes it hard:
;; When originally writing this namespace, we used this technique:
;; (def api-name-for-fn deeper.namespace/where-the-defn-is)
;;
;; Turns out, not doing a `defn` in the API itself makes it hard:
;; - to auto-generate API docs
;; - for IDEs to provide code completion on functions in the API.
;; - for IDEs to provide code completion on functions in the API
;;
;; Which is annoying. But there are pros and cons and we haven't yet
;; revisited the decision. So, sorry, in advance. To compensate we've
;; added more nudity to the official docs.
;; -- dispatch ---------------------------------------------------------------
;; -- dispatch ----------------------------------------------------------------
(def dispatch router/dispatch)
(def dispatch-sync router/dispatch-sync)
;; -- subscriptions ----------------------------------------------------------
;; -- subscriptions -----------------------------------------------------------
(def reg-sub subs/reg-sub)
(def subscribe subs/subscribe)
@ -61,7 +60,7 @@
(def clear-cofx (partial registrar/clear-handlers cofx/kind)) ;; think unreg-cofx
;; -- Events -----------------------------------------------------------------
;; -- Events ------------------------------------------------------------------
(defn reg-event-db
"Register the given event `handler` (function) for the given `id`. Optionally, provide