├── .gitignore
├── .nrepl-port
├── README.md
├── project.clj
├── resources
└── public
│ └── index.html
├── script
└── figwheel.clj
└── src
└── posh_todo
├── categories.cljs
├── components.cljs
├── core.cljs
├── dashboard.cljs
├── db.cljs
├── tasks.cljs
└── util.cljs
/.gitignore:
--------------------------------------------------------------------------------
1 | ./resources/public/js
2 | /figwheel_server.log
3 | /resources/public/js/
4 | /target/
5 |
--------------------------------------------------------------------------------
/.nrepl-port:
--------------------------------------------------------------------------------
1 | 53623
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Todo
2 |
3 | This is a Todo application using
4 | [Posh](https://github.com/mpdairy/posh), a library that lets you
5 | easily use a DataScript database to keep your entire app state.
6 |
7 | This Todo app lets you add or delete tasks to different categories and
8 | view by checked/unchecked or by category.
9 |
10 | There's no styling and it's a really lame todo list, but I made it
11 | just to show an example of how to use Posh.
12 |
13 | You can see posh-todo in action here: http://otherway.org/posh-todo/
14 |
15 | ## Usage
16 |
17 | Clone it, then
18 |
19 | ```
20 | lein run -m clojure.main script/figwheel.clj
21 | ```
22 |
23 | Then go here in your browser:
24 |
25 | ```
26 | http://localhost:3449/
27 | ```
28 | ## Some nice components
29 |
30 | ### Checkbox
31 |
32 | If you have an entity with a boolean value that you want the user to
33 | be able to change, just load this component with the `id` and `attr`
34 | of the entity.
35 |
36 | ```clj
37 | (defn checkbox [conn id attr checked?]
38 | [:input
39 | {:type "checkbox"
40 | :checked checked?
41 | :onChange #(p/transact! conn [[:db/add id attr (not checked?)]])}])
42 | ```
43 |
44 | The component above would be called from another component that loads
45 | the entity and supplies the `checked?` value of the `attr`.
46 |
47 | If you wanted a standalone checkbox, you could query within the
48 | component:
49 |
50 | ```clj
51 | (defn checkbox [conn id attr]
52 | (let [checked? (attr @(p/pull conn [attr] id))]
53 | [:input
54 | {:type "checkbox"
55 | :checked checked?
56 | :onChange #(p/transact! conn [[:db/add id attr (not checked?)]])}]))
57 | ```
58 |
59 | ### Add Box
60 |
61 | This component loads a text input with an add button that calls the
62 | callback function with the value of the text box whenever the add
63 | button is clicked. It uses a local reagent atom to update the text
64 | box.
65 |
66 | ```clj
67 | (defn add-box [add-fn]
68 | (let [edit (r/atom "")]
69 | (fn [add-fn]
70 | [:span
71 | [:input
72 | {:type "text"
73 | :value @edit
74 | :onChange #(reset! edit (-> % .-target .-value))}]
75 | [:button
76 | {:onClick #(when-not (empty? @edit)
77 | (add-fn @edit)
78 | (reset! edit ""))}
79 | (or (:button-text options) "Add")]])))
80 | ```
81 |
82 | The add-fn would be something like `(partial add-task! conn
83 | category-id)` where `add-task!` is:
84 |
85 | ```clj
86 | (defn add-task!
87 | [conn category-id task-name]
88 | (util/new-entity! conn {:task/name task-name
89 | :task/category category-id
90 | :task/done false}))
91 | ```
92 |
93 | This adds a new task to a category in the todo.
94 |
95 |
96 | ## License
97 |
98 | Copyright © 2015 Matt Parker
99 |
100 | Distributed under the Eclipse Public License either version 1.0 or (at
101 | your option) any later version.
102 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject posh-todo "0.1.0-SNAPSHOT"
2 | :description "An example of a Todo using Posh"
3 | :url "http://github.com/mpdairy/posh/"
4 | :license {:name "Eclipse Public License"
5 | :url "http://www.eclipse.org/legal/epl-v10.html"}
6 | :dependencies [[org.clojure/clojure "1.8.0"]
7 | [org.clojure/clojurescript "1.7.228"]
8 | [org.clojure/core.match "0.3.0-alpha4"]
9 | [datascript "0.15.0"]
10 | [posh "0.5"]
11 | [reagent "0.6.0-rc"]
12 | [figwheel-sidecar "0.5.0-SNAPSHOT" :scope "test"]]
13 | :plugins [[lein-cljsbuild "1.1.3"]]
14 | :cljsbuild {
15 | :builds [ {:id "posh-todo"
16 | :source-paths ["src/"]
17 | :figwheel false
18 | :compiler {:main "posh-todo.core"
19 | :asset-path "js"
20 | :output-to "resources/public/js/main.js"
21 | :output-dir "resources/public/js"} } ]
22 | })
23 |
--------------------------------------------------------------------------------
/resources/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Posh Todo example
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/script/figwheel.clj:
--------------------------------------------------------------------------------
1 | (require '[figwheel-sidecar.repl :as r]
2 | '[figwheel-sidecar.repl-api :as ra])
3 |
4 | (ra/start-figwheel!
5 | {:figwheel-options {}
6 | :build-ids ["dev"]
7 | :all-builds
8 | [{:id "dev"
9 | :figwheel true
10 | :source-paths ["src"]
11 | :compiler {:main 'posh-todo.core
12 | :asset-path "js"
13 | :output-to "resources/public/js/main.js"
14 | :output-dir "resources/public/js"
15 | :verbose true}}]})
16 |
17 | (ra/cljs-repl)
18 |
--------------------------------------------------------------------------------
/src/posh_todo/categories.cljs:
--------------------------------------------------------------------------------
1 | (ns posh-todo.categories
2 | (:require [posh.reagent :as p]
3 | [posh-todo.util :as util]
4 | [posh-todo.tasks :as tasks]
5 | [posh-todo.components :as comp]
6 | [posh-todo.dashboard :as dash]))
7 |
8 | ;; todo components
9 |
10 | (defn delete-category [conn category-id]
11 | (let [category @(p/pull conn [:category/name] category-id)]
12 | [comp/stage-button
13 | [(str "Delete \"" (:category/name category) "\" Category") "This will delete all its tasks, ok?"]
14 | #(p/transact! conn [[:db.fn/retractEntity category-id]])]))
15 |
16 | (defn category-panel [conn todo-id]
17 | (let [c @(p/q '[:find ?c .
18 | :in $ ?t
19 | :where
20 | [?t :todo/display-category ?c]]
21 | conn
22 | todo-id)]
23 |
24 | ;;'[:q [:find ?c . :in $ ?t :where [?t :todo/display-category ?c]] ([:db :conn0] 1)]
25 | (if (not c)
26 | [dash/dashboard conn todo-id]
27 | [:div
28 | [:h2 [comp/editable-label conn c :category/name]]
29 | [delete-category conn c]
30 | [tasks/task-panel conn c]
31 | ;[add-task c]
32 | ])))
33 |
34 | (defn add-category!
35 | [conn todo-id category-name]
36 | (util/new-entity! conn {:category/name category-name :category/todo todo-id}))
37 |
38 | (defn add-new-category [conn todo-id]
39 | [:div "Add new category: " [comp/add-box conn (partial add-category! conn todo-id)]])
40 |
41 | (defn category-item [conn todo-id category]
42 | [:button
43 | {:on-click #(p/transact!
44 | conn
45 | [[:db/add todo-id :todo/display-category (:db/id category)]])}
46 | (:category/name category)
47 | " (" (count (:task/_category category)) ")"])
48 |
49 | (defn category-menu [conn todo-id]
50 | (let [cats (->> @(p/pull conn
51 | '[{:category/_todo [:db/id :category/name {:task/_category [:db/id]}]}]
52 | todo-id)
53 | :category/_todo
54 | (sort-by :category/name))]
55 | [:span
56 | (for [c cats]
57 | ^{:key (:db/id c)}
58 | [category-item conn todo-id c])]))
59 |
--------------------------------------------------------------------------------
/src/posh_todo/components.cljs:
--------------------------------------------------------------------------------
1 | (ns posh-todo.components
2 | (:require [posh.reagent :as p]
3 | [posh-todo.db :as db :refer [conn]]
4 | [reagent.core :as r]
5 | [posh-todo.util :as util]))
6 |
7 | ;;; General Purpose Components
8 |
9 | ;;;;; input box that sends the value of the text back to add-fn
10 |
11 | (defn add-box [conn add-fn]
12 | (let [edit (r/atom "")]
13 | (fn [conn add-fn]
14 | [:span
15 | [:input
16 | {:type "text"
17 | :value @edit
18 | :onChange #(reset! edit (-> % .-target .-value))}]
19 | [:button
20 | {:on-click #(when-not (empty? @edit)
21 | (add-fn @edit)
22 | (reset! edit ""))}
23 | "Add"]])))
24 |
25 | ;;;;; edit box
26 |
27 | (defn edit-box [conn edit-id id attr]
28 | (let [edit @(p/pull conn [:edit/val] edit-id)]
29 | [:span
30 | [:input
31 | {:type "text"
32 | :value (:edit/val edit)
33 | :onChange #(p/transact! conn [[:db/add edit-id :edit/val (-> % .-target .-value)]])}]
34 | [:button
35 | {:on-click #(p/transact! conn [[:db/add id attr (:edit/val edit)]
36 | [:db.fn/retractEntity edit-id]])}
37 | "Done"]
38 | [:button
39 | {:on-click #(p/transact! conn [[:db.fn/retractEntity edit-id]])}
40 | "Cancel"]]))
41 |
42 | (defn editable-label [conn id attr]
43 | (let [val (attr @(p/pull conn [attr] id))
44 | edit @(p/q '[:find ?edit .
45 | :in $ ?id ?attr
46 | :where
47 | [?edit :edit/id ?id]
48 | [?edit :edit/attr ?attr]]
49 | conn id attr)]
50 | (if-not edit
51 | [:span val
52 | [:button
53 | {:on-click #(util/new-entity! conn {:edit/id id :edit/val val :edit/attr attr})}
54 | "Edit"]]
55 | [edit-box conn edit id attr])))
56 |
57 | ;;; check box
58 |
59 | (defn checkbox [conn id attr checked?]
60 | [:input
61 | {:type "checkbox"
62 | :checked checked?
63 | :onChange #(p/transact! conn [[:db/add id attr (not checked?)]])}])
64 |
65 | ;; stage button
66 |
67 | (defn stage-button [stages finish-fn]
68 | (let [stage (r/atom 0)]
69 | (fn [stages finish-fn]
70 | (when (= @stage (count stages))
71 | (do (finish-fn)
72 | (reset! stage 0)))
73 | [:button
74 | {:on-click #(swap! stage inc)
75 | :onMouseOut #(reset! stage 0)}
76 | (nth stages @stage)])))
77 |
78 |
79 |
80 |
--------------------------------------------------------------------------------
/src/posh_todo/core.cljs:
--------------------------------------------------------------------------------
1 | (ns posh-todo.core
2 | (:require [reagent.core :as r]
3 | [posh.reagent :as p]
4 | [datascript.core :as d]
5 | [posh-todo.db :as db :refer [conn]]
6 | [posh-todo.util :as util :refer [tempid]]
7 | [posh-todo.categories :as cats]
8 | [posh-todo.dashboard :as dash]
9 | [posh-todo.components :as comp]
10 | [posh.lib.update :as u]))
11 |
12 | (enable-console-print!)
13 |
14 | ;;; setup
15 |
16 | (db/populate! conn)
17 |
18 | (p/posh! conn)
19 |
20 | ;(p/pull conn '[*] [:task/name "Mop Floors"])
21 | (defn testdog [conn]
22 | (let [floors @(p/pull conn '[*] [:task/name "Mop Floors"])]
23 | [:div
24 | {:on-click
25 | #(p/transact! conn [[:db/add (:db/id floors) :task/done (not (:task/done floors))]])}
26 | "Hey guys"
27 | (pr-str floors)
28 | ])
29 |
30 | )
31 |
32 | (defn app [conn todo-id]
33 | (let [todo @(p/pull conn '[:todo/name] [:todo/name "Matt's List"])]
34 | [:div
35 | [testdog conn]
36 | [:h1 (:todo/name todo)]
37 | [dash/dashboard-button conn todo-id]
38 | [cats/category-menu conn todo-id]
39 | [cats/add-new-category conn todo-id]
40 | [cats/category-panel conn todo-id]
41 | [:div
42 | {:on-click #(println
43 | "cache: "
44 | ;;(:cache @(p/get-posh-atom conn))
45 | (u/update-q-with-dbvarmap-debug
46 | @(p/get-posh-atom conn)
47 | '[:q
48 | [:find ?cat ?cat_name ?task_cat
49 | :in $ ?t
50 | :where
51 | [?t :task/category ?task_cat]
52 | [?task_cat :category/todo ?todo]
53 | [?cat :category/todo ?todo]
54 | [?cat :category/name ?cat_name]]
55 | ([:db :conn0] 5)]))}
56 | "hey"]]))
57 |
58 |
59 | (defn start [conn]
60 | (let [todo-id (d/q '[:find ?todo . :where [?todo :todo/name _]] @conn)]
61 | (r/render-component
62 | [app conn todo-id]
63 | (.getElementById js/document "app"))))
64 |
65 | (start conn)
66 |
67 |
--------------------------------------------------------------------------------
/src/posh_todo/dashboard.cljs:
--------------------------------------------------------------------------------
1 | (ns posh-todo.dashboard
2 | (:require [posh.reagent :as p]
3 | [posh-todo.db :as db :refer [conn]]
4 | [posh-todo.util :as util]
5 | [posh-todo.tasks :as tasks]
6 | [posh-todo.components :as comp]))
7 |
8 |
9 | (defn dashboard-category [conn todo-id category]
10 | [:div
11 | [:button
12 | {:on-click #(p/transact!
13 | conn
14 | [[:db/add todo-id :todo/display-category (:db/id category)]])}
15 | (:category/name category)] " (" (count (:task/_category category)) ")"])
16 |
17 | (defn delete-listed [conn tasks]
18 | [comp/stage-button
19 | ["Delete Listed" "Are you sure?" "They'll be gone forever, ok?"]
20 | #(p/transact! conn (map (fn [t] [:db.fn/retractEntity t]) tasks))])
21 |
22 | (defn category-select [conn task-id]
23 | (let [cats @(p/q '[:find ?cat ?cat_name ?task_cat :in $ ?t
24 | :where
25 | [?t :task/category ?task_cat]
26 | [?task_cat :category/todo ?todo]
27 | [?cat :category/todo ?todo]
28 | [?cat :category/name ?cat_name]]
29 | conn task-id)]
30 | [:span
31 | [:select {:on-change #(p/transact!
32 | conn
33 | [[:db/add task-id :task/category
34 | (cljs.reader/read-string (.. % -target -value))]])
35 | :default-value (nth (first cats) 2)}
36 | (for [c cats]
37 | ^{:key (first c)} [:option {:value (first c)} (second c)])]]))
38 |
39 | (defn dash-task [conn task-id]
40 | (let [task @(p/pull conn '[:db/id :task/done :task/pinned :task/name
41 | {:task/category [:db/id :category/name]}]
42 | task-id)]
43 | [:span
44 | [comp/checkbox conn task-id :task/done (:task/done task)]
45 | [comp/editable-label conn task-id :task/name]
46 | [comp/stage-button ["X" "X?"]
47 | (fn [] (p/transact! conn [[:db.fn/retractEntity task-id]]))]
48 | [category-select conn task-id]]))
49 |
50 | (defn task-list [conn todo-id]
51 | (let [listing (-> @(p/pull conn [:todo/listing] todo-id)
52 | :todo/listing)
53 | tasks (case listing
54 | :all @(p/q '[:find [?t ...]
55 | :in $ ?todo
56 | :where
57 | [?c :category/todo ?todo]
58 | [?t :task/category ?c]]
59 | conn todo-id)
60 | @(p/q '[:find [?t ...]
61 | :in $ ?todo ?done
62 | :where
63 | [?c :category/todo ?todo]
64 | [?t :task/category ?c]
65 | [?t :task/done ?done]]
66 | conn todo-id (= listing :done)))]
67 | [:div
68 | [:h3 (case listing
69 | :all "All Tasks"
70 | :done "Completed Tasks"
71 | :not-done "Uncompleted Tasks")]
72 | (if-not (empty? tasks)
73 | [:div
74 | (for [t tasks]
75 | ^{:key t} [:div [dash-task conn t]])
76 | [delete-listed conn tasks]]
77 | [:div "None"])]))
78 |
79 | (defn change-listing! [conn todo-id v]
80 | (p/transact! conn [[:db/add todo-id :todo/listing v]]))
81 |
82 | (defn listing-buttons [conn todo-id]
83 | [:div
84 | [:button
85 | {:on-click #(change-listing! conn todo-id :all)}
86 | "All"]
87 | [:button
88 | {:on-click #(change-listing! conn todo-id :done)}
89 | "Checked"]
90 | [:button
91 | {:on-click #(change-listing! conn todo-id :not-done)}
92 | "Un-checked"]])
93 |
94 | (defn dashboard [conn todo-id]
95 | (let [cats (->> @(p/pull conn
96 | '[{:category/_todo [:db/id :category/name {:task/_category [:db/id]}]}]
97 | todo-id)
98 | :category/_todo
99 | (sort-by :category/name))]
100 | [:div
101 | [:h2 "DASHBOARD: "] [listing-buttons conn todo-id]
102 | [task-list conn todo-id]]))
103 |
104 | (defn dashboard-button [conn todo-id]
105 | (let [current-category (-> @(p/pull conn [:todo/display-category] todo-id)
106 | :todo/display-category
107 | :db/id)]
108 | [:button
109 | {:on-click #(p/transact!
110 | conn
111 | (if current-category
112 | [[:db/retract todo-id :todo/display-category current-category]
113 | [:db/add todo-id :todo/listing :all]]
114 | []))}
115 | "Dashboard"]))
116 |
--------------------------------------------------------------------------------
/src/posh_todo/db.cljs:
--------------------------------------------------------------------------------
1 | (ns posh-todo.db
2 | (:require [datascript.core :as d]
3 | [posh-todo.util :as util :refer [tempid]]))
4 |
5 | (def schema {:task/category {:db/valueType :db.type/ref}
6 | :category/todo {:db/valueType :db.type/ref}
7 | :todo/display-category {:db/valueType :db.type/ref}
8 | :task/name {:db/unique :db.unique/identity}
9 | :todo/name {:db/unique :db.unique/identity}
10 | :action/editing {:db/cardinality :db.cardinality/many}})
11 |
12 | (def conn (d/create-conn schema))
13 |
14 | (defn populate! [conn]
15 | (let [todo-id (util/new-entity! conn {:todo/name "Matt's List" :todo/listing :all})
16 | at-home (util/new-entity! conn {:category/name "At Home" :category/todo todo-id})
17 | work-stuff (util/new-entity! conn {:category/name "Work Stuff" :category/todo todo-id})
18 | hobby (util/new-entity! conn {:category/name "Hobby" :category/todo todo-id})]
19 | (d/transact!
20 | conn
21 | [{:db/id (tempid)
22 | :task/name "Clean Dishes"
23 | :task/done true
24 | :task/category at-home}
25 | {:db/id (tempid)
26 | :task/name "Mop Floors"
27 | :task/done true
28 | :task/pinned true
29 | :task/category at-home}
30 | {:db/id (tempid)
31 | :task/name "Draw a picture of a cat"
32 | :task/done false
33 | :task/category hobby}
34 | {:db/id (tempid)
35 | :task/name "Compose opera"
36 | :task/done true
37 | :task/category hobby}
38 | {:db/id (tempid)
39 | :task/name "stock market library"
40 | :task/done false
41 | :task/pinned true
42 | :task/category work-stuff}])))
43 |
44 |
--------------------------------------------------------------------------------
/src/posh_todo/tasks.cljs:
--------------------------------------------------------------------------------
1 | (ns posh-todo.tasks
2 | (:require [posh.reagent :as p]
3 | [posh-todo.db :as db :refer [conn]]
4 | [posh-todo.util :as util]
5 | [posh-todo.components :as comp]))
6 |
7 | (defn task [conn task-id]
8 | (let [task @(p/pull conn '[:task/done :task/pinned] task-id)]
9 | [:span [comp/checkbox conn task-id :task/done (:task/done task)]
10 | [comp/editable-label conn task-id :task/name]
11 | [comp/stage-button ["X" "X?"]
12 | (fn [] (p/transact! conn [[:db.fn/retractEntity task-id]]))]]))
13 |
14 | (defn add-task!
15 | [conn category-id task-name]
16 | (util/new-entity! conn {:task/name task-name
17 | :task/category category-id
18 | :task/done false}))
19 |
20 | (defn task-panel [conn category-id]
21 | (let [c @(p/pull conn
22 | '[:category/name {:task/_category [:db/id]}]
23 | category-id)
24 | cat-name (:category/name c)
25 | tasks (:task/_category c)]
26 | (println "TASK PANEL: " category-id)
27 | [:div
28 | [:div "Add new task to \"" cat-name "\": "
29 | ^{:key category-id} [comp/add-box conn (partial add-task! conn category-id)]]
30 | (for [t tasks]
31 | ^{:key (:db/id t)} [:div [task conn (:db/id t)]])]))
32 |
--------------------------------------------------------------------------------
/src/posh_todo/util.cljs:
--------------------------------------------------------------------------------
1 | (ns posh-todo.util
2 | (:require [datascript.core :as d]))
3 |
4 | ;;; util
5 | (defn pairmap [pair] (apply merge (map (fn [[a b]] {a b}) pair)))
6 |
7 | (defn ents [db ids] (map (partial d/entity db) ids))
8 |
9 | (defn new-entity! [conn varmap]
10 | ((:tempids (d/transact! conn [(merge varmap {:db/id -1})])) -1))
11 |
12 | ;;; setup
13 |
14 | (def tempid (let [n (atom 0)] (fn [] (swap! n dec))))
15 |
--------------------------------------------------------------------------------