Add more comments to todomvc

This commit is contained in:
Mike Thompson 2016-12-28 22:42:45 +11:00
parent 209470e761
commit 4bba231548
5 changed files with 87 additions and 63 deletions

View File

@ -14,10 +14,15 @@
;; -- Debugging aids ----------------------------------------------------------
(devtools/install!) ;; we love https://github.com/binaryage/cljs-devtools
(enable-console-print!) ;; so println writes to console.log
(enable-console-print!) ;; so that println writes to `console.log`
;; -- Routes and History ------------------------------------------------------
;; Although we use the secretary library below, that's mostly a historical
;; accident. You might also consider using:
;; - https://github.com/DomKM/silk
;; - https://github.com/juxt/bidi
;; We don't have a strong opinion.
;;
(defroute "/" [] (dispatch [:set-showing :all]))
(defroute "/:filter" [filter] (dispatch [:set-showing (keyword filter)]))
@ -29,10 +34,23 @@
;; -- Entry Point -------------------------------------------------------------
;; Within ../../resources/public/index.html you'll see this code
;; window.onload = function () {
;; todomvc.core.main();
;; }
;; So this is the entry function that kicks off the app once the HTML is loaded.
;;
(defn ^:export main
[]
;; Put an initial value into app-db.
;; The event handler for `:initialise-db` can be found in `events.cljs`
;; Using the sync version of dispatch means that value is in
;; place before we go onto the next step.
(dispatch-sync [:initialise-db])
(reagent/render [todomvc.views/todo-app]
;; Render the UI into the HTML's <div id="app" /> element
;; The view function `todomvc.views/todo-app` is the
;; root view for the entire UI.
(reagent/render [todomvc.views/todo-app] ;;
(.getElementById js/document "app")))

View File

@ -38,7 +38,7 @@
;;
;; When the application first starts, this will be the value put in app-db
;; Unless, or course, there are todos in the LocalStore (see further below)
;; Look in core.cljs for "(dispatch-sync [:initialise-db])"
;; Look in `core.cljs` for "(dispatch-sync [:initialise-db])"
;;
(def default-value ;; what gets put into app-db by default.
@ -50,16 +50,19 @@
;;
;; Part of the todomvc challenge is to store todos in LocalStorage, and
;; on app startup, reload the todos from when the program was last run.
;; But we are not to load the setting for the "showing" filter. Just the todos.
;; But the challenge stipulates to NOT load the setting for the "showing"
;; 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
(.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`
(re-frame/reg-cofx
:local-store-todos
(fn [cofx _]

View File

@ -8,12 +8,9 @@
;; -- Interceptors --------------------------------------------------------------
;;
;; XXX Add URL for docs here
;; XXX figure out first time spec error
;; XXX seems to be a bug if you refresh the page, and other than "All" is selected.
(defn check-and-throw
"throw an exception if db doesn't match the spec."
"throw an exception if db doesn't match the spec"
[a-spec db]
(when-not (s/valid? a-spec db)
(throw (ex-info (str "spec check failed: " (s/explain-str a-spec db)) {}))))
@ -28,8 +25,10 @@
;; we attach it to each event handler which could update todos
(def ->local-store (after todos->local-store))
;; the chain of interceptors we use for all handlers that manipulate todos
;; Each event handler can have its own set of interceptors (middleware)
;; But we use the same set of interceptors for all event habdlers related
;; to manipulating 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
@ -49,22 +48,23 @@
;; -- Event Handlers ----------------------------------------------------------
;; XXX make localstore a coeffect interceptor
;; usage: (dispatch [:initialise-db])
;; usage: (dispatch [:initialise-db])
(reg-event-fx ;; on app startup, create initial state
:initialise-db ;; event id being handled
[(inject-cofx :local-store-todos)
[(inject-cofx :local-store-todos) ;; 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])
;; usage: (dispatch [:set-showing :active])
(reg-event-db ;; this handler changes the todo filter
:set-showing ;; event-id
[check-spec-interceptor (path :showing) trim-v] ;; this collection of interceptors wrap the handler
;; this chain of two interceptors wrap the handler
[check-spec-interceptor (path :showing) trim-v]
;; The event handler
;; 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.
@ -74,11 +74,18 @@
new-filter-kw)) ;; return new state for the path
;; usage: (dispatch [:add-todo "Finish comments"])
;; usage: (dispatch [:add-todo "Finish comments"])
(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
;; writing todos to local store, etc.
todo-interceptors
(fn [todos [text]] ;; the "path" interceptor in `todo-interceptors` means 1st parameter is :todos
;; The event handler function.
;; The "path" interceptor in `todo-interceptors` means 1st parameter is :todos
(fn [todos [text]]
(let [id (allocate-next-id todos)]
(assoc todos id {:id id :title text :done false}))))

View File

@ -2,18 +2,18 @@
(:require [re-frame.core :refer [reg-sub subscribe]]))
;; -------------------------------------------------------------------------------------
;; Layer 2 (see the infographic for meaning)
;; Layer 2 (see the Subscriptions Infographic for meaning)
;;
(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.
;; so that `fn` is a pure function
;; 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 the first registration, which was done in one step.
;; Two steps. This is different to that first registration, above, which was done in one step.
(defn sorted-todos
[db _]
(:todos db))
@ -79,10 +79,14 @@
;; - the second function (which is the computation, destructures this 2-vector as its first parameter)
(reg-sub
:visible-todos
(fn [query-v _] ;; returns a vector of two signals.
;; signal function
;; returns a vector of two input signals
(fn [query-v _]
[(subscribe [:todos])
(subscribe [:showing])])
;; computation function
(fn [[todos showing] _] ;; that 1st parameter is a 2-vector of values
(let [filter-fn (case showing
:active (complement :done)
@ -109,8 +113,6 @@
;; Now for some syntactic sugar...
;; The purpose of the sugar is to remove boilerplate noise. To distill to the essential
;; in 90% of cases.
;; Is this a good idea?
;; If it is a good idea, is this good syntax?
;; Because it is so common to nominate 1 or more input signals,
;; reg-sub provides some macro sugar so you can nominate a very minimal
;; vector of input signals. The 1st function is not needed.

View File

@ -49,42 +49,38 @@
(defn task-list
[]
(let [visible-todos (subscribe [:visible-todos])
all-complete? (subscribe [:all-complete?])]
(fn []
(let [visible-todos @(subscribe [:visible-todos])
all-complete? @(subscribe [:all-complete?])]
[:section#main
[:input#toggle-all
{:type "checkbox"
:checked @all-complete?
:on-change #(dispatch [:complete-all-toggle (not @all-complete?)])}]
:checked all-complete?
:on-change #(dispatch [:complete-all-toggle (not all-complete?)])}]
[:label
{:for "toggle-all"}
"Mark all as complete"]
[:ul#todo-list
(for [todo @visible-todos]
^{:key (:id todo)} [todo-item todo])]])))
(for [todo visible-todos]
^{:key (:id todo)} [todo-item todo])]]))
(defn footer-controls
[]
(let [footer-stats (subscribe [:footer-counts])
showing (subscribe [:showing])]
(fn []
(let [[active done] @footer-stats
a-fn (fn [filter-kw txt]
[:a {:class (when (= filter-kw @showing) "selected")
(let [[active done] @(subscribe [:footer-counts])
showing @(subscribe [:showing])
a-fn (fn [filter-kw txt]
[:a {:class (when (= filter-kw showing) "selected")
:href (str "#/" (name filter-kw))} txt])]
[:footer#footer
[:span#todo-count
[:strong active] " " (case active 1 "item" "items") " left"]
[:ul#filters
[:li (a-fn :all "All")]
[:li (a-fn :active "Active")]
[:li (a-fn :done "Completed")]]
(when (pos? done)
[:button#clear-completed {:on-click #(dispatch [:clear-completed])}
"Clear completed"])]))))
[:footer#footer
[:span#todo-count
[:strong active] " " (case active 1 "item" "items") " left"]
[:ul#filters
[:li (a-fn :all "All")]
[:li (a-fn :active "Active")]
[:li (a-fn :done "Completed")]]
(when (pos? done)
[:button#clear-completed {:on-click #(dispatch [:clear-completed])}
"Clear completed"])]))
(defn task-entry
@ -99,13 +95,11 @@
(defn todo-app
[]
(let [todos (subscribe [:todos])]
(fn []
[:div
[:section#todoapp
[task-entry]
(when (seq @todos)
[task-list])
[footer-controls]]
[:footer#info
[:p "Double-click to edit a todo"]]])))
[:div
[:section#todoapp
[task-entry]
(when (seq @(subscribe [:todos]))
[task-list])
[footer-controls]]
[:footer#info
[:p "Double-click to edit a todo"]]])