Edit recipes + major refactoring
This commit is contained in:
parent
f76eba6829
commit
81299b43dd
11 changed files with 664 additions and 509 deletions
|
@ -23,7 +23,7 @@
|
|||
:output-to "public/js/grub_dev.js"
|
||||
;:source-map "public/js/grub_dev.js.map"
|
||||
:optimizations :whitespace
|
||||
:pretty-print false}}
|
||||
:pretty-print true}}
|
||||
:prod {:source-paths ["src/cljs"]
|
||||
:compiler {:output-to "public/js/grub.js"
|
||||
:optimizations :simple}}}}
|
||||
|
|
|
@ -14,6 +14,10 @@ h3 {
|
|||
clear: right;
|
||||
}
|
||||
|
||||
.recipe-list {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
@ -84,6 +88,11 @@ tr:hover .grub-close {
|
|||
color: default;
|
||||
}
|
||||
|
||||
.panel-body {
|
||||
padding: 10px;
|
||||
padding-top: 0px;
|
||||
}
|
||||
|
||||
.recipe-panel {
|
||||
padding: 0px;
|
||||
margin-bottom: -1px;
|
||||
|
@ -110,14 +119,14 @@ tr:hover .grub-close {
|
|||
outline: none;
|
||||
}
|
||||
|
||||
.recipe-done-btn {
|
||||
.recipe-btn {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.recipe-steps {
|
||||
.recipe-grubs {
|
||||
}
|
||||
|
||||
.recipe-steps-input {
|
||||
.recipe-grubs-input {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
transition: none;
|
||||
|
@ -125,13 +134,9 @@ tr:hover .grub-close {
|
|||
resize: none;
|
||||
}
|
||||
|
||||
.recipe-steps-input:focus {
|
||||
.recipe-grubs-input:focus {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
transition: none;
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
#new-recipe {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
|
|
@ -12,36 +12,38 @@
|
|||
|
||||
(defmulti handle-event :event :default :unknown-event)
|
||||
|
||||
(defmethod handle-event :add [event]
|
||||
(defmethod handle-event :add-grub [event]
|
||||
(let [grub (-> event
|
||||
(select-keys [:_id :grub :completed]))]
|
||||
(mc/insert grub-collection grub)))
|
||||
|
||||
(defmethod handle-event :complete [event]
|
||||
(defmethod handle-event :complete-grub [event]
|
||||
(mc/update grub-collection
|
||||
{:_id (:_id event)}
|
||||
{mo/$set {:completed true}}))
|
||||
|
||||
(defmethod handle-event :uncomplete [event]
|
||||
(defmethod handle-event :uncomplete-grub [event]
|
||||
(mc/update grub-collection
|
||||
{:_id (:_id event)}
|
||||
{mo/$set {:completed false}}))
|
||||
|
||||
(defmethod handle-event :update [event]
|
||||
(defmethod handle-event :update-grub [event]
|
||||
(mc/update grub-collection
|
||||
{:_id (:_id event)}
|
||||
{mo/$set {:grub (:grub event)}}))
|
||||
|
||||
(defmethod handle-event :delete [event]
|
||||
(mc/remove grub-collection {:_id (:_id event)}))
|
||||
|
||||
(defmethod handle-event :clear-all [event]
|
||||
(defmethod handle-event :clear-all-grubs [event]
|
||||
(clear-grubs))
|
||||
|
||||
(defmethod handle-event :add-recipe [event]
|
||||
(let [recipe (select-keys event [:_id :name :steps])]
|
||||
(mc/insert recipe-collection recipe)))
|
||||
|
||||
(defmethod handle-event :update-recipe [event]
|
||||
(mc/update recipe-collection
|
||||
{:_id (:_id event)}
|
||||
{mo/$set {:name (:name event) :grubs (:grubs event)}}))
|
||||
|
||||
(defmethod handle-event :unknown-event [event]
|
||||
(println "Cannot handle unknown event:" event))
|
||||
|
||||
|
@ -50,7 +52,7 @@
|
|||
sorted-grubs (sort-by :_id (vec grubs))
|
||||
events (map (fn [g] (-> g
|
||||
(select-keys [:_id :grub :completed])
|
||||
(assoc :event :add)))
|
||||
(assoc :event :add-grub)))
|
||||
sorted-grubs)
|
||||
out (chan)]
|
||||
(a/onto-chan out events)
|
||||
|
@ -60,7 +62,7 @@
|
|||
(let [recipes (mc/find-maps recipe-collection)
|
||||
sorted-recipes (sort-by :_id (vec recipes))
|
||||
events (map (fn [r] (-> r
|
||||
(select-keys [:_id :name :steps])
|
||||
(select-keys [:_id :name :grubs])
|
||||
(assoc :event :add-recipe)))
|
||||
sorted-recipes)
|
||||
out (chan)]
|
||||
|
@ -72,7 +74,6 @@
|
|||
|
||||
(defn handle-incoming-events [in]
|
||||
(a/go-loop [] (let [event (<! in)]
|
||||
(println "db received event:" event)
|
||||
(handle-event event)
|
||||
(recur))))
|
||||
|
||||
|
|
|
@ -9,14 +9,16 @@
|
|||
|
||||
(defn wire-channels-together []
|
||||
(let [to-remote (chan)
|
||||
to-state (chan)
|
||||
;to-state (chan)
|
||||
to-view (chan)
|
||||
from-remote (a/mult (ws/get-remote-chan to-remote))
|
||||
from-view (a/mult (view/setup-and-get-view-events to-view))]
|
||||
(state/handle-incoming-events to-state)
|
||||
(a/tap from-view to-state)
|
||||
(a/tap from-view to-remote)
|
||||
(a/tap from-remote to-state)
|
||||
(a/tap from-remote to-view)))
|
||||
from-remote (ws/get-remote-chan to-remote)
|
||||
from-view (view/setup-and-get-view-events to-view)]
|
||||
;(state/handle-incoming-events to-state)
|
||||
;; (a/tap from-view to-state)
|
||||
;; (a/tap from-view to-remote)
|
||||
;(a/tap from-remote to-state)))
|
||||
;(a/tap from-remote to-view)))
|
||||
(a/pipe from-remote to-view)
|
||||
(a/pipe from-view to-remote)))
|
||||
|
||||
(wire-channels-together)
|
||||
|
|
|
@ -1,177 +0,0 @@
|
|||
(ns grub.dom
|
||||
(:require [dommy.core :as dommy]
|
||||
[cljs.core.async :as a :refer [<! >! chan]])
|
||||
(:require-macros [grub.macros :refer [log logs go-loop]]
|
||||
[dommy.macros :refer [deftemplate sel1 node]]
|
||||
[cljs.core.async.macros :refer [go]]))
|
||||
|
||||
(defn listen
|
||||
([el type] (listen el type nil))
|
||||
([el type f] (listen el type f (chan)))
|
||||
([el type f out]
|
||||
(let [push-fn (fn [e] (when f (f e)) (go (>! out e)))
|
||||
unlisten #(do (dommy/unlisten! el type push-fn)
|
||||
(a/close! out))]
|
||||
(dommy/listen! el type push-fn)
|
||||
{:chan out :unlisten unlisten})))
|
||||
|
||||
(defn listen-once
|
||||
([el type] (listen el type nil))
|
||||
([el type f] (listen el type f (chan)))
|
||||
([el type f out]
|
||||
(let [push-fn (fn [e] (when f (f e)) (go (>! out e)))
|
||||
unlisten #(do (dommy/unlisten! el type push-fn)
|
||||
(a/close! out))]
|
||||
(dommy/listen-once! el type push-fn)
|
||||
{:chan out :unlisten unlisten})))
|
||||
|
||||
(def add-grub-text
|
||||
(node [:input.form-control {:id "add-grub-input" :type "text" :placeholder "2 grubs"}]))
|
||||
|
||||
(def add-grub-btn
|
||||
(node [:button.btn.btn-primary {:id "add-grub-btn" :type "button"} "Add"]))
|
||||
|
||||
(def clear-all-btn
|
||||
(node [:button.btn.hidden.pull-right
|
||||
{:id "clear-all-btn" :type "button"}
|
||||
"Clear all"]))
|
||||
|
||||
(defn make-grub-node [grub]
|
||||
(node [:li.list-group-item.grub-item
|
||||
{:id (:_id grub)
|
||||
:class (when (:completed grub) "completed")}
|
||||
[:span.grub-static
|
||||
(if (:completed grub)
|
||||
[:span.glyphicon.glyphicon-check]
|
||||
[:span.glyphicon.glyphicon-unchecked])
|
||||
[:span.grub-text (:grub grub)]]
|
||||
[:input.grub-input {:type "text" :value (:grub grub)}]]))
|
||||
|
||||
(defn grubs-selector []
|
||||
[(sel1 :#grub-list) :.grub-item])
|
||||
|
||||
(defn make-recipe-node [id name steps]
|
||||
(node [:div.panel.panel-default.recipe-panel {:id id}
|
||||
[:div.panel-heading.recipe-header
|
||||
[:input.form-control.recipe-header-input
|
||||
{:id "recipe-name"
|
||||
:type "text"
|
||||
:placeholder "Grub pie"
|
||||
:value name}]]
|
||||
[:div.panel-body.recipe-steps.hidden
|
||||
[:textarea.form-control.recipe-steps-input
|
||||
{:id "recipe-steps"
|
||||
:rows 3
|
||||
:placeholder "2 grubs"}
|
||||
steps]
|
||||
[:button.btn.btn-primary.recipe-done-btn.hidden.pull-right
|
||||
{:type "button"} "Done"]]]))
|
||||
|
||||
(defn add-new-recipe [id name steps]
|
||||
(log "add new recipe:" name "steps:" steps)
|
||||
(let [node (make-recipe-node id name steps)
|
||||
recipe-list (sel1 :#recipe-list)]
|
||||
(logs "node:" node)
|
||||
(logs "recipe-list:" recipe-list)
|
||||
(dommy/append! recipe-list node)
|
||||
node))
|
||||
|
||||
(def new-recipe (make-recipe-node "new-recipe" "" ""))
|
||||
|
||||
(defn recipes-selector []
|
||||
[(sel1 :#recipe-list) :.recipe-panel])
|
||||
|
||||
(defn recipe-done-btns-selector []
|
||||
[(sel1 :body) :.recipe-done-btn])
|
||||
|
||||
(deftemplate main-template []
|
||||
[:div.container
|
||||
[:div.row
|
||||
[:div.col-sm-6.leftmost-column
|
||||
[:h3 "Grub List"]
|
||||
[:div.input-group
|
||||
add-grub-text
|
||||
[:span.input-group-btn
|
||||
add-grub-btn]]
|
||||
[:ul#grub-list.list-group]
|
||||
clear-all-btn]
|
||||
[:div.col-sm-6
|
||||
[:h3.recipes-title "Recipes"]
|
||||
new-recipe
|
||||
[:ul#recipe-list.list-group.recipe-list]]]])
|
||||
|
||||
(defn render-body []
|
||||
(dommy/prepend! (sel1 :body) (main-template)))
|
||||
|
||||
(defn render-grub-list [grubs]
|
||||
(let [grub-list (sel1 :#grub-list)
|
||||
sorted-grubs (sort-by (juxt :completed :_id) grubs)]
|
||||
(aset grub-list "innerHTML" "")
|
||||
(doseq [grub sorted-grubs]
|
||||
(let [node (make-grub-node grub)]
|
||||
(dommy/append! grub-list node)))))
|
||||
|
||||
(defn get-add-grub-text []
|
||||
(dommy/value add-grub-text))
|
||||
|
||||
(defn clear-add-grub-text []
|
||||
(dommy/set-value! add-grub-text ""))
|
||||
|
||||
|
||||
(defprotocol IActivatable
|
||||
(-activate! [view])
|
||||
(-deactivate! [view]))
|
||||
|
||||
(defprotocol IHideable
|
||||
(-hide! [view])
|
||||
(-show! [view]))
|
||||
|
||||
(defprotocol IEditable
|
||||
(-set-editing! [view])
|
||||
(-unset-editing! [view]))
|
||||
|
||||
(defprotocol IExpandable
|
||||
(-expand! [view])
|
||||
(-unexpand! [view]))
|
||||
|
||||
(defprotocol IClearable
|
||||
(-clear! [view]))
|
||||
|
||||
(extend-type js/HTMLElement
|
||||
IActivatable
|
||||
(-activate! [view]
|
||||
(dommy/add-class! view :grub-active))
|
||||
(-deactivate! [view]
|
||||
(dommy/remove-class! view :grub-active)))
|
||||
|
||||
(extend-type js/HTMLElement
|
||||
IHideable
|
||||
(-hide! [view]
|
||||
(dommy/add-class! view :hidden))
|
||||
(-show! [view]
|
||||
(dommy/remove-class! view :hidden)))
|
||||
|
||||
(extend-type js/HTMLLIElement
|
||||
IEditable
|
||||
(-set-editing! [view]
|
||||
(-deactivate! view)
|
||||
(dommy/add-class! view :edit)
|
||||
(.focus (sel1 view :input)))
|
||||
(-unset-editing! [view]
|
||||
(dommy/remove-class! view :edit)))
|
||||
|
||||
(extend-type js/HTMLDivElement
|
||||
IExpandable
|
||||
(-expand! [view]
|
||||
(dommy/remove-class! (sel1 view ".recipe-steps") :hidden)
|
||||
(dommy/remove-class! (sel1 view ".recipe-done-btn") :hidden))
|
||||
(-unexpand! [view]
|
||||
(dommy/add-class! (sel1 view ".recipe-steps") :hidden)
|
||||
(dommy/add-class! (sel1 view ".recipe-done-btn") :hidden)))
|
||||
|
||||
(extend-type js/HTMLDivElement
|
||||
IClearable
|
||||
(-clear! [view]
|
||||
(dommy/set-value! (sel1 view "#recipe-name") "")
|
||||
(dommy/set-value! (sel1 view "#recipe-steps") "")))
|
||||
|
|
@ -6,3 +6,29 @@
|
|||
(defmacro logs [& args]
|
||||
(let [strings (map (fn [a] `(pr-str ~a)) args)]
|
||||
`(.log js/console ~@strings)))
|
||||
|
||||
|
||||
;; Maybe monad
|
||||
(defmacro and-let* [bindings & body]
|
||||
(when (not= (count bindings) 2)
|
||||
(throw (IllegalArgumentException.
|
||||
"and-let* requires an even number of forms in binding vector")))
|
||||
(let [form (bindings 0)
|
||||
tst (bindings 1)]
|
||||
`(let [temp# ~tst]
|
||||
(when temp#
|
||||
(let [~form temp#]
|
||||
~@body)))))
|
||||
|
||||
(defmacro and-let [bindings & body]
|
||||
(when (not (even? (count bindings)))
|
||||
(throw (IllegalArgumentException.
|
||||
"and-let requires an even number of forms in binding vector")))
|
||||
(let [whenlets (reduce (fn [sexpr bind]
|
||||
(let [form (first bind)
|
||||
tst (second bind)]
|
||||
(conj sexpr `(and-let* [~form ~tst]))))
|
||||
()
|
||||
(partition 2 bindings))
|
||||
body (cons 'do body)]
|
||||
`(->> ~body ~@whenlets)))
|
||||
|
|
|
@ -18,6 +18,7 @@
|
|||
(defmulti handle-event :event :default :unknown-event)
|
||||
|
||||
(defmethod handle-event :add [event]
|
||||
(logs "handle event add:" event)
|
||||
(let [grub (select-keys event [:_id :grub :completed])]
|
||||
(swap! grubs (fn [current] (conj current grub)))))
|
||||
|
||||
|
|
|
@ -1,313 +1,25 @@
|
|||
(ns grub.view
|
||||
(:require [grub.state :as state]
|
||||
[grub.dom :as dom]
|
||||
[grub.view.dom :as dom]
|
||||
[grub.view.grub :as grub-view]
|
||||
[grub.view.recipe :as recipe-view]
|
||||
[dommy.core :as dommy]
|
||||
[cljs.core.async :as a :refer [<! >! chan]])
|
||||
(:require-macros [grub.macros :refer [log logs do-chan]]
|
||||
(:require-macros [grub.macros :refer [log logs and-let]]
|
||||
[dommy.macros :refer [deftemplate sel1 node]]
|
||||
[cljs.core.async.macros :refer [go go-loop]]))
|
||||
|
||||
(defn re-render-when-state-changes []
|
||||
(add-watch state/grubs
|
||||
:grub-add-watch
|
||||
(fn [key ref old new]
|
||||
(if (empty? new)
|
||||
(dom/-hide! dom/clear-all-btn)
|
||||
(dom/-show! dom/clear-all-btn))
|
||||
(dom/render-grub-list new))))
|
||||
|
||||
(defn get-grubs-from-clicks []
|
||||
(->> (:chan (dom/listen dom/add-grub-btn :click))
|
||||
(a/map< #(dom/get-add-grub-text))))
|
||||
|
||||
(defn get-grubs-from-enter []
|
||||
(->> (:chan (dom/listen dom/add-grub-text :keyup))
|
||||
(a/filter< #(= (.-keyIdentifier %) "Enter"))
|
||||
(a/map< #(dom/get-add-grub-text))))
|
||||
|
||||
(defn get-created-events []
|
||||
(let [grubs (a/merge [(get-grubs-from-clicks)
|
||||
(get-grubs-from-enter)])]
|
||||
(->> grubs
|
||||
(a/filter< #(not (empty? %)))
|
||||
(a/map< (fn [g] {:grub g})))))
|
||||
|
||||
(defn get-clear-all-events []
|
||||
(:chan (dom/listen dom/clear-all-btn :click)))
|
||||
|
||||
(defn get-grub-mousedown-events []
|
||||
(:chan (dom/listen (dom/grubs-selector) :mousedown)))
|
||||
|
||||
(defn get-grub-mouseup-events []
|
||||
(:chan (dom/listen (dom/grubs-selector) :mouseup)))
|
||||
|
||||
(defn get-grub-mouseleave-events []
|
||||
(:chan (dom/listen (dom/grubs-selector) :mouseleave)))
|
||||
|
||||
(defn get-body-clicks []
|
||||
(:chan (dom/listen (sel1 :body) :click)))
|
||||
|
||||
(defn get-enters []
|
||||
(->> (:chan (dom/listen (sel1 :body) :keyup))
|
||||
(a/filter< #(= (.-keyIdentifier %) "Enter"))))
|
||||
|
||||
(defn get-ctrl-enters []
|
||||
(->> (:chan (dom/listen (sel1 :body) :keyup))
|
||||
(a/filter< #(and (= (.-keyIdentifier %) "Enter") (.-ctrlKey %)))))
|
||||
|
||||
(defn get-new-recipe-clicks []
|
||||
(:chan (dom/listen dom/new-recipe :click)))
|
||||
|
||||
(defn get-edit-recipe-clicks []
|
||||
(->> (:chan (dom/listen (dom/recipes-selector) :click))
|
||||
(a/map< (fn [e] {:elem (.-selectedTarget e)}))))
|
||||
|
||||
(defn get-recipe-done-btn-clicks []
|
||||
(->> (:chan (dom/listen (dom/recipe-done-btns-selector) :click))
|
||||
(a/map< (fn [e] (log "done button click:" (.-selectedTarget e))
|
||||
{:elem (.-selectedTarget e)}))))
|
||||
|
||||
(defn parse-completed-event [event]
|
||||
(let [target (.-selectedTarget event)
|
||||
id (.-id target)
|
||||
completed (dommy/has-class? target "completed")
|
||||
event-type (if completed :uncomplete :complete)]
|
||||
{:_id id :event event-type}))
|
||||
|
||||
(defmulti enter-state
|
||||
(fn [old-state new-state-name args]
|
||||
new-state-name)
|
||||
:default :unhandled)
|
||||
|
||||
(defmethod enter-state :unhandled [old-state new-state-name args]
|
||||
(logs "Unhandled enter transition from " (:name old-state) "to" new-state-name)
|
||||
old-state)
|
||||
|
||||
(defmulti exit-state
|
||||
(fn [state]
|
||||
(:name state))
|
||||
:default :unhandled)
|
||||
|
||||
(defmethod exit-state :unhandled [state]
|
||||
(logs "Unhandled exit transition from " (:name state))
|
||||
state)
|
||||
|
||||
(defn transition [state new-state-name & args]
|
||||
(logs "transition from" (:name state) "to" new-state-name)
|
||||
(-> state
|
||||
(exit-state)
|
||||
(enter-state new-state-name args)
|
||||
(assoc :name new-state-name)))
|
||||
|
||||
(defmulti handle-event
|
||||
(fn [state event]
|
||||
[(:name state) (:event event)])
|
||||
:default [:unhandled-state :unhandled-event])
|
||||
|
||||
(defmethod handle-event [:unhandled-state :unhandled-event] [state event]
|
||||
(logs "Unhandled event [" (:name state) (:event event) "]")
|
||||
state)
|
||||
|
||||
|
||||
|
||||
|
||||
(defmethod handle-event [:default :created] [state event]
|
||||
(let [add-event (-> event
|
||||
(assoc :event :add)
|
||||
(assoc :_id (str "grub-" (.now js/Date)))
|
||||
(assoc :completed false))]
|
||||
(go (>! (:out state) add-event))
|
||||
(dom/clear-add-grub-text)
|
||||
state))
|
||||
|
||||
(defmethod handle-event [:default :clear-all] [state event]
|
||||
(go (>! (:out state) {:event :clear-all}))
|
||||
state)
|
||||
|
||||
(defmethod handle-event [:default :mousedown] [state event]
|
||||
(let [mouseevent (:data event)]
|
||||
(dom/-activate! (.-selectedTarget mouseevent))
|
||||
(let [now (.now js/Date)
|
||||
new-state (assoc state :mousedown-time now)]
|
||||
(go (<! (a/timeout 500))
|
||||
(>! (:edit (:channels state))
|
||||
{:mousedown-time now :elem (.-selectedTarget mouseevent)}))
|
||||
new-state)))
|
||||
|
||||
(defmethod handle-event [:default :mouseup] [state event]
|
||||
(dom/-deactivate! (.-selectedTarget (:data event)))
|
||||
(go (>! (:out state) (parse-completed-event (:data event))))
|
||||
(let [new-state (assoc state :mousedown-time nil)]
|
||||
new-state))
|
||||
|
||||
(defmethod handle-event [:default :mouseleave] [state event]
|
||||
(dom/-deactivate! (.-selectedTarget (:data event)))
|
||||
state)
|
||||
|
||||
(defmethod handle-event [:default :edit] [state event]
|
||||
(if (and (:mousedown-time state)
|
||||
(= (:mousedown-time event)
|
||||
(:mousedown-time state)))
|
||||
(transition state :edit-grub (:elem event))
|
||||
state))
|
||||
|
||||
(defmethod handle-event [:default :new-recipe-click] [state event]
|
||||
(transition state :new-recipe))
|
||||
|
||||
(defmethod handle-event [:default :edit-recipe-click] [state event]
|
||||
(transition state :edit-recipe (:elem event)))
|
||||
|
||||
(defmethod handle-event [:default :add-recipe] [state event]
|
||||
(log "handle event add-recipe")
|
||||
(dom/add-new-recipe (:_id event) (:name event) (:steps event))
|
||||
state)
|
||||
|
||||
|
||||
|
||||
|
||||
(defmethod enter-state :edit-grub [old-state new-state-name [edit-elem]]
|
||||
(dom/-set-editing! edit-elem)
|
||||
(assoc old-state :edit-elem edit-elem))
|
||||
|
||||
(defmethod exit-state :edit-grub [state]
|
||||
(let [edit-elem (:edit-elem state)]
|
||||
(dom/-unset-editing! edit-elem)
|
||||
(let [grub-text (.-value (sel1 edit-elem :.grub-input))
|
||||
id (.-id edit-elem)
|
||||
update-event {:event :update :grub grub-text :_id id}
|
||||
new-state (dissoc state :edit-elem)]
|
||||
(go (>! (:out state) update-event))
|
||||
new-state)))
|
||||
|
||||
(defmethod handle-event [:edit-grub :body-click] [state event]
|
||||
(let [clicked-elem (.-target (:data event))
|
||||
edit-elem (:edit-elem state)]
|
||||
(if (dommy/descendant? clicked-elem edit-elem)
|
||||
state
|
||||
(transition state :default))))
|
||||
|
||||
(defmethod handle-event [:edit-grub :enter] [state event]
|
||||
(transition state :default))
|
||||
|
||||
|
||||
|
||||
|
||||
(defmethod enter-state :new-recipe [old-state new-state-name args]
|
||||
(dom/-expand! dom/new-recipe)
|
||||
old-state)
|
||||
|
||||
(defn get-new-recipe-info []
|
||||
(let [name (.-value (sel1 dom/new-recipe "#recipe-name"))
|
||||
steps (.-value (sel1 dom/new-recipe "#recipe-steps"))]
|
||||
(when (not (or (empty? name) (empty? steps)))
|
||||
(let [id (str "recipe-" (.now js/Date))]
|
||||
{:name name :steps steps :_id id}))))
|
||||
|
||||
(defmethod exit-state :new-recipe [state]
|
||||
(dom/-unexpand! dom/new-recipe)
|
||||
(let [recipe-info (get-new-recipe-info)]
|
||||
(if recipe-info
|
||||
(let [recipe-node (dom/add-new-recipe (:_id recipe-info)
|
||||
(:name recipe-info)
|
||||
(:steps recipe-info))]
|
||||
(log "new recipe name:" (:name recipe-info) "steps" (:steps recipe-info))
|
||||
(dom/-clear! dom/new-recipe)
|
||||
(go (>! (:out state) (assoc recipe-info :event :add-recipe)))
|
||||
(assoc state
|
||||
:recipes (assoc (:recipes state) (.-id recipe-node) recipe-node)))
|
||||
state)))
|
||||
|
||||
(defmethod handle-event [:new-recipe :body-click] [state event]
|
||||
(let [clicked-elem (.-target (:data event))]
|
||||
(if (dommy/descendant? clicked-elem dom/new-recipe)
|
||||
state
|
||||
(transition state :default))))
|
||||
|
||||
(defmethod handle-event [:new-recipe :recipe-done-btn-click] [state event]
|
||||
(log "handle new recipe done btn click")
|
||||
(if (dommy/descendant? (:elem event) dom/new-recipe)
|
||||
(transition state :default)
|
||||
state))
|
||||
|
||||
(defmethod handle-event [:new-recipe :ctrl-enter] [state event]
|
||||
(transition state :default))
|
||||
|
||||
|
||||
(defmethod enter-state :edit-recipe [old-state new-state-name [elem]]
|
||||
(dom/-expand! elem)
|
||||
(assoc old-state :edit-elem elem))
|
||||
|
||||
(defmethod exit-state :edit-recipe [state]
|
||||
(let [recipe-node (:edit-elem state)
|
||||
recipe-name (.-value (sel1 recipe-node "#recipe-name"))
|
||||
recipe-steps (.-value (sel1 recipe-node "#recipe-steps"))]
|
||||
(log "update recipe new name:" recipe-name "new steps:" recipe-steps)
|
||||
(dom/-unexpand! recipe-node)
|
||||
(-> state
|
||||
(dissoc :edit-elem))))
|
||||
|
||||
(defmethod handle-event [:edit-recipe :body-click] [state event]
|
||||
(log "edit-recipe body click")
|
||||
(let [clicked-elem (.-target (:data event))
|
||||
recipe-node (:edit-elem state)]
|
||||
(if (dommy/descendant? clicked-elem recipe-node)
|
||||
state
|
||||
(transition state :default))))
|
||||
|
||||
(defmethod handle-event [:edit-recipe :recipe-done-btn-click] [state event]
|
||||
(if (dommy/descendant? (:elem event) (:edit-elem state))
|
||||
(transition state :default)
|
||||
state))
|
||||
|
||||
|
||||
|
||||
|
||||
(defn main-loop [channels]
|
||||
(let [out (chan)
|
||||
events (chan)
|
||||
event-mix (a/mix events)]
|
||||
(doseq [[name c] (seq channels)] (a/admix event-mix c))
|
||||
(go-loop [state {:name :default
|
||||
:channels channels
|
||||
:out out
|
||||
:recipes {}}]
|
||||
(let [event (<! events)]
|
||||
(logs "handle event" (:name state) event)
|
||||
(recur (handle-event state event))))
|
||||
out))
|
||||
|
||||
|
||||
|
||||
(defn get-raw-view-channels []
|
||||
{:created (get-created-events)
|
||||
:clear-all (get-clear-all-events)
|
||||
:mousedown (get-grub-mousedown-events)
|
||||
:mouseup (get-grub-mouseup-events)
|
||||
:mouseleave (get-grub-mouseleave-events)
|
||||
:body-click (get-body-clicks)
|
||||
:edit (chan)
|
||||
:enter (get-enters)
|
||||
:ctrl-enter (get-ctrl-enters)
|
||||
:new-recipe-click (get-new-recipe-clicks)
|
||||
:edit-recipe-click (get-edit-recipe-clicks)
|
||||
:recipe-done-btn-click (get-recipe-done-btn-clicks)})
|
||||
|
||||
(defn append-event-name-to-channel-events [channels]
|
||||
(into {}
|
||||
(for [[name c] channels]
|
||||
[name (a/map< (fn [e]
|
||||
(if (map? e)
|
||||
(assoc e :event name)
|
||||
{:event name :data e}))
|
||||
c)])))
|
||||
|
||||
(defn get-named-channels [remote-channel]
|
||||
(let [raw-view-channels (get-raw-view-channels)
|
||||
named-view-channels (append-event-name-to-channel-events raw-view-channels)]
|
||||
(assoc named-view-channels :remote-channel remote-channel)))
|
||||
|
||||
|
||||
(defn setup-and-get-view-events [remote-channel]
|
||||
(dom/render-body)
|
||||
(re-render-when-state-changes)
|
||||
(main-loop (get-named-channels remote-channel)))
|
||||
(let [out (chan)
|
||||
remote (a/mult remote-channel)
|
||||
to-grubs (chan)
|
||||
to-recipes (chan)
|
||||
from-grubs (grub-view/handle-grubs to-grubs)
|
||||
from-recipes (a/mult (recipe-view/handle-recipes to-recipes))]
|
||||
(a/tap remote to-grubs)
|
||||
(a/tap remote to-recipes)
|
||||
(a/tap from-recipes to-grubs)
|
||||
(a/tap from-recipes out)
|
||||
(a/pipe from-grubs out)
|
||||
out))
|
||||
|
|
262
src/cljs/grub/view/dom.cljs
Normal file
262
src/cljs/grub/view/dom.cljs
Normal file
|
@ -0,0 +1,262 @@
|
|||
(ns grub.view.dom
|
||||
(:require [dommy.core :as dommy]
|
||||
[cljs.core.async :as a :refer [<! >! chan]])
|
||||
(:require-macros [grub.macros :refer [log logs go-loop]]
|
||||
[dommy.macros :refer [deftemplate sel1 node]]
|
||||
[cljs.core.async.macros :refer [go]]))
|
||||
|
||||
(defn listen
|
||||
([el type] (listen el type nil))
|
||||
([el type f] (listen el type f (chan)))
|
||||
([el type f out]
|
||||
(let [push-fn (fn [e] (when f (f e)) (go (>! out e)))
|
||||
unlisten #(do (dommy/unlisten! el type push-fn)
|
||||
(a/close! out))]
|
||||
(dommy/listen! el type push-fn)
|
||||
{:chan out :unlisten unlisten})))
|
||||
|
||||
(defn listen-once
|
||||
([el type] (listen el type nil))
|
||||
([el type f] (listen el type f (chan)))
|
||||
([el type f out]
|
||||
(let [push-fn (fn [e] (when f (f e)) (go (>! out e)))
|
||||
unlisten #(do (dommy/unlisten! el type push-fn)
|
||||
(a/close! out))]
|
||||
(dommy/listen-once! el type push-fn)
|
||||
{:chan out :unlisten unlisten})))
|
||||
|
||||
(defn get-away-clicks [elem]
|
||||
(let [{c :chan unlisten :unlisten} (listen (sel1 :body) :click)
|
||||
filtered-chan (a/filter< #(not (dommy/descendant? (.-target %) elem)) c)]
|
||||
{:unlisten unlisten :chan filtered-chan}))
|
||||
|
||||
(def add-grub-text
|
||||
(node [:input.form-control {:id "add-grub-input" :type "text" :placeholder "2 grubs"}]))
|
||||
|
||||
(def add-grub-btn
|
||||
(node [:button.btn.btn-primary {:id "add-grub-btn" :type "button"} "Add"]))
|
||||
|
||||
(def clear-all-btn
|
||||
(node [:button.btn.hidden.pull-right
|
||||
{:id "clear-all-btn" :type "button"}
|
||||
"Clear all"]))
|
||||
|
||||
(defn clear-grubs! []
|
||||
(dommy/set-html! (sel1 :#grub-list) ""))
|
||||
|
||||
(defn get-grub-completed-glyph [completed]
|
||||
(node (if completed
|
||||
[:span.glyphicon.glyphicon-check]
|
||||
[:span.glyphicon.glyphicon-unchecked])))
|
||||
|
||||
(defn make-grub-node [id grub completed]
|
||||
(node [:li.list-group-item.grub-item
|
||||
{:id id
|
||||
:class (when completed "completed")}
|
||||
[:span.grub-static
|
||||
(get-grub-completed-glyph completed)
|
||||
[:span.grub-text grub]]
|
||||
[:input.grub-input {:type "text" :value grub}]]))
|
||||
|
||||
(defn grubs-selector []
|
||||
[(sel1 :#grub-list) :.grub-item])
|
||||
|
||||
(defn make-recipe-node
|
||||
([id name grubs] (make-recipe-node id name grubs false))
|
||||
([id name grubs new-recipe]
|
||||
(node [:div.panel.panel-default.recipe-panel
|
||||
{:id id}
|
||||
[:div.panel-heading.recipe-header
|
||||
[:input.form-control.recipe-header-input
|
||||
{:id "recipe-name"
|
||||
:type "text"
|
||||
:placeholder "Grub pie"
|
||||
:value name}]]
|
||||
[:div.panel-body.recipe-grubs.hidden
|
||||
[:textarea.form-control.recipe-grubs-input
|
||||
{:id "recipe-grubs"
|
||||
:rows 3
|
||||
:placeholder "2 grubs"}
|
||||
grubs]
|
||||
(when-not new-recipe
|
||||
[:button.btn.btn-primary.pull-left.recipe-btn.recipe-add-grubs-btn
|
||||
{:type "button"} "Add Grubs"])
|
||||
[:button.btn.btn-primary.hidden.pull-right.recipe-btn.recipe-done-btn
|
||||
{:type "button"} "Done"]]])))
|
||||
|
||||
(def new-recipe (make-recipe-node "new-recipe" "" "" true))
|
||||
|
||||
(defn recipes-selector []
|
||||
[(sel1 :#recipe-list) :.recipe-panel])
|
||||
|
||||
(defn recipe-done-btns-selector []
|
||||
[(sel1 :body) :.recipe-done-btn])
|
||||
|
||||
(defn recipe-add-grubs-btns-selector []
|
||||
[(sel1 :body) :.recipe-add-grubs-btn])
|
||||
|
||||
(deftemplate main-template []
|
||||
[:div.container
|
||||
[:div.row
|
||||
[:div.col-sm-6.leftmost-column
|
||||
[:h3 "Grub List"]
|
||||
[:div.input-group
|
||||
add-grub-text
|
||||
[:span.input-group-btn
|
||||
add-grub-btn]]
|
||||
[:ul#grub-list.list-group]
|
||||
clear-all-btn]
|
||||
[:div.col-sm-6
|
||||
[:h3.recipes-title "Recipes"]
|
||||
new-recipe
|
||||
[:ul#recipe-list.list-group.recipe-list]]]])
|
||||
|
||||
(defn render-body []
|
||||
(dommy/prepend! (sel1 :body) (main-template)))
|
||||
|
||||
(defn get-add-grub-text []
|
||||
(dommy/value add-grub-text))
|
||||
|
||||
(defn clear-add-grub-text []
|
||||
(dommy/set-value! add-grub-text ""))
|
||||
|
||||
|
||||
(defprotocol IHideable
|
||||
(-hide! [this])
|
||||
(-show! [this]))
|
||||
|
||||
(defprotocol IGrub
|
||||
(-activate! [this])
|
||||
(-deactivate! [this])
|
||||
|
||||
(-complete! [this])
|
||||
(-uncomplete! [this])
|
||||
(-completed? [this])
|
||||
|
||||
(-set-editing! [this])
|
||||
(-unset-editing! [this])
|
||||
(-editing? [this])
|
||||
(-update-grub! [this grub]))
|
||||
|
||||
(defprotocol IRecipe
|
||||
(-expand! [this])
|
||||
(-unexpand! [this])
|
||||
|
||||
(-update-recipe! [this])
|
||||
|
||||
(-get-name [this])
|
||||
(-get-grubs-str [this])
|
||||
(-get-grubs [this]))
|
||||
|
||||
(defprotocol IClearable
|
||||
(-clear! [this]))
|
||||
|
||||
(extend-type js/HTMLElement
|
||||
IHideable
|
||||
(-hide! [this]
|
||||
(dommy/add-class! this :hidden))
|
||||
(-show! [this]
|
||||
(dommy/remove-class! this :hidden)))
|
||||
|
||||
|
||||
(extend-type js/HTMLElement
|
||||
IGrub
|
||||
(-activate! [this]
|
||||
(dommy/add-class! this :grub-active))
|
||||
(-deactivate! [this]
|
||||
(dommy/remove-class! this :grub-active))
|
||||
|
||||
(-complete! [this]
|
||||
(dommy/add-class! this :completed)
|
||||
(dommy/replace! (sel1 this ".glyphicon")
|
||||
(get-grub-completed-glyph true)))
|
||||
(-uncomplete! [this]
|
||||
(dommy/remove-class! this :completed)
|
||||
(dommy/replace! (sel1 this ".glyphicon")
|
||||
(get-grub-completed-glyph false)))
|
||||
(-completed? [this]
|
||||
(dommy/has-class? this :completed))
|
||||
|
||||
(-set-editing! [this]
|
||||
(-deactivate! this)
|
||||
(dommy/add-class! this :edit)
|
||||
(.focus (sel1 this :input)))
|
||||
(-unset-editing! [this]
|
||||
(dommy/remove-class! this :edit))
|
||||
(-editing? [this]
|
||||
(dommy/has-class? this :edit)))
|
||||
|
||||
(defrecord Grub [elem id grub completed]
|
||||
dommy.template/PElement
|
||||
(-elem [this] elem)
|
||||
|
||||
IGrub
|
||||
(-set-editing! [this] (-set-editing! elem))
|
||||
(-unset-editing! [this] (-unset-editing! elem))
|
||||
(-editing? [this] (-editing? elem))
|
||||
|
||||
(-complete! [this] (-complete! elem))
|
||||
(-uncomplete! [this] (-uncomplete! elem))
|
||||
(-completed? [this] (-completed? elem))
|
||||
|
||||
(-set-editing! [this] (-set-editing! elem))
|
||||
(-unset-editing! [this] (-unset-editing! elem))
|
||||
(-editing? [this] (-editing? elem))
|
||||
|
||||
(-update-grub! [this grub]
|
||||
(dommy/set-text! (sel1 elem ".grub-text") grub)
|
||||
(dommy/set-value! (sel1 elem ".grub-input") grub)))
|
||||
|
||||
(defn add-new-grub [id grub completed]
|
||||
(let [node (make-grub-node id grub completed)
|
||||
grub (Grub. node id grub completed)
|
||||
grub-list (sel1 :#grub-list)]
|
||||
(dommy/append! grub-list grub)
|
||||
(dommy/set-value! (sel1 :#add-grub-input) "")
|
||||
grub))
|
||||
|
||||
(extend-type js/HTMLDivElement
|
||||
IRecipe
|
||||
(-expand! [this]
|
||||
(dommy/remove-class! (sel1 this ".recipe-grubs") :hidden)
|
||||
(dommy/remove-class! (sel1 this ".recipe-done-btn") :hidden))
|
||||
(-unexpand! [this]
|
||||
(dommy/add-class! (sel1 this ".recipe-grubs") :hidden)
|
||||
(dommy/add-class! (sel1 this ".recipe-done-btn") :hidden))
|
||||
|
||||
(-get-name [this]
|
||||
(dommy/value (sel1 this :#recipe-name)))
|
||||
(-get-grubs-str [this]
|
||||
(dommy/value (sel1 this :#recipe-grubs)))
|
||||
(-get-grubs [this]
|
||||
(let [split-grubs (clojure.string/split-lines (-get-grubs-str this))]
|
||||
(when split-grubs (into [] split-grubs)))))
|
||||
|
||||
|
||||
(extend-type js/HTMLDivElement
|
||||
IClearable
|
||||
(-clear! [this]
|
||||
(dommy/set-value! (sel1 this "#recipe-name") "")
|
||||
(dommy/set-value! (sel1 this "#recipe-grubs") "")))
|
||||
|
||||
(defrecord Recipe [elem id name grubs]
|
||||
dommy.template/PElement
|
||||
(-elem [this] elem)
|
||||
|
||||
IRecipe
|
||||
(-expand! [this] (-expand! elem))
|
||||
(-unexpand! [this] (-unexpand! elem))
|
||||
|
||||
(-clear! [this] (-clear! elem))
|
||||
|
||||
(-update-recipe! [this]
|
||||
(dommy/set-value! (sel1 this :#recipe-name) name)
|
||||
(dommy/set-text! (sel1 this :#recipe-grubs) grubs)))
|
||||
|
||||
(defn add-new-recipe [id name grubs]
|
||||
(let [node (make-recipe-node id name grubs)
|
||||
recipe (Recipe. node id name grubs)
|
||||
recipe-list (sel1 :#recipe-list)]
|
||||
(dommy/append! recipe-list recipe)
|
||||
recipe))
|
||||
|
170
src/cljs/grub/view/grub.cljs
Normal file
170
src/cljs/grub/view/grub.cljs
Normal file
|
@ -0,0 +1,170 @@
|
|||
(ns grub.view.grub
|
||||
(:require [grub.view.dom :as dom]
|
||||
[dommy.core :as dommy]
|
||||
[cljs.core.async :as a :refer [<! >! chan]])
|
||||
(:require-macros [grub.macros :refer [log logs and-let]]
|
||||
[dommy.macros :refer [sel1]]
|
||||
[cljs.core.async.macros :refer [go go-loop]]))
|
||||
|
||||
(defn get-add-grub-clicks []
|
||||
(:chan (dom/listen dom/add-grub-btn :click)))
|
||||
|
||||
(defn get-add-grub-enters []
|
||||
(->> (:chan (dom/listen dom/add-grub-text :keyup))
|
||||
(a/filter< #(= (.-keyIdentifier %) "Enter"))))
|
||||
|
||||
(defn get-create-events []
|
||||
(let [events (a/merge [(get-add-grub-clicks)
|
||||
(get-add-grub-enters)])]
|
||||
(->> events
|
||||
(a/map< #(dom/get-add-grub-text))
|
||||
(a/filter< #(not (empty? %)))
|
||||
(a/map< (fn [g]
|
||||
{:event :add-grub
|
||||
:_id (str "grub-" (.now js/Date))
|
||||
:grub g
|
||||
:completed false})))))
|
||||
|
||||
(defn parse-complete-event [elem]
|
||||
(let [id (.-id elem)
|
||||
completed (dom/-completed? elem)
|
||||
event-type (if completed :uncomplete-grub :complete-grub)]
|
||||
{:_id id
|
||||
:event event-type}))
|
||||
|
||||
(defn get-complete-events []
|
||||
(->> (:chan (dom/listen (dom/grubs-selector) :click))
|
||||
(a/map< #(.-selectedTarget %))
|
||||
(a/filter< #(not (dom/-editing? %)))
|
||||
(a/map< parse-complete-event)))
|
||||
|
||||
(defn get-clear-all-events []
|
||||
(->> (:chan (dom/listen dom/clear-all-btn :click))
|
||||
(a/map< (fn [e] {:event :clear-all-grubs}))))
|
||||
|
||||
(defn get-grub-mousedown-events []
|
||||
(let [{c :chan unlisten :unlisten} (dom/listen (dom/grubs-selector) :mousedown)]
|
||||
{:unlisten unlisten
|
||||
:chan (a/map< (fn [e] {:selected-grub (.-selectedTarget e)}) c)}))
|
||||
|
||||
(defn get-grub-mouseup-events [grub-elem]
|
||||
(dom/listen grub-elem :mouseup))
|
||||
|
||||
(defn get-grub-mouseleave-events [grub-elem]
|
||||
(dom/listen grub-elem :mouseleave))
|
||||
|
||||
(defn wait-for-mousedown-on-grub []
|
||||
(let [out (chan)]
|
||||
(go (let [{mousedown :chan unlisten :unlisten} (get-grub-mousedown-events)
|
||||
event (<! mousedown)
|
||||
selected-grub (:selected-grub event)]
|
||||
(unlisten)
|
||||
(>! out selected-grub)))
|
||||
out))
|
||||
|
||||
(defn wait-for-grub-mousedown-timeout [grub]
|
||||
(let [out (chan)]
|
||||
(dom/-activate! grub)
|
||||
(go (let [{mouseup :chan
|
||||
unlisten-mouseup :unlisten} (get-grub-mouseup-events grub)
|
||||
{mouseleave :chan
|
||||
unlisten-mouseleave :unlisten } (get-grub-mouseleave-events grub)
|
||||
timeout (a/timeout 500)
|
||||
[_ c] (a/alts! [mouseup mouseleave timeout])]
|
||||
(unlisten-mouseleave)
|
||||
(unlisten-mouseup)
|
||||
(dom/-deactivate! grub)
|
||||
(>! out (= c timeout))))
|
||||
out))
|
||||
|
||||
(defn get-enters []
|
||||
(let [{c :chan unlisten :unlisten} (dom/listen (sel1 :body) :keyup)]
|
||||
{:unlisten unlisten
|
||||
:chan (a/filter< #(= (.-keyIdentifier %) "Enter") c)}))
|
||||
|
||||
(defn make-grub-update-event [grub-elem orig-grub-text]
|
||||
(let [grub-text (.-value (sel1 grub-elem :.grub-input))
|
||||
id (.-id grub-elem)]
|
||||
(when (not (= grub-text orig-grub-text))
|
||||
{:event :update-grub
|
||||
:grub grub-text
|
||||
:_id id})))
|
||||
|
||||
(defn wait-for-update-event [grub]
|
||||
(let [out (chan)
|
||||
orig-grub (.-value (sel1 grub :.grub-input))]
|
||||
(go (let [{bodyclick :chan
|
||||
unlisten-bodyclick :unlisten} (dom/get-away-clicks grub)
|
||||
{enter :chan
|
||||
unlisten-enter :unlisten} (get-enters)]
|
||||
(dom/-set-editing! grub)
|
||||
(a/alts! [bodyclick enter])
|
||||
(unlisten-bodyclick)
|
||||
(unlisten-enter)
|
||||
(dom/-unset-editing! grub)
|
||||
(if-let [update-event (make-grub-update-event grub orig-grub)]
|
||||
(>! out update-event)
|
||||
(a/close! out))))
|
||||
out))
|
||||
|
||||
(defn get-update-events []
|
||||
(let [out (chan)]
|
||||
(go-loop []
|
||||
(and-let [grub (<! (wait-for-mousedown-on-grub))
|
||||
timeout? (<! (wait-for-grub-mousedown-timeout grub))
|
||||
update-event (<! (wait-for-update-event grub))]
|
||||
(>! out update-event))
|
||||
(recur))
|
||||
out))
|
||||
|
||||
(defn get-grub-with-index [grubs id]
|
||||
(let [grub-index (->> grubs
|
||||
(map-indexed vector)
|
||||
(filter #(= (:_id (second %)) id))
|
||||
(first)
|
||||
(first))
|
||||
grub (grubs grub-index)]
|
||||
[grub-index grub]))
|
||||
|
||||
(defmulti handle-event (fn [event grubs] (:event event))
|
||||
:default :unknown-event)
|
||||
|
||||
(defmethod handle-event :unknown-event [event grubs]
|
||||
;(logs "Cannot handle unknown event:" event)
|
||||
grubs)
|
||||
|
||||
(defmethod handle-event :add-grub [event grubs]
|
||||
(let [grub (dom/add-new-grub (:_id event) (:grub event) (:completed event))]
|
||||
(dom/-show! dom/clear-all-btn)
|
||||
(assoc grubs (:id grub) grub)))
|
||||
|
||||
(defmethod handle-event :complete-grub [event grubs]
|
||||
(let [grub (get grubs (:_id event))]
|
||||
(dom/-complete! grub)
|
||||
(assoc-in grubs [(:_id event) :completed] true)))
|
||||
|
||||
(defmethod handle-event :uncomplete-grub [event grubs]
|
||||
(dom/-uncomplete! (get grubs (:_id event)))
|
||||
(assoc-in grubs [(:_id event) :completed] false))
|
||||
|
||||
(defmethod handle-event :update-grub [event grubs]
|
||||
(dom/-update-grub! (get grubs (:_id event)) (:grub event))
|
||||
(assoc-in grubs [(:_id event) :grub] (:grub event)))
|
||||
|
||||
(defmethod handle-event :clear-all-grubs [event grubs]
|
||||
(dom/-hide! dom/clear-all-btn)
|
||||
(dom/clear-grubs!)
|
||||
{})
|
||||
|
||||
(defn handle-grubs [remote-events]
|
||||
(let [out (chan)
|
||||
local-events [(get-create-events)
|
||||
(get-complete-events)
|
||||
(get-clear-all-events)
|
||||
(get-update-events)]]
|
||||
(go-loop [grubs {}]
|
||||
(let [[event c] (a/alts! (conj local-events remote-events))]
|
||||
(when-not (= c remote-events)
|
||||
(>! out event))
|
||||
(recur (handle-event event grubs))))
|
||||
out))
|
153
src/cljs/grub/view/recipe.cljs
Normal file
153
src/cljs/grub/view/recipe.cljs
Normal file
|
@ -0,0 +1,153 @@
|
|||
(ns grub.view.recipe
|
||||
(:require [grub.view.dom :as dom]
|
||||
[dommy.core :as dommy]
|
||||
[cljs.core.async :as a :refer [<! >! chan]])
|
||||
(:require-macros [grub.macros :refer [log logs and-let]]
|
||||
[dommy.macros :refer [sel1]]
|
||||
[cljs.core.async.macros :refer [go go-loop]]))
|
||||
|
||||
(defn wait-for-new-recipe-input-click []
|
||||
(:chan (dom/listen-once dom/new-recipe :click)))
|
||||
|
||||
(defn get-ctrl-enters []
|
||||
(let [{c :chan unlisten :unlisten} (dom/listen (sel1 :body) :keyup)
|
||||
filtered-chan (a/filter< #(and (= (.-keyIdentifier %) "Enter")
|
||||
(.-ctrlKey %))
|
||||
c)]
|
||||
{:chan filtered-chan :unlisten unlisten}))
|
||||
|
||||
(defn parse-new-recipe-event []
|
||||
(let [name (dom/-get-name dom/new-recipe)
|
||||
grubs (dom/-get-grubs-str dom/new-recipe)]
|
||||
(when (not (or (empty? name) (empty? grubs)))
|
||||
(let [id (str "recipe-" (.now js/Date))]
|
||||
{:event :add-recipe
|
||||
:name name
|
||||
:grubs grubs
|
||||
:_id id}))))
|
||||
|
||||
(defn wait-for-create-event []
|
||||
(let [out (chan)
|
||||
{ctrl-enters :chan
|
||||
ctrl-enters-unlisten :unlisten} (get-ctrl-enters)
|
||||
{away-clicks :chan
|
||||
away-clicks-unlisten :unlisten} (dom/get-away-clicks dom/new-recipe)
|
||||
{done-clicks :chan
|
||||
done-clicks-unlisten :unlisten} (dom/listen
|
||||
(sel1 dom/new-recipe ".recipe-done-btn")
|
||||
:click)]
|
||||
(go (a/alts! [ctrl-enters away-clicks done-clicks])
|
||||
(ctrl-enters-unlisten)
|
||||
(away-clicks-unlisten)
|
||||
(done-clicks-unlisten)
|
||||
(if-let [event (parse-new-recipe-event)]
|
||||
(>! out event)
|
||||
(a/close! out)))
|
||||
out))
|
||||
|
||||
(defn get-create-events []
|
||||
(let [out (chan)]
|
||||
(go-loop []
|
||||
(<! (wait-for-new-recipe-input-click))
|
||||
(dom/-expand! dom/new-recipe)
|
||||
(when-let [create-event (<! (wait-for-create-event))]
|
||||
(>! out create-event)
|
||||
(dom/-clear! dom/new-recipe))
|
||||
(dom/-unexpand! dom/new-recipe)
|
||||
(recur))
|
||||
out))
|
||||
|
||||
(defn wait-for-edit-recipe-input-click []
|
||||
(->> (:chan (dom/listen-once (dom/recipes-selector) :click))
|
||||
(a/map< #(.-selectedTarget %))))
|
||||
|
||||
(defn parse-update-recipe-event [elem]
|
||||
(let [id (.-id elem)
|
||||
name (dom/-get-name elem)
|
||||
grubs (dom/-get-grubs-str elem)]
|
||||
(when (not (or (empty? name) (empty? grubs)))
|
||||
{:event :update-recipe
|
||||
:name name
|
||||
:grubs grubs
|
||||
:_id id})))
|
||||
|
||||
(defn wait-for-update-event [elem]
|
||||
(let [out (chan)
|
||||
{ctrl-enters :chan
|
||||
ctrl-enters-unlisten :unlisten} (get-ctrl-enters)
|
||||
{away-clicks :chan
|
||||
away-clicks-unlisten :unlisten} (dom/get-away-clicks elem)
|
||||
{done-clicks :chan
|
||||
done-clicks-unlisten :unlisten} (dom/listen
|
||||
(sel1 elem ".recipe-done-btn")
|
||||
:click)]
|
||||
(go (a/alts! [ctrl-enters away-clicks done-clicks])
|
||||
(ctrl-enters-unlisten)
|
||||
(away-clicks-unlisten)
|
||||
(done-clicks-unlisten)
|
||||
(if-let [event (parse-update-recipe-event elem)]
|
||||
(>! out event)
|
||||
(a/close! out)))
|
||||
out))
|
||||
|
||||
(defn get-update-events []
|
||||
(let [out (chan)]
|
||||
(go-loop []
|
||||
(let [recipe-elem (<! (wait-for-edit-recipe-input-click))]
|
||||
(dom/-expand! recipe-elem)
|
||||
(when-let [update-event (<! (wait-for-update-event recipe-elem))]
|
||||
(>! out update-event))
|
||||
(dom/-unexpand! recipe-elem)
|
||||
(recur)))
|
||||
out))
|
||||
|
||||
(defn get-add-grub-events []
|
||||
(let [out (chan)
|
||||
clicks (:chan (dom/listen (dom/recipe-add-grubs-btns-selector) :click))]
|
||||
(go-loop []
|
||||
(let [e (<! clicks)
|
||||
elem (dommy/closest (.-selectedTarget e) :.recipe-panel)
|
||||
id (.-id elem)
|
||||
grubs (dom/-get-grubs elem)
|
||||
events (map-indexed (fn [index g] {:event :add-grub
|
||||
:_id (str "grub-" (.now js/Date) index)
|
||||
:grub g
|
||||
:completed false}) grubs)]
|
||||
(a/onto-chan out events false))
|
||||
(recur))
|
||||
out))
|
||||
|
||||
|
||||
(defmulti handle-event (fn [event recipes] (:event event))
|
||||
:default :unknown-event)
|
||||
|
||||
(defmethod handle-event :unknown-event [event recipes]
|
||||
;(logs "Cannot handle unknown event:" event)
|
||||
recipes)
|
||||
|
||||
(defmethod handle-event :add-recipe [event recipes]
|
||||
(let [recipe (dom/add-new-recipe (:_id event)
|
||||
(:name event)
|
||||
(:grubs event))]
|
||||
(assoc recipes (:id recipe) recipe)))
|
||||
|
||||
(defmethod handle-event :update-recipe [event recipes]
|
||||
(let [recipe (get recipes (:_id event))
|
||||
updated-recipe (-> recipe
|
||||
(assoc :name (:name event))
|
||||
(assoc :grubs (:grubs event)))]
|
||||
(dom/-update-recipe! updated-recipe)
|
||||
(assoc recipes (:id recipes) updated-recipe)))
|
||||
|
||||
(defn handle-recipes [remote-events]
|
||||
(let [out (chan)
|
||||
local-events [(get-create-events)
|
||||
(get-update-events)]
|
||||
add-grub-events (get-add-grub-events)]
|
||||
(a/pipe add-grub-events out)
|
||||
(go-loop [recipes {}]
|
||||
(let [[event c] (a/alts! (conj local-events remote-events))]
|
||||
(when-not (= c remote-events)
|
||||
(>! out event))
|
||||
(recur (handle-event event recipes))))
|
||||
out))
|
Loading…
Reference in a new issue