├── .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 | --------------------------------------------------------------------------------