Merge branch 'om'

This commit is contained in:
Nicholas Kariniemi 2014-08-03 00:20:11 +03:00
commit 3f687afb4b
16 changed files with 17304 additions and 897 deletions

View file

@ -1,59 +0,0 @@
(ns grub.integration-test
(:require [grub.db :as db]
[grub.websocket :as ws]
[clj-webdriver.taxi :as taxi]
[clj-webdriver.core :as webdriver]
[clojure.test :as test]))
;; Hard-code path to chromedriver
(defn set-chromedriver-path! []
(System/setProperty "webdriver.chrome.driver" "bin/chromedriver"))
(defn add-grub [driver grub-text]
(taxi/input-text driver "#add-grub-input" grub-text)
(taxi/click driver {:text "Add"}))
(defn get-driver [url]
(webdriver/start {:browser :chrome} url))
(defn get-rand-grub []
(str "testgrub" (rand-int 10000)))
(defn test-adding-synced-grubs [url driver1 driver2]
(taxi/to driver1 url)
(taxi/to driver2 url)
(let [grubs (repeatedly 4 get-rand-grub)]
(doseq [grub grubs]
(add-grub driver1 grub))
(doseq [grub grubs]
(test/is (taxi/find-element driver2 {:text grub})
"Added grubs should appear in other browser")))
(db/clear-grubs))
(defn test-grubs-are-stored-on-server [url driver]
(taxi/to driver url)
(let [grubs (repeatedly 4 get-rand-grub)]
(doseq [grub grubs]
(add-grub driver grub))
(Thread/sleep 200)
(taxi/refresh driver)
(Thread/sleep 200)
(doseq [grub grubs]
(test/is (taxi/find-element driver {:text grub})
"Previously added grubs should be loaded on refresh")))
(db/clear-grubs))
(defn run [port]
(set-chromedriver-path!)
(let [db-chan (db/connect-and-handle-events "grub-integration-test")
site-url (str "http://localhost:" port)]
(println "Starting integration test")
(ws/pass-received-events-to-clients-and-db db-chan)
(let [driver1 (get-driver site-url)
driver2 (get-driver site-url)]
(test-grubs-are-stored-on-server site-url driver1)
(test-adding-synced-grubs site-url driver1 driver2)
(taxi/quit driver1)
(taxi/quit driver2)))
(db/clear-grubs))

View file

@ -4,7 +4,7 @@
:license {:name "Eclipse Public License"
:url "http://www.eclipse.org/legal/epl-v10.html"}
:dependencies [[org.clojure/clojure "1.6.0"]
[org.clojure/clojurescript "0.0-2234"]
[org.clojure/clojurescript "0.0-2277"]
[org.clojure/core.async "0.1.303.0-886421-alpha"]
[http-kit "2.1.18"]
[compojure "1.1.8"]
@ -14,7 +14,10 @@
[prismatic/dommy "0.1.2"]
[com.novemberain/monger "2.0.0"]
[org.clojure/tools.cli "0.3.1"]
[clj-webdriver "0.6.1" :exclusions [org.clojure/core.cache]]]
[clj-webdriver "0.6.1" :exclusions [org.clojure/core.cache]]
[om "0.7.0"]
[sablono "0.2.17"]
[cljs-uuid "0.0.4"]]
:profiles {:uberjar {:aot :all}}
:min-lein-version "2.1.2"
:plugins [[lein-cljsbuild "1.0.3"]
@ -27,7 +30,7 @@
:prod {:source-paths ["src/cljs"]
:compiler {:output-to "public/js/grub.js"
:optimizations :advanced}}}}
:source-paths ["src/clj" "integration"]
:source-paths ["src/clj" "src/test"]
:test-paths ["spec/clj"]
:ring {:handler grub.core/app}
:uberjar-name "grub-standalone.jar"

View file

