Split todomvc into bite sized namespaces
This commit is contained in:
parent
48e058f6dc
commit
1d92bd7b3c
|
@ -1,287 +1,22 @@
|
||||||
(ns todomvc.core
|
(ns todomvc.core
|
||||||
(:require-macros [reagent.ratom :refer [reaction]])
|
|
||||||
(:require [reagent.core :as reagent :refer [atom]]
|
(:require [reagent.core :as reagent :refer [atom]]
|
||||||
[re-frame.core :refer [register-pure-handler
|
[re-frame.core :refer [dispatch]]
|
||||||
register-sub
|
[todomvc.handlers]
|
||||||
subscribe
|
[todomvc.subs]
|
||||||
dispatch
|
[todomvc.views]))
|
||||||
path trim-v debug]]))
|
|
||||||
|
|
||||||
;; TODOs
|
;; TODOs
|
||||||
;; Get preoject.cljs up to speed `lein run` lein debug`
|
;; Get preoject.cljs up to speed `lein run` lein debug`
|
||||||
;; split into files view, handlers, subs, middleware
|
;; split into files view, handlers, subs, middleware
|
||||||
;; load todos off localstorage via merge ... and write back
|
;; load todos off localstorage via merge ... and write back
|
||||||
;; Show off debugging capabiliteis
|
|
||||||
;; Add Prismatic schema - modules called state
|
;; Add Prismatic schema - modules called state
|
||||||
;; add middleware to save to local storage
|
;; add middleware to save to local storage
|
||||||
|
|
||||||
;; -- Helpers -------------
|
|
||||||
|
|
||||||
(enable-console-print!)
|
(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
|
(defn ^:export main
|
||||||
[]
|
[]
|
||||||
(dispatch [:initialise-db])
|
(dispatch [:initialise-db])
|
||||||
(reagent/render [main-panel] (js/document.getElementById "app")))
|
(reagent/render [todomvc.views/top-panel]
|
||||||
|
(.getElementById js/document "app")))
|
||||||
;; you must always return the db
|
|
||||||
|
|
|
@ -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
|
||||||
|
})
|
|
@ -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))))
|
|
@ -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
|
||||||
|
|
|
@ -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 ...."]))))
|
||||||
|
|
Loading…
Reference in New Issue