Split todomvc into bite sized namespaces

This commit is contained in:
mike-thompson-day8 2015-03-03 13:15:44 +11:00
parent 48e058f6dc
commit 1d92bd7b3c
5 changed files with 261 additions and 271 deletions

View File

@ -1,287 +1,22 @@
(ns todomvc.core
(:require-macros [reagent.ratom :refer [reaction]])
(:require [reagent.core :as reagent :refer [atom]]
[re-frame.core :refer [register-pure-handler
register-sub
subscribe
dispatch
path trim-v debug]]))
[re-frame.core :refer [dispatch]]
[todomvc.handlers]
[todomvc.subs]
[todomvc.views]))
;; TODOs
;; Get preoject.cljs up to speed `lein run` lein debug`
;; split into files view, handlers, subs, middleware
;; load todos off localstorage via merge ... and write back
;; Show off debugging capabiliteis
;; Add Prismatic schema - modules called state
;; add middleware to save to local storage
;; -- Helpers -------------
(enable-console-print!)
(defn next-id
[todos]
(if (empty? todos)
0
(inc (apply max (keys todos))))) ;; hillariously inefficient, but yeah
(defn filter-fn-for
[showing-kw]
(case showing-kw
:active (complement :done)
:done :done
:all identity))
(defn completed-count
"return the count of todos which have a :done of true"
[todos]
(count (filter :done (vals todos))))
;; -- Middleware -------------
;;
;; Handlers can be wrapped in middleware.
;;
;; Below we are composing 3 pieces of middleware (into one unit)
;; - we want an undo checkpoint on each change
;; - we want our handler to be be given the value at :todos
;; rather than the root of the map.
;; - for asthetic reasons, we want to strip the leding event
;; id on the event-vector, so only the real params are
;; passed into handlers.
;;
;; Debug Middleware:
;; - in debug we always want to write events to console
;; - in debug, after each handler has run, we want to see how `app-db` has changed.
;;
;; These two pieces of middleware make debugging a delight.
;; State changes ONLY ever happen via handlers. So being able to
;; monitor their effects is gold. And then turn it off pnce we get
;; into production.
;;
;; Middleware means our handlers are pure and simple.
;;
(def todo-middleware [(path [:todos]) debug trim-v])
;; -- Handlers -------------
;; we dispatch this event on program startup - responsible for initialising-db
(register-pure-handler
:initialise-db
[debug] ;; middleware
(fn
[_ _]
{:todos (sorted-map)
:showing :all}))
(register-pure-handler
:set-showing
[debug trim-v] ;; middleware
(fn
[db [filter-kw]]
(assoc db :showing filter-kw)))
(register-pure-handler
:add-todo
todo-middleware
(fn
[todos [text]]
(let [id (next-id todos)]
(assoc todos id {:id id :title text :done false}))))
(register-pure-handler
:complete-all-toggle
todo-middleware
(fn
[todos []]
(let [val (every? (comp true? :done) (vals todos))]
(println "todos " todos)
(println "val " val)
(reduce #(assoc-in %1 [%2 :done] val)
todos
(keys todos)))))
(register-pure-handler
:toggle-done
todo-middleware
(fn
[todos [id]]
(update-in todos [id :done] not)))
(register-pure-handler
:save
todo-middleware
(fn
[todos [id title]]
(assoc-in todos [id :title] title)))
(register-pure-handler
:delete-todo
todo-middleware
(fn
[todos [id]]
(dissoc todos id)))
(register-pure-handler
:clear-completed
todo-middleware
(fn
[todos _]
(->> (vals todos)
(filter :done)
(map :id)
(reduce dissoc todos))))
;; -- Subscriptions ---------
(register-sub
:initialised?
(fn
[db _]
(reaction (not= {} @db))))
(register-sub
:visible-todos
(fn
[db _]
(reaction (filter (filter-fn-for (:showing @db))
(vals (:todos @db))))))
(register-sub
:completed-count
(fn
[db _]
(reaction (completed-count (:todos @db)))))
#_(register-sub
:footer-stats
(fn
[db _]
(let [todos (reaction (:todos @db))
completed-count (subscribe [:completed-count])
active-count (reaction (- (count (vals @todos)) @completed-count))
showing (reaction (:showing @db))]
(reaction
[@active-count @completed-count @showing])))) ;; tuple
(register-sub
:footer-stats
(fn
[db _]
(reaction
(let [todos (:todos @db)
completed-count (completed-count todos)
active-count (- (count todos) completed-count)
showing (:showing db)]
[active-count completed-count showing])))) ;; tuple
;; -- Components ---------
(defn todo-input [{:keys [title on-save on-stop]}]
(let [val (atom title)
stop #(do (reset! val "")
(if on-stop (on-stop)))
save #(let [v (-> @val str clojure.string/trim)]
(if-not (empty? v) (on-save v))
(stop))]
(fn [props]
[:input (merge props
{:type "text"
:value @val
:on-blur save
:on-change #(reset! val (-> % .-target .-value))
:on-key-down #(case (.-which %)
13 (save)
27 (stop)
nil)})])))
(def todo-edit (with-meta todo-input
{:component-did-mount #(.focus (reagent/dom-node %))}))
(defn stats-footer
[]
(let [footer-stats (subscribe [:footer-stats])]
(fn []
(let [[active done filter] @footer-stats
props-for (fn [filter-kw]
{:class (if (= filter-kw filter) "selected")
:on-click #(dispatch [:set-showing filter-kw])})]
(println active done filter)
[:footer#footer
[:div
[:span#todo-count
[:strong active] " " (case active 1 "item" "items") " left"]
[:ul#filters
[:li [:a (props-for :all) "All"]]
[:li [:a (props-for :active) "Active"]]
[:li [:a (props-for :done) "Completed"]]]
(when (pos? done)
[:button#clear-completed {:on-click #(dispatch [:clear-completed])}
"Clear completed " done])]]))))
(defn todo-item
[]
(let [editing (atom false)]
(fn [{:keys [id done title]}]
[:li {:class (str (if done "completed ")
(if @editing "editing"))}
[:div.view
[:input.toggle {:type "checkbox" :checked done
:on-change #(dispatch [:toggle-done id])}]
[:label {:on-double-click #(reset! editing true)} title]
[:button.destroy {:on-click #(dispatch [:delete-todo id])}]]
(when @editing
[todo-edit {:class "edit" :title title
:on-save #(dispatch [:save id %])
:on-stop #(reset! editing false)}])])))
(defn todo-list
[visible-todos]
[:ul#todo-list
(for [todo @visible-todos]
^{:key (:id todo)} [todo-item todo])])
(defn todo-app
[]
(let [visible-todos (subscribe [:visible-todos])
completed-count (subscribe [:completed-count])]
(fn []
[:div
[:section#todoapp
[:header#header
[:h1 "todos"]
[todo-input {:id "new-todo"
:placeholder "What needs to be done?"
:on-save #(dispatch [:add-todo %])}]]
(when-not (empty? @visible-todos)
[:div
[:section#main
[:input#toggle-all
{:type "checkbox"
:checked (pos? @completed-count)
:on-change #(dispatch [:complete-all-toggle])}]
[:label {:for "toggle-all"} "Mark all as complete"]
[todo-list visible-todos]]
[stats-footer]])]
[:footer#info
[:p "Double-click to edit a todo"]]])))
(defn main-panel
[]
(let [initialised? (subscribe [:initialised?])]
(fn []
(if @initialised?
[todo-app]
[:div "Loading ...."]))))
(defn ^:export main
[]
(dispatch [:initialise-db])
(reagent/render [main-panel] (js/document.getElementById "app")))
;; you must always return the db
(reagent/render [todomvc.views/top-panel]
(.getElementById js/document "app")))

View File

@ -0,0 +1,7 @@
(ns todomvc.db)
(def default-initial-state
{:todos (sorted-map) ;; todo ids are the keys (for sort)
:showing :all ;; one of :all :done or :active
})

View File

@ -0,0 +1,85 @@
(ns todomvc.handlers
(:require [re-frame.core :refer [register-pure-handler
path
trim-v
debug]]))
;; -- Middleware --------------------------------------------------------------
;; To be used for handlers operating solely on todos
;;
(def todo-middleware [(path [:todos]) debug trim-v])
;; -- Helpers -----------------------------------------------------------------
(defn next-id
[todos]
(if (empty? todos)
0
(inc (apply max (keys todos))))) ;; hillariously inefficient, but yeah
;; -- Handlers ----------------------------------------------------------------
(register-pure-handler ;; disptached to on program startup
:initialise-db ;; event id being handled
(fn [_ _] ;; the handler
default-initial-state)) ;; all hail the new state
(register-pure-handler ;; disptached to when
:set-showing ;; event-id
[debug trim-v] ;; middleware (wraps the handler)
(fn ;; handler
[db [filter-kw]]
(assoc db :showing filter-kw)))
(register-pure-handler ;; given only the text, create a new todo
:add-todo
todo-middleware ;; only 'todo' part of db provided
(fn [todos [text]]
(let [id (next-id todos)]
(assoc todos id {:id id :title text :done false}))))
(register-pure-handler
:complete-all-toggle
todo-middleware
(fn [todos]
(let [val (not-every? :done (vals todos))]
(reduce #(assoc-in %1 [%2 :done] val)
todos
(keys todos)))))
(register-pure-handler
:toggle-done
todo-middleware
(fn [todos [id]]
(update-in todos [id :done] not)))
(register-pure-handler
:save
todo-middleware
(fn [todos [id title]]
(assoc-in todos [id :title] title)))
(register-pure-handler
:delete-todo
todo-middleware
(fn [todos [id]]
(dissoc todos id)))
(register-pure-handler
:clear-completed
todo-middleware
(fn [todos _]
(->> (vals todos) ;; remove all todos where :done is true
(filter :done)
(map :id)
(reduce dissoc todos))))

View File

@ -0,0 +1,57 @@
(ns todomvc.subs
(:require-macros [reagent.ratom :refer [reaction]])
(:require [re-frame.core :refer [register-sub
subscribe ]]))
;; -- Helpers -----------------------------------------------------------------
(defn filter-fn-for
[showing-kw]
(case showing-kw
:active (complement :done)
:done :done
:all identity))
(defn completed-count
"return the count of todos where :done is true"
[todos]
(count (filter :done (vals todos))))
;; -- Subscriptions -----------------------------------------------------------
(register-sub ;; has app-db been initialised yet?
:initialised? ;; usage: (subscribe [:initialised?])
(fn [db _]
(reaction (not (empty? @db)))))
(register-sub
:visible-todos
(fn [db _]
(reaction
(let [filter-fn (filter-fn-for (:showing @db))
todos (:todos @db)]
(filter filter-fn todos)))))
(register-sub
:completed-count
(fn [db _]
(reaction (completed-count (:todos @db)))))
(register-sub
:footer-stats
(fn
[db _]
(reaction
(let [todos (:todos @db)
completed-count (completed-count todos)
active-count (- (count todos) completed-count)
showing (:showing db)]
[active-count completed-count showing])))) ;; tuple

View File

@ -0,0 +1,106 @@
(ns todomvc.views
(:require [reagent.core :as reagent :refer [atom]]
[re-frame.core :refer [subscribe dispatch]]))
(defn todo-input [{:keys [title on-save on-stop]}]
(let [val (atom title)
stop #(do (reset! val "")
(if on-stop (on-stop)))
save #(let [v (-> @val str clojure.string/trim)]
(if-not (empty? v) (on-save v))
(stop))]
(fn [props]
[:input (merge props
{:type "text"
:value @val
:on-blur save
:on-change #(reset! val (-> % .-target .-value))
:on-key-down #(case (.-which %)
13 (save)
27 (stop)
nil)})])))
(def todo-edit (with-meta todo-input
{:component-did-mount #(.focus (reagent/dom-node %))}))
(defn stats-footer
[]
(let [footer-stats (subscribe [:footer-stats])]
(fn []
(let [[active done filter] @footer-stats
props-for (fn [filter-kw]
{:class (if (= filter-kw filter) "selected")
:on-click #(dispatch [:set-showing filter-kw])})]
(println active done filter)
[:footer#footer
[:div
[:span#todo-count
[:strong active] " " (case active 1 "item" "items") " left"]
[:ul#filters
[:li [:a (props-for :all) "All"]]
[:li [:a (props-for :active) "Active"]]
[:li [:a (props-for :done) "Completed"]]]
(when (pos? done)
[:button#clear-completed {:on-click #(dispatch [:clear-completed])}
"Clear completed " done])]]))))
(defn todo-item
[]
(let [editing (atom false)]
(fn [{:keys [id done title]}]
[:li {:class (str (if done "completed ")
(if @editing "editing"))}
[:div.view
[:input.toggle {:type "checkbox" :checked done
:on-change #(dispatch [:toggle-done id])}]
[:label {:on-double-click #(reset! editing true)} title]
[:button.destroy {:on-click #(dispatch [:delete-todo id])}]]
(when @editing
[todo-edit {:class "edit"
:title title
:on-save #(dispatch [:save id %])
:on-stop #(reset! editing false)}])])))
(defn todo-list
[visible-todos]
[:ul#todo-list
(for [todo @visible-todos]
^{:key (:id todo)} [todo-item todo])])
(defn todo-app
[]
(let [visible-todos (subscribe [:visible-todos])
completed-count (subscribe [:completed-count])]
(fn []
[:div
[:section#todoapp
[:header#header
[:h1 "todos"]
[todo-input {:id "new-todo"
:placeholder "What needs to be done?"
:on-save #(dispatch [:add-todo %])}]]
(when-not (empty? @visible-todos)
[:div
[:section#main
[:input#toggle-all
{:type "checkbox"
:checked (pos? @completed-count)
:on-change #(dispatch [:complete-all-toggle])}]
[:label {:for "toggle-all"} "Mark all as complete"]
[todo-list visible-todos]]
[stats-footer]])]
[:footer#info
[:p "Double-click to edit a todo"]]])))
(defn top-panel
[]
(let [initialised? (subscribe [:initialised?])]
(fn []
(if @initialised?
[todo-app]
[:div "Loading ...."]))))