@ -1,161 +1,162 @@
html, body {
height: 100%;
min-height: 100%;
width: 100%;
min-width: 100%;
margin: 0px;
height: 100%;
min-height: 100%;
width: 100%;
min-width: 100%;
margin: 0px;
}
h3 {
margin-top: 10px;
margin-top: 10px;
}
.recipes-title {
clear: right;
clear: right;
}
.recipe-list {
margin-top: 10px;
margin-top: 10px;
}
#recipe-grubs {
width: 100%;
width: 100%;
}
.recipe-add-grubs-btn {
float: right;
clear: both;
margin: 2px;
float: right;
clear: both;
margin: 2px;
}
.hidden {
display: none;
display: none;
}
.leftmost-column {
margin-bottom: 45px;
margin-bottom: 45px;
}
.grub-close {
display: none;
display: none;
}
tr:hover .grub-close {
display: block;
display: block;
}
#grub-list {
margin-top: 10px;
margin-bottom: 10px;
margin-top: 10px;
margin-bottom: 10px;
}
.add-grub-input-form .input-group-btn {
width: 100%;
}
.grub-item {
cursor: default;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
cursor: default;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
}
.grub-item.grub-active {
color: #ffffff;
background-color: rgb(71, 73, 73);
color: #ffffff;
background-color: rgb(71, 73, 73);
}
.grub-item .input-span {
display: block;
overflow: hidden;
display: block;
overflow: hidden;
}
.grub-item .grub-static {
display: inline;
display: inline;
}
.grub-item .grub-input {
display: none;
display: none;
}
.grub-item.edit .grub-static {
display: none;
display: none;
}
.grub-item.edit .grub-input {
display: inline;
width: 100%;
display: inline;
width: 100%;
}
.grub-item .glyphicon {
margin-right: 5px;
margin-right: 5px;
}
.completed {
text-decoration: line-through;
color: #a9a9a9;
text-decoration: line-through;
color: #a9a9a9;
}
.completed.edit {
text-decoration: none;
color: default;
text-decoration: none;
color: default;
}
.panel-body {
padding: 10px;
padding-top: 0px;
padding: 10px;
padding-top: 0px;
}
.panel-heading {
background-color: #ffffff;
background-color: #ffffff;
}
.recipe-panel {
padding: 0px;
margin-bottom: -1px;
padding: 0px;
margin-bottom: 0px;
}
.recipe-panel > .recipe-header {
background-color: #ffffff;
background-color: #ffffff;
}
.recipe-header {
margin: 0px;
padding: 0px;
border-bottom: none;
margin: 0px;
padding: 0px;
border-bottom: none;
}
.recipe-header-input {
border: none;
box-shadow: none;
transition: none;
border-bottom: none;
width: inherit;
display: inline;
border: none;
box-shadow: none;
transition: none;
border-bottom: none;
display: inline;
width: inherit;
}
.recipe-header-input:focus {
border: none;
box-shadow: none;
transition: none;
outline: none;
border: none;
box-shadow: none;
transition: none;
outline: none;
}
.recipe-btn {
margin-top: 10px;
}
.recipe-grubs {
margin-top: 10px;
}
.recipe-grubs-input {
border: none;
box-shadow: none;
transition: none;
border-bottom: none;
resize: none;
border: none;
box-shadow: none;
transition: none;
border-bottom: none;
resize: none;
}
.recipe-grubs-input:focus {
border: none;
box-shadow: none;
transition: none;
border-bottom: none;
border: none;
box-shadow: none;
transition: none;
border-bottom: none;
}

16644
public/js/react-0.9.0.js Normal file

File diff suppressed because it is too large Load diff

21
public/js/react-0.9.0.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,84 +0,0 @@
(ns grub.db-spec
(:require [speclj.core :refer :all]
[grub.db :as db]
[monger.collection :as mc]
[clojure.core.async :refer [>!! <!! timeout]]))
(def test-db "grub-test")
(defn short-delay []
(<!! (timeout 50)))
(defn get-test-grub []
{:_id (str "grub" (rand-int 10000))
:grub (str "testtext" (rand-int 1000))
:completed ([true false] (rand-int 1))})
(describe
"grub.db"
(before-all (db/connect-and-handle-events test-db))
(before (db/clear-grubs))
(describe "Add"
(it "should add a grub when an add event comes"
(let [test-grub "testgrub"
test-id 12345]
(>!! @db/incoming-events {:event :add
:_id test-id
:grub test-grub
:completed false})
(short-delay)
(should=
{:_id test-id :grub test-grub :completed false}
(mc/find-one-as-map db/grub-collection {:_id test-id})))))
(describe "Complete"
(it "should complete a grub when a complete event comes"
(let [test-grub {:_id 123456 :completed false}]
(mc/insert db/grub-collection test-grub)
(>!! @db/incoming-events {:event :complete :_id (:_id test-grub)})
(short-delay)
(should=
{:_id (:_id test-grub) :completed true}
(mc/find-one-as-map db/grub-collection {:_id (:_id test-grub)})))))
(describe "Uncomplete"
(it "should uncomplete a grub when an uncomplete event comes"
(let [test-grub {:_id 123456 :completed true}]
(mc/insert db/grub-collection test-grub)
(>!! @db/incoming-events {:event :uncomplete :_id (:_id test-grub)})
(short-delay)
(should=
{:_id (:_id test-grub) :completed false}
(mc/find-one-as-map db/grub-collection {:_id (:_id test-grub)})))))
(describe "Update"
(it "should update grub info when an update event comes"
(let [test-grub {:_id 123456 :grub "original"}]
(mc/insert db/grub-collection test-grub)
(>!! @db/incoming-events {:event :update
:_id (:_id test-grub)
:grub "updated"})
(short-delay)
(should=
{:_id (:_id test-grub) :grub "updated"}
(mc/find-one-as-map db/grub-collection {:_id (:_id test-grub)})))))
(describe "Delete"
(it "should delete a grub when a delete event comes"
(let [test-grub {:_id 123456 :completed true}]
(mc/insert db/grub-collection test-grub)
(>!! @db/incoming-events {:event :delete :_id (:_id test-grub)})
(short-delay)
(should=
nil
(mc/find-one-as-map db/grub-collection {:_id (:_id test-grub)})))))
(describe "Clear all"
(it "should delete all grubs"
(let [test-grub {:_id 123456 :completed true}]
(mc/insert db/grub-collection test-grub)
(>!! @db/incoming-events {:event :clear-all})
(short-delay)
(should
(empty?
(mc/find-maps db/grub-collection)))))))

View file

@ -1,7 +1,7 @@
(ns grub.core
(:require [grub.websocket :as ws]
[grub.db :as db]
[grub.integration-test :as integration-test]
[grub.test.integration.core :as integration-test]
[ring.middleware.reload :as reload]
[ring.middleware.file :as file]
[ring.util.response :as resp]
@ -25,7 +25,8 @@
(html5
index-page-header
[:body
(include-js "http://fb.me/react-0.9.0.js")
[:div#container]
(include-js "/js/react-0.9.0.min.js")
(include-js "/js/jquery.js")
(include-js "/js/bootstrap.js")
(include-js "/js/grub.js")]))
@ -34,7 +35,8 @@
(html5
index-page-header
[:body
(include-js "http://fb.me/react-0.9.0.js")
[:div#container]
(include-js "/js/react-0.9.0.js")
(include-js "/js/out/goog/base.js")
(include-js "/js/jquery.js")
(include-js "/js/bootstrap.js")
@ -58,14 +60,13 @@
(handler/site routes))))
(def default-port 3000)
(def integration-test-port 3456)
(defn start-server [port]
(httpkit/run-server app {:port port}))
(defn run-integration-test []
(let [stop-server (start-server integration-test-port)]
(integration-test/run integration-test-port)
(let [stop-server (start-server integration-test/server-port)]
(integration-test/run)
(stop-server)))
(defn start-production-server [{:keys [port mongo-url]}]

View file

@ -12,6 +12,13 @@
(defn clear-grubs []
(mc/drop @db grub-collection))
(defn clear-recipes []
(mc/drop @db recipe-collection))
(defn clear-all []
(clear-grubs)
(clear-recipes))
(defmulti handle-event :event :default :unknown-event)
(defn insert-grub [event]
@ -38,9 +45,9 @@
{mo/$set {:completed false}}))
(defmethod handle-event :update-grub [event]
(mc/update @db grub-collection
{:_id (:id event)}
{mo/$set {:grub (:grub event)}}))
(let [orig (mc/find-one-as-map @db grub-collection {:_id (:id event)})
new (dissoc event :event-type :id)]
(mc/update-by-id @db grub-collection (:id event) (merge orig new))))
(defmethod handle-event :clear-all-grubs [event]
(clear-grubs))
@ -60,22 +67,18 @@
(println "Cannot handle unknown event:" event))
(defn get-current-grubs []
(let [raw-grubs (mc/find-maps @db grub-collection)
sorted-grubs (sort-by :_id (vec raw-grubs))
grubs (map (fn [g] (-> g
(select-keys [:_id :grub :completed])
(clojure.set/rename-keys {:_id :id})))
sorted-grubs)]
grubs))
(->> (mc/find-maps @db grub-collection)
(sort-by :_id)
(map #(select-keys % [:_id :grub :completed]))
(map #(clojure.set/rename-keys % {:_id :id}))
(vec)))
(defn get-current-recipes []
(let [raw-recipes (mc/find-maps @db recipe-collection)
sorted-recipes (sort-by :_id (vec raw-recipes))
recipes (map (fn [r] (-> r
(select-keys [:_id :name :grubs])
(clojure.set/rename-keys {:_id :id})))
sorted-recipes)]
recipes))
(->> (mc/find-maps @db recipe-collection)
(sort-by :_id)
(map #(select-keys % [:_id :name :grubs]))
(map #(clojure.set/rename-keys % {:_id :id}))
(vec)))
(def production-db "grub")
(def development-db "grub-dev")

View file

@ -1,16 +1,16 @@
(ns grub.core
(:require [grub.view :as view]
(:require [grub.state :as state]
[grub.websocket :as ws]
[cljs.core.async :as a :refer [<! >! chan]])
(:require-macros [grub.macros :refer [log logs go-loop]]
(:require-macros [grub.macros :refer [log logs]]
[cljs.core.async.macros :refer [go]]))
(defn wire-channels-together []
(let [to-remote (chan)
to-view (chan)
to-state (chan)
from-remote (ws/get-remote-chan to-remote)
from-view (view/setup-and-get-view-events to-view)]
(a/pipe from-remote to-view)
(a/pipe from-view to-remote)))
from-state (state/update-state-and-render to-state)]
(a/pipe from-remote to-state)
(a/pipe from-state to-remote)))
(wire-channels-together)

72
src/cljs/grub/state.cljs Normal file
View file

@ -0,0 +1,72 @@
(ns grub.state
(:require [grub.view.app :as view]
[cljs.core.async :as a :refer [<! >! chan]])
(:require-macros [grub.macros :refer [log logs]]
[cljs.core.async.macros :refer [go go-loop]]))
(def app-state (atom {:grubs {}
:recipes {}}))
(defmulti handle-event (fn [event state] (:event event))
:default :unknown-event)
(defmethod handle-event :unknown-event [event state]
state)
(defn new-grub [id grub completed]
{:id id :grub grub :completed completed})
(defmethod handle-event :add-grub [event state]
(let [grub (new-grub (:id event) (:grub event) (:completed event))]
(assoc-in state [:grubs (:id grub)] grub)))
(defn map-by-key [key coll]
(->> coll
(map (fn [a] [(get a key) a]))
(into {})))
(defmethod handle-event :add-grub-list [event state]
(->> event
:grubs
(map-by-key :id)
(merge (:grubs state))
(assoc state :grubs)))
(defmethod handle-event :update-grub [event state]
(let [new-grub-info (dissoc event :event-type)
orig-grub (get-in state [:grubs (:id event)])]
(assoc-in state [:grubs (:id event)] (merge orig-grub new-grub-info))))
(defmethod handle-event :clear-all-grubs [event state]
(assoc state :grubs {}))
(defn new-recipe [id name grubs]
{:id id :name name :grubs grubs})
(defmethod handle-event :add-recipe [event state]
(let [recipe (new-recipe (:id event) (:name event) (:grubs event))]
(assoc-in state [:recipes (:id recipe)] recipe)))
(defmethod handle-event :add-recipe-list [event state]
(->> event
:recipes
(map-by-key :id)
(merge (:recipes state))
(assoc state :recipes)))
(defmethod handle-event :update-recipe [event state]
(-> state
(assoc-in [:recipes (:id event) :name] (:name event))
(assoc-in [:recipes (:id event) :grubs] (:grubs event))))
(defn update-state-and-render [remote]
(let [out (chan)
view-events (view/render-app app-state)]
(go-loop []
(let [[event ch] (alts! [remote view-events])
new-state (handle-event event @app-state)]
(reset! app-state new-state)
(when (= ch view-events)
(>! out event))
(recur)))
out))

View file

@ -1,24 +0,0 @@
(ns grub.view
(:require [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 and-let]]
[dommy.macros :refer [deftemplate sel1 node]]
[cljs.core.async.macros :refer [go go-loop]]))
(defn setup-and-get-view-events [remote-channel]
(dom/render-body)
(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))

View file

@ -0,0 +1,48 @@
(ns grub.view.app
(:require [om.core :as om :include-macros true]
[sablono.core :as html :refer-macros [html]]
[cljs.core.async :as a :refer [<! put! chan]]
[grub.view.dom :as dom]
[grub.view.grub :as grub]
[grub.view.recipe :as recipe])
(:require-macros [grub.macros :refer [log logs]]
[cljs.core.async.macros :refer [go go-loop]]))
(defn app-view [props owner]
(reify
om/IRender
(render [this]
(html
[:div.container
[:div.row
[:div.col-sm-6.leftmost-column
(om/build grub/grubs-view (:grubs props))]
[:div.col-sm-6
(om/build recipe/recipes-view (:recipes props))]]]))
om/IWillMount
(will-mount [_]
(let [>events (om/get-shared owner :>events)]
(dom/on-document-mousedown #(put! >events {:type :body-mousedown :event %}))))))
(defn render-app [state]
(let [grub-add (chan)
grub-update (chan)
grub-clear-all (chan)
recipe-add (chan)
recipe-add-grubs (chan)
recipe-update (chan)
out (a/merge [grub-add grub-update grub-clear-all recipe-add recipe-add-grubs recipe-update])
>events (chan)
<events (a/pub >events :type)]
(om/root app-view
state
{:target (.getElementById js/document "container")
:shared {:grub-add grub-add
:grub-update grub-update
:grub-clear-all grub-clear-all
:recipe-add recipe-add
:recipe-add-grubs recipe-add-grubs
:recipe-update recipe-update
:>events >events
:<events <events}})
out))

View file

@ -1,317 +1,14 @@
(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]]))
(ns grub.view.dom)
(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 enter-pressed? [event]
(let [enter-keycode 13]
(= (.-which event) enter-keycode)))
(def ENTER-KEYCODE 13)
(defn click-on-self? [event node]
(.contains node (.-target event)))
(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 on-document-mousedown [f]
(.addEventListener js/document "mousedown" f))
(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}))
(defn get-clicks [elem]
(listen elem :click))
(defn get-enters [elem]
(let [{c :chan unlisten :unlisten} (listen elem :keyup)
filtered-chan (a/filter< #(= (.-which %) ENTER-KEYCODE) c)]
{:unlisten unlisten
:chan filtered-chan}))
(defn get-ctrl-enters []
(let [{c :chan unlisten :unlisten} (listen (sel1 :body) :keyup)
filtered-chan (a/filter< #(and (= (.-which %) ENTER-KEYCODE)
(.-ctrlKey %))
c)]
{:chan filtered-chan :unlisten unlisten}))
(defn get-body-enters []
(get-enters (sel1 :body)))
(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}]
(when-not new-recipe
[:button.btn.btn-primary.btn-sm.recipe-add-grubs-btn {:type "button"} "Add Grubs"])]
[:div.panel-body.recipe-grubs.hidden
[:textarea.form-control.recipe-grubs-input
{:id "recipe-grubs"
:rows 3
:placeholder "2 grubs"}
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))
(def new-recipe-done-btn
(sel1 new-recipe ".recipe-done-btn"))
(defn recipes-selector []
[(sel1 :#recipe-list) :.recipe-panel])
(defn recipe-done-btns-selector []
[(sel1 :body) :.recipe-done-btn])
(defn recipe-done-btn-selector [recipe-elem]
(sel1 recipe-elem :.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]]]])
(deftemplate grub-list-template [grubs]
(node (for [grub grubs]
(make-grub-node (:id grub) (:grub grub) (:completed grub)))))
(defn render-body []
(dommy/prepend! (sel1 :body) (main-template)))
(defn render-grub-list [grubs]
(let [grub-list (sel1 :#grub-list)]
(aset grub-list "innerHTML" "")
(dommy/replace-contents! grub-list (grub-list-template grubs))))
(defn get-add-grub-text []
(dommy/value add-grub-text))
(defn clear-add-grub-text []
(dommy/set-value! add-grub-text ""))
(defn get-recipe-add-grubs-clicks []
(->> (:chan (listen (recipe-add-grubs-btns-selector) :click))
(a/map< #(dommy/closest (.-selectedTarget %) :.recipe-panel))))
(defn get-edit-recipe-input-click []
(->> (:chan (listen-once (recipes-selector) :click))
(a/filter< #(not (dommy/has-class? (.-selectedTarget %) :btn)))
(a/map< #(.-selectedTarget %))))
(defprotocol IHideable
(-hide! [this])
(-show! [this]))
(defprotocol IGrub
(-activate! [this])
(-deactivate! [this])
(-id [this])
(-grub-text [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))
(-id [this]
(.-id this))
(-grub-text [this]
(.-value (sel1 this :.grub-input)))
(-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 make-new-grub [id grub completed]
(let [node (make-grub-node id grub completed)
grub (Grub. node id grub completed)
grub-list (sel1 :#grub-list)]
grub))
(defn clear-new-grub-input! []
(dommy/set-value! (sel1 :#add-grub-input) ""))
(defn focus-new-grub-input! []
(.focus (sel1 :#add-grub-input)))
(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))
(defn event-val [event]
(.. event -target -value))

View file

@ -1,193 +1,148 @@
(ns grub.view.grub
(:require [grub.view.dom :as dom]
[cljs.core.async :as a :refer [<! >! chan]])
(:require-macros [grub.macros :refer [log logs and-let]]
(:require [om.core :as om :include-macros true]
[sablono.core :as html :refer-macros [html]]
[cljs.core.async :as a :refer [<! put! chan]]
[grub.view.dom :as dom]
[cljs-uuid.core :as uuid])
(:require-macros [grub.macros :refer [log logs]]
[cljs.core.async.macros :refer [go go-loop]]))
(defn get-add-grub-clicks []
(:chan (dom/get-clicks dom/add-grub-btn)))
(defn new-grub [grub]
{:id (str "grub-" (uuid/make-random))
:grub grub
:completed false})
(defn get-add-grub-enters []
(:chan (dom/get-enters dom/add-grub-text)))
(defn add-event [grub]
(assoc (new-grub grub) :event :add-grub))
(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 add-list-event [grubs]
{:event :add-grub-list
:grubs grubs})
(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 edit-event [id grub]
{:event :update-grub
:id id
:grub grub})
(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 complete-event [{:keys [id completed]}]
{:event :update-grub
:id id
:completed (not completed)})
(defn get-clear-all-events []
(->> (:chan (dom/listen dom/clear-all-btn :click))
(a/map< (fn [e] {:event :clear-all-grubs}))))
(defn grub-view [{:keys [id grub completed] :as props} owner]
(reify
om/IInitState
(init-state [_]
(let [publisher (chan)]
{:edit-state :waiting
:>local-events publisher
:<local-events (a/pub publisher identity)
:grub grub}))
(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)}))
om/IRenderState
(render-state [_ {:keys [edit-state >local-events] :as state}]
(html
[:li.list-group-item.grub-item
{:class [(when completed "completed")
(when (= edit-state :pressed) "grub-active")
(when (= edit-state :editing) "edit")]
:on-mouse-down #(put! >local-events :mouse-down)
:on-mouse-up #(put! >local-events :mouse-up)
:on-mouse-leave #(put! >local-events :mouse-leave)
:on-click #(when (#{:waiting :pressed} edit-state)
(put! (om/get-shared owner :grub-update) (complete-event @props)))}
[:span.grub-static
(if completed
[:span.glyphicon.glyphicon-check]
[:span.glyphicon.glyphicon-unchecked])
[:span.grub-text grub]]
[:input.grub-input
{:type "text"
:value (:grub state)
:on-change #(om/set-state! owner :grub (.. % -target -value))
:on-key-up #(when (dom/enter-pressed? %) (put! >local-events :enter))}]]))
(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 make-grub-update-event [grub-elem orig-grub-text]
(let [grub-text (dom/-grub-text grub-elem)
id (dom/-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 (dom/-grub-text grub)]
(go (let [{bodyclick :chan
unlisten-bodyclick :unlisten} (dom/get-away-clicks grub)
{enter :chan
unlisten-enter :unlisten} (dom/get-body-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]))
om/IWillMount
(will-mount [_]
(let [<local-events (om/get-state owner :<local-events)
<events (om/get-shared owner :<events)
subscriber (chan)]
(go-loop []
(om/set-state! owner :edit-state :waiting)
(let [subscriber (chan)]
(a/sub <local-events :mouse-down subscriber)
(<! subscriber)
(a/unsub <local-events :mouse-down subscriber)
(a/close! subscriber))
(om/set-state! owner :edit-state :pressed)
(a/sub <local-events :mouse-leave subscriber)
(a/sub <local-events :mouse-up subscriber)
(let [timeout (a/timeout 500)
[event c] (a/alts! [timeout subscriber])]
(a/unsub <local-events :mouse-leave subscriber)
(a/unsub <local-events :mouse-up subscriber)
(if (= c timeout)
(do (om/set-state! owner :edit-state :editing)
(a/sub <events :body-mousedown subscriber)
(a/sub <local-events :enter subscriber)
(loop []
(let [event (<! subscriber)]
(when (and (= (:type event) :body-mousedown)
(dom/click-on-self? (:event event) (om/get-node owner)))
(recur))))
(a/unsub <events :body-mousedown subscriber)
(a/unsub <local-events :enter subscriber)
(put! (om/get-shared owner :grub-update)
(edit-event id (om/get-state owner :grub))))
(om/set-state! owner :edit-state :waiting)))
(recur))))))
(defn get-grub-ingredient [grub]
(let [text (clojure.string/lower-case (:grub grub))
match (re-find #"[a-z]{3}.*$" text)]
match))
(when-not (nil? (:grub grub))
(let [text (clojure.string/lower-case (:grub grub))
match (re-find #"[a-z]{3}.*$" text)]
match)))
(defn sort-and-render-grub-list! [grubs]
(let [sorted-grubs (sort-by (juxt :completed get-grub-ingredient) (vals grubs))]
(dom/render-grub-list sorted-grubs)))
(defn sort-grubs [grubs]
(sort-by (juxt :completed get-grub-ingredient :grub) (vals grubs)))
(defmulti handle-event (fn [event grubs] (:event event))
:default :unknown-event)
(defn add-grub [add new-grub owner]
(when (not (empty? new-grub))
(om/set-state! owner :new-grub "")
(put! add (add-event new-grub))))
(defmethod handle-event :unknown-event [event grubs]
;(logs "Cannot handle unknown event:" event)
grubs)
(defmethod handle-event :add-grub [event grubs]
(let [grub (dom/make-new-grub (:id event) (:grub event) (:completed event))
new-grubs (assoc grubs (:id grub) grub)]
(dom/-show! dom/clear-all-btn)
(sort-and-render-grub-list! new-grubs)
(dom/clear-new-grub-input!)
(dom/focus-new-grub-input!)
new-grubs))
(defn assoc-new-grub [current new]
(assoc current (:id new)
(dom/make-new-grub (:id new) (:grub new) (:completed new))))
(defn make-add-grubs-map [grub-events]
(reduce assoc-new-grub {} grub-events))
(defmethod handle-event :add-grub-list [event grubs]
(let [add-grub-events (:grubs event)
add-grubs (make-add-grubs-map add-grub-events)
new-grubs (merge grubs add-grubs)]
(dom/-show! dom/clear-all-btn)
(sort-and-render-grub-list! new-grubs)
new-grubs))
(defmethod handle-event :complete-grub [event grubs]
(let [grub (get grubs (:id event))
new-grubs (assoc-in grubs [(:id event) :completed] true)]
(sort-and-render-grub-list! new-grubs)
new-grubs))
(defmethod handle-event :uncomplete-grub [event grubs]
(let [new-grubs (assoc-in grubs [(:id event) :completed] false)]
(sort-and-render-grub-list! new-grubs)
new-grubs))
(defmethod handle-event :update-grub [event grubs]
(let [new-grubs (assoc-in grubs [(:id event) :grub] (:grub event))]
(sort-and-render-grub-list! new-grubs)
new-grubs))
(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))
(defn grubs-view [props owner]
(reify
om/IInitState
(init-state [_]
{:new-grub ""})
om/IRenderState
(render-state [this {:keys [new-grub] :as state}]
(let [add (om/get-shared owner :grub-add)]
(html
[:div
[:h3 "Grub List"]
[:div.input-group.add-grub-input-form
[:span.input-group-btn
[:input.form-control#add-grub-input
{:type "text"
:placeholder "What do you need?"
:value new-grub
:on-key-up #(when (dom/enter-pressed? %)
(add-grub add new-grub owner))
:on-change #(om/set-state! owner :new-grub (dom/event-val %))}]]
[:button.btn.btn-primary
{:id "add-grub-btn"
:type "button"
:on-click #(add-grub add new-grub owner)}
"Add"]]
[:ul#grub-list.list-group
(for [grub (sort-grubs props)]
(om/build grub-view grub {:key :id}))]
[:button.btn.pull-right
{:id "clear-all-btn"
:class (when (empty? props) "hidden")
:type "button"
:on-click #(put! (om/get-shared owner :grub-clear-all)
{:event :clear-all-grubs})}
"Clear all"]])))))

View file

@ -1,152 +1,194 @@
(ns grub.view.recipe
(:require [grub.view.dom :as dom]
[cljs.core.async :as a :refer [<! >! chan]])
(:require-macros [grub.macros :refer [log logs and-let]]
(:require [om.core :as om :include-macros true]
[sablono.core :as html :refer-macros [html]]
[cljs.core.async :as a :refer [<! put! chan]]
[cljs-uuid.core :as uuid]
[grub.view.dom :as dom]
[grub.view.grub :as grub-view])
(:require-macros [grub.macros :refer [log logs]]
[cljs.core.async.macros :refer [go go-loop]]))
(defn wait-for-new-recipe-input-click []
(:chan (dom/listen-once dom/new-recipe :click)))
(defn add-event [name grubs]
{:event :add-recipe
:id (str "recipe-" (uuid/make-random))
:name name
:grubs grubs})
(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 update-event [id name grubs]
{:event :update-recipe
:id id
:name name
:grubs grubs})
(defn wait-for-create-event []
(let [out (chan)
{ctrl-enters :chan
ctrl-enters-unlisten :unlisten} (dom/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/get-clicks dom/new-recipe-done-btn)]
(go (a/alts! [ctrl-enters away-clicks done-clicks])
(ctrl-enters-unlisten)
(away-clicks-unlisten)
(done-clicks-unlisten)
(when-let [event (parse-new-recipe-event)]
(>! out event))
(a/close! out))
out))
(defn parse-grubs-from-str [grubs-str]
(->> grubs-str
(clojure.string/split-lines)
(map grub-view/new-grub)
(into [])))
(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 add-grubs [add-grubs-ch grubs-str]
(let [grubs (parse-grubs-from-str grubs-str)
event (grub-view/add-list-event grubs)]
(put! add-grubs-ch event)))
(defn wait-for-edit-recipe-input-click []
(dom/get-edit-recipe-input-click))
(defn update-recipe [ch id name grubs owner]
(when (and (not (empty? name))
(not (empty? grubs)))
(om/set-state! owner :editing false)
(put! ch (update-event id name grubs))))
(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 recipe-view [{:keys [id] :as props} owner]
(reify
om/IInitState
(init-state [_]
(let [publisher (chan)]
{:editing false
:>local-events publisher
:<local-events (a/pub publisher identity)
:name (:name props)
:grubs (:grubs props)}))
(defn wait-for-update-event [elem]
(let [out (chan)
{ctrl-enters :chan
ctrl-enters-unlisten :unlisten} (dom/get-ctrl-enters)
{away-clicks :chan
away-clicks-unlisten :unlisten} (dom/get-away-clicks elem)
{done-clicks :chan
done-clicks-unlisten :unlisten} (dom/get-clicks (dom/recipe-done-btn-selector elem))]
(go (a/alts! [ctrl-enters away-clicks done-clicks])
(ctrl-enters-unlisten)
(away-clicks-unlisten)
(done-clicks-unlisten)
(when-let [event (parse-update-recipe-event elem)]
(>! out event))
(a/close! out))
out))
om/IWillReceiveProps
(will-receive-props [this next-props]
(om/set-state! owner :name (:name next-props))
(om/set-state! owner :grubs (:grubs next-props)))
(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))
om/IRenderState
(render-state [this {:keys [editing >local-events name grubs]}]
(let [update (om/get-shared owner :recipe-update)
add-grubs-ch (om/get-shared owner :recipe-add-grubs)]
(html
[:div.panel.panel-default.recipe-panel
{:on-click #(put! >local-events :click)}
[:div.panel-heading.recipe-header
[:input.form-control.recipe-header-input
{:type "text"
:value name
:on-change #(om/set-state! owner :name (dom/event-val %))}]
[:button.btn.btn-primary.btn-sm.recipe-add-grubs-btn
{:type "button"
:on-click #(add-grubs add-grubs-ch grubs)}
"Add Grubs"]]
[:div.panel-body.recipe-grubs
{:class (when (not editing) "hidden")}
[:textarea.form-control.recipe-grubs-input
{:id "recipe-grubs"
:rows 3
:value grubs
:on-change #(om/set-state! owner :grubs (dom/event-val %))}]
[:button.btn.btn-primary.pull-right.recipe-btn.recipe-done-btn
{:type "button"
:on-click #(update-recipe update id name grubs owner)}
"Save"]]])))
om/IWillMount
(will-mount [_]
(let [<local-events (om/get-state owner :<local-events)
<events (om/get-shared owner :<events)]
(go-loop []
(let [subscriber (chan)]
(a/sub <local-events :click subscriber)
(<! subscriber)
(a/unsub <local-events :click subscriber)
(a/close! subscriber))
(om/set-state! owner :editing true)
(let [subscriber (chan)]
(a/sub <events :body-mousedown subscriber)
(loop []
(let [event (<! subscriber)]
(when (and (= (:type event) :body-mousedown)
(dom/click-on-self? (:event event) (om/get-node owner)))
(recur))))
(a/unsub <events :body-mousedown subscriber)
(a/close! subscriber))
(om/set-state! owner :editing false)
(recur))))))
(defn get-add-grub-events []
(let [out (chan)
recipe-add-grubs-clicks (dom/get-recipe-add-grubs-clicks)]
(go-loop []
(let [elem (<! recipe-add-grubs-clicks)
grub-texts (dom/-get-grubs elem)
grubs (map-indexed (fn [index g] {:id (str "grub-" (.now js/Date) index)
:grub g
:completed false})
grub-texts)
event {:event :add-grub-list
:grubs grubs}]
(>! out event))
(recur))
out))
(defn add-recipe [ch name grubs owner]
(when (and (not (empty? name))
(not (empty? grubs)))
(om/set-state! owner :new-recipe-name "")
(om/set-state! owner :new-recipe-grubs "")
(om/set-state! owner :editing false)
(put! ch (add-event name grubs))))
(defmulti handle-event (fn [event recipes] (:event event))
:default :unknown-event)
(defn new-recipe-view [_ owner]
(reify
om/IInitState
(init-state [_]
(let [publisher (chan)]
{:editing false
:>local-events publisher
:<local-events (a/pub publisher identity)
:new-recipe-name ""
:new-recipe-grubs ""}))
(defmethod handle-event :unknown-event [event recipes]
;(logs "Cannot handle unknown event:" event)
recipes)
om/IRenderState
(render-state [this {:keys [editing >local-events new-recipe-name new-recipe-grubs]}]
(let [add (om/get-shared owner :recipe-add)]
(html
[:div.panel.panel-default.recipe-panel
{:on-click #(put! >local-events :click)}
[:div.panel-heading.recipe-header
[:input.form-control.recipe-header-input
{:id "new-recipe-name"
:type "text"
:placeholder "New recipe"
:value new-recipe-name
:on-change #(om/set-state! owner :new-recipe-name (dom/event-val %))}]]
[:div.panel-body.recipe-grubs
{:class (when (not editing) "hidden")}
[:textarea.form-control.recipe-grubs-input
{:id "new-recipe-grubs"
:rows 3
:placeholder "Recipe ingredients"
:value new-recipe-grubs
:on-change #(om/set-state! owner :new-recipe-grubs (dom/event-val %))}]
[:button.btn.btn-primary.pull-right.recipe-btn.recipe-done-btn
{:type "button"
:on-click #(put! >local-events :done)}
"Done"]]])))
om/IWillMount
(will-mount [_]
(let [<local-events (om/get-state owner :<local-events)
<events (om/get-shared owner :<events)
add (om/get-shared owner :recipe-add)
]
(go-loop []
(let [subscriber (chan)]
(a/sub <local-events :click subscriber)
(<! subscriber)
(a/unsub <local-events :click subscriber)
(a/close! subscriber))
(om/set-state! owner :editing true)
(let [subscriber (chan)]
(a/sub <events :body-mousedown subscriber)
(a/sub <local-events :done subscriber)
(loop []
(let [event (<! subscriber)]
(if-not (and (= (:type event) :body-mousedown)
(dom/click-on-self? (:event event) (om/get-node owner)))
(when (= event :done)
(add-recipe add
(om/get-state owner :new-recipe-name)
(om/get-state owner :new-recipe-grubs)
owner))
(recur))))
(a/unsub <events :body-mousedown subscriber)
(a/unsub <local-events :done subscriber)
(a/close! subscriber))
(om/set-state! owner :editing false)
(recur))))))
(defmethod handle-event :add-recipe [event recipes]
(let [recipe (dom/add-new-recipe! (:id event)
(:name event)
(:grubs event))]
(assoc recipes (:id recipe) recipe)))
(defn assoc-new-recipe! [current new]
(assoc current (:id new)
(dom/add-new-recipe! (:id new) (:name new) (:grubs new))))
(defn add-new-recipes! [recipe-events]
(reduce assoc-new-recipe! {} recipe-events))
(defmethod handle-event :add-recipe-list [event recipes]
(let [add-recipe-events (:recipes event)
added-recipes (add-new-recipes! add-recipe-events)
new-recipes (merge recipes added-recipes)]
new-recipes))
(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))
(defn recipes-view [recipes owner]
(reify
om/IRender
(render [this]
(html
[:div
[:h3.recipes-title "Recipes"]
(om/build new-recipe-view recipes)
[:ul#recipe-list.list-group.recipe-list
(for [recipe (vals recipes)]
(om/build recipe-view recipe {:key :id}))]]))))

View file

@ -0,0 +1,87 @@
(ns grub.test.integration.core
(:require [grub.db :as db]
[grub.websocket :as ws]
[clj-webdriver.taxi :as taxi]
[clj-webdriver.core :as webdriver]
[clojure.test :as test]))
(def server-port 3456)
(def site-url (str "http://localhost:" server-port))
;; Hard-coded path to chromedriver
(defn set-chromedriver-path! []
(System/setProperty "webdriver.chrome.driver" "bin/chromedriver"))
(defn get-driver [url]
(webdriver/start {:browser :chrome} url))
(defn get-rand-grub []
(str "testgrub" (rand-int 10000)))
(defn add-grub [driver grub-text]
(taxi/input-text driver "#add-grub-input" grub-text)
(taxi/click driver {:text "Add"}))
(defn test-grubs-saved-to-server [url driver]
(taxi/to driver url)
(let [grubs (repeatedly 4 get-rand-grub)]
(doseq [grub grubs]
(add-grub driver grub))
(Thread/sleep 200)
(taxi/refresh driver)
(Thread/sleep 200)
(doseq [grub grubs]
(test/is (taxi/find-element driver {:text grub})
"Previously added grubs should be loaded on refresh")))
(db/clear-grubs))
(defn test-added-grubs-sync [url driver1 driver2]
(taxi/to driver1 url)
(taxi/to driver2 url)
(let [grubs (repeatedly 4 get-rand-grub)]
(doseq [grub grubs]
(add-grub driver1 grub))
(doseq [grub grubs]
(test/is (taxi/find-element driver2 {:text grub})
"Added grubs should appear in other browser"))))
(defn get-rand-recipe []
{:name (str "recipe" (rand-int 10000))
:grubs "grubs\nstuff\nmorestuff"})
(defn add-recipe [driver {:keys [name grubs]}]
(taxi/click driver "#new-recipe-name")
(taxi/input-text driver "#new-recipe-name" name)
(taxi/input-text driver "#new-recipe-grubs" grubs)
(taxi/click driver {:text "Done"}))
(defn test-added-recipes-sync [url driver1 driver2]
(taxi/to driver1 url)
(taxi/to driver2 url)
(let [recipes (repeatedly 4 get-rand-recipe )]
(doseq [recipe recipes]
(add-recipe driver1 recipe))
(doseq [{:keys [name]} recipes]
(test/is (taxi/find-element driver2 {:value name})
"Added recipes should appear in other browser"))))
(defn run-tests [site-url driver1 driver2]
(test-grubs-saved-to-server site-url driver1)
(test-added-grubs-sync site-url driver1 driver2)
(test-added-recipes-sync site-url driver1 driver2))
(defn start-db-and-websocket-server! []
(let [db-chan (db/connect-and-handle-events "grub-integration-test")]
(db/clear-all)
(ws/pass-received-events-to-clients-and-db db-chan)))
(defn run []
(println "Starting integration test")
(set-chromedriver-path!)
(start-db-and-websocket-server!)
(let [driver1 (get-driver site-url)
driver2 (get-driver site-url)]
(run-tests site-url driver1 driver2)
(taxi/quit driver1)
(taxi/quit driver2))
(db/clear-all))