The ClojureScript NYC DataScript Webinar (2014-12) video is long (nearly 2.5 hours) but quite an informative session of Tonsky assembling / going through his datascript-todo sample.
In the video, he starts with a mostly "static" version of his sample code. Over the course of the session he gradually transforms the sample into a more fully working version. In his editor, the left-most tab is the code being demonstrated and the right-most tab contains a more full version. Throughout the session, he copies code from the right-most tab to the left-most tab and adapts it appropriately. It can be helpful to pay attention to which tab is focused at any given time.
Since the video was made, the code has evolved further. One obvious difference is that the original code covered did not use Rum. So in a way, the code covered in the video might be a bit more accessible to a wider audience.
However, it might not correspond to any particular commit, but some commits that seemed close were this or this. This was determined by examing the code in various commits as well as what was in the video and comparing the dependency information in project.clj with what appeared at 04:40 in the video.
Specifically, that dependency information was:
[org.clojure/clojure "1.7.0-alpha4"]
[org.clojure/clojurescript "0.0-2411"]
[datascript "0.6.0"]
[sablono "0.2.22"]
[com.facebook/react "0.11.2"]
FWIW, near the end of the video, Tonsky briefly showed the datascript-todo github repository. One could see at that point that the latest commit was e9772a8.
(Note that kristianmandrup provided another set of notes regarding a later version of the code which corresponds to a version that uses Rum. That document is part of a larger collection of potentially useful information which has a very nice entry point which is not obvious from viewing the top-level README for that repository.)
-
multi-valued relations, references
(def schema {:todo/tags {:db/cardinality :db.cardinality/many} :todo/project {:db/valueType :db.type/ref}}) (defonce conn (d/create-conn schema))
-
(18:03) Ability to create todos
basic transact!
(defn extract-todo [] (when-let [text (dom/value (dom/q ".add-text"))] {:text text :project (dom/value (dom/q ".add-project")) :due (dom/date-value (dom/q ".add-due")) :tags (dom/array-value (dom/q ".add-tags"))})) (defn add-todo [] (when-let [todo (extract-todo)] (let [ent (->> {:todo/text (:text todo) :todo/due (:due todo) :todo/tags (:tags todo)} (u/remove-vals nil?))] (d/transact! conn [ent]))))
-
(25:09) Displaying list of todos
basic query
(r/defc todo-pane [db] [:.todo-pane (let [todos (d/q '[:find ?e :where [?e :todo/text _]] db)] (for [[eid] (->> todos (sort-by first)) :let [todo (d/entity db eid)]] [:.todo [:.todo-checkbox "✔︎"] [:.todo-text (:todo/text todo)] [:.todo-subtext (when-let [due (:todo/due todo)] [:span (.toDateString due)]) (for [tag (:todo/tags todo)] [:span tag])]]))])
-
(33:20) Persisting database to localStorage
serialization / deserialization
;; persisting DB between page reloads (d/listen! conn :persistence (fn [tx-report] (when-let [db (:db-after tx-report)] (js/localStorage.setItem "datascript/db" (pr-str db))))) ;; restoring once persisted DB on page load (cljs.reader/register-tag-parser! "datascript/DB" d/db-from-reader) (when-let [stored (js/localStorage.getItem "datascript/db")] (reset! conn (cljs.reader/read-string stored)))
-
transact fn
(defn toggle-todo [db eid] (let (done? (:todo/done (not done?)] [(:db/add eid :todo/done (not done?)]])) (r/defc todo-pane [db] [:.todo-pane (let [todos (d/q '[:find ?e :where [?e :todo/text _]] db)] (for [[eid] (->> todos (sort-by first)) :let [todo (d/entity db eid)]] [:.todo {:class (when (:todo/done (d/entity db eid)) "todo_done" } [:.todo-checkbox {:on-click (fn [_] (d/transact! conn [[:db.fn/call toggle-todo eid]]))} "✔︎"] [:.todo-text (:todo/text todo)] [:.todo-subtext (when-let [due (:todo/due todo)] [:span (.toDateString due)]) (for [tag (:todo/tags todo)] [:span tag])]]))]) ,,, (defn add-todo [] (when-let [todo (extract-todo)] (let [ent (->> {:todo/text (:text todo) :todo/done false :todo/due (:due todo) :todo/tags (:tags todo)} (u/remove-vals nil?))] (d/transact! conn [ent]))))
-
(48:11) Creating and displaying context and project on todos
entity navigation
(48:11) Creating and displaying tags on todos
multi-valued attrs
(r/defc todo-pane [db] [:.todo-pane (let [todos (d/q '[:find ?e :where [?e :todo/text _]] db)] (for [[eid] (->> todos (sort-by first)) :let [todo (d/entity db eid)]] [:.todo {:class (when (:todo/done (d/entity db eid)) "todo_done" } [:.todo-checkbox {:on-click (fn [_] (d/transact! conn [[:db.fn/call toggle-todo eid]]))} "✔︎"] [:.todo-text (:todo/text todo)] [:.todo-subtext (when-let [due (:todo/due todo)] [:span (.toDateString due)]) (when-let [project (:todo/project todo)] [:span (:project/name project)]) (for [tag (:todo/tags todo)] [:span tag])]]))]) ,,, (defn clean-todo [] (dom/set-value! (dom/q ".add-text") nil) (dom/set-value! (dom/q ".add-project") nil) (dom/set-value! (dom/q ".add-due") nil) (dom/set-value! (dom/q ".add-tags") nil)) (defn add-todo [] (when-let [todo (extract-todo)] (let [project (:project todo) project-id (when project (u/e-by-av @conn :project/name project)) project-tx (when (and project (nil? project-id)) [[:db/add -1 :project/name project]]) ent (->> {:db/id -2 :todo/text (:text todo) :todo/done false :todo/project (when project (or project-id -1)) :todo/due (:due todo) :todo/tags (:tags todo)} (u/remove-vals nil?))] (d/transact! conn (concat project-tx [ent]))) (clean-todo)))
-
(1:04:11) Make left panel display task counts: project, context
aggregate queries
(r/defc projects-group [db] [:.group [:.group-title "Projects"] (for [[name count] (d/q '[:find ?name (count ?todo) :with ?project :where [?todo :todo/project ?project] [?todo :todo/done false] [?project :project/name ?name]] db)] [:.group-item [:span name] [:span.group-item-count count]])])
-
(1:14:23) Make left panel display task count for inbox
"negate" query - no project, no context, no due date
(r/defc inbox-group [db] [:.group (let [count (->> (d/q '[:find (count ?todo) :where [?todo :todo/text _] [?todo :todo/done false] [(get-else $ ?todo :todo/project :none) ?project] [(get-else $ ?todo :todo/due :none) ?due] [(= ?project :none)] [(= ?due :none)]] db) ffirst)] [:.group-item [:span "Inbox"] (when count [:span.group-item-count count])])])
-
(1:22:46) Make left panel display "by month" grouping
custom fn call in a query
(r/defc plan-group [db] [:.group [:.group-title "Plan"] (for [[[year month] count] (->> (d/q '[:find ?month (count ?todo) :in $ ?date->month :where [?todo :todo/due ?date] [?todo :todo/done false] [(?date->month ?date) ?month]] db u/date->month) (sort-by first))] [:.group-item [:span (u/format-month month year)] [:span.group-item-count count]])])
-
(1:31:12) Make left panel navigable
storing housekeeping app state in a db
(defn set-system-attrs! [& args] (d/transact! conn (for [[attr value] (partition 2 args)] (if value [:db/add 0 attr value] [:db.fn/retractAttribute 0 attr])))) ,,, (defn all-todos [db] (->> (d/q '[:find ?e :where [?e :todo/text]] db) (map first) set)) (defmulti todos-by-group (fn [db group item] group)) (defmethod todos-by-group :inbox [db _ _] (->> (d/q '[:find ?todo :where [?todo :todo/text] [(get-else $ ?todo :todo/project :none) ?project] [(get-else $ ?todo :todo/due :none) ?due] [(= ?project :none)] [(= ?due :none)]] db) (map first) set)) (defmethod todos-by-group :project [db _ pid] (->> (d/q '[:find ?todo :in $ ?pid :where [?todo :todo/project ?pid]] db pid) (map first) set)) (defmethod todos-by-group :month [db _ [year month]] (->> (d/q '[:find ?todo :in $ [?from ?to] :where [?todo :todo/due ?due] [(<= ?from ?due ?to)]] db [(u/month-start month year) (u/month-end month year)]) (map first) set)) (r/defc todo-pane [db] [:.todo-pane (let [todos (all-todos db) todos (if-let (group (u/v-by-ea db 0 :system/group)] (let [item (u/v-by-ea db 0 :system/group-item)] (set/intersection todos (todos-by-group db group item))) todos)] (for [eid (sort todos) :let [todo (d/entity db eid)]] [:.todo {:class (when (:todo/done (d/entity db eid)) "todo_done") } [:.todo-checkbox {:on-click (fn [_] (d/transact! conn [[:db.fn/call toggle-todo eid]]))} "✔︎"] [:.todo-text (:todo/text todo)] [:.todo-subtext (when-let [due (:todo/due todo)] [:span (.toDateString due)]) (when-let [project (:todo/project todo)] [:span (:project/name project)]) (for [tag (:todo/tags todo)] [:span tag])]]))]) ,,, (r/defc inbox-group [db] [:.group (let [count (->> (d/q '[:find (count ?todo) :where [?todo :todo/text _] [?todo :todo/done false] [(get-else $ ?todo :todo/project :none) ?project] [(get-else $ ?todo :todo/due :none) ?due] [(= ?project :none)] [(= ?due :none)]] db) ffirst)] [:.group-item {:class (when (= :inbox (u/v-by-ea db 0 :system/group)) "group-item_selected")} [:span {:on-click (fn [_] (set-system-attrs! :system/group :inbox :system/group-item nil)) } "Inbox"] (when count [:span.group-item-count count])])]) (r/defc plan-group [db] [:.group [:.group-title "Plan"] (for [[[year month] count] (->> (d/q '[:find ?month (count ?todo) :in $ ?date->month :where [?todo :todo/due ?date] [?todo :todo/done false] [(?date->month ?date) ?month]] db u/date->month) (sort-by first))] [:.group-item {:class (when (and (= :month (u/v-by-ea db 0 :system/group)) (= [year month] (u/v-by-ea db 0 :system/group-item))) "group-item_selected")} [:span {:on-click (fn [_] (set-system-attrs! :system/group :month :system/group-item [year month]))} (u/format-month month year)] [:span.group-item-count count]])]) (r/defc projects-group [db] [:.group [:.group-title "Projects"] (for [[pid name count] (d/q '[:find ?project ?name (count ?todo) :where [?todo :todo/project ?project] [?todo :todo/done false] [?project :project/name ?name]] db)] [:.group-item {:class (when (and (= :month (u/v-by-ea db 0 :system/group)) (= [year month] (u/v-by-ea db 0 :system/group-item))) "group-item_selected")} [:span {:on-click (fn [_] (set-system-attrs! :system/group :project :system/group-item pid)) } name] [:span.group-item-count count]])])
-
(1:56:58) Filter bar to work as implicit OR
search by project, context, tags - rules
(r/defc filter-pane [] [:.filter-pane [:input.filter {:type "text" :on-change (fn [_] (set-system-attrs! :system/filter (dom/value (dom/q ".filter")))) :placeholder "Filter"}]]) ,,, (defn filter-terms [db] (not-empty (str/split (:system/filter (d/entity db 0)) #"\s+"))) (def filter-rule '[[(match ?todo ?term) [?todo :todo/project ?p] [?p :project/name ?term]] [(match ?todo ?term) [?todo :todo/tags ?term]]]) (defn todos-by-filter [db terms] (->> (d/q '[:find ?e :in $ % [?term ...] :where [?e :todo/text] (match ?e ?term)] db filter-rule terms) (map first) set)) (r/defc todo-pane [db] [:.todo-pane (let [todos (all-todos db) todos (if-let [ft (filter-terms db)] (set/intersection todos (todos-by-filter db ft)) todos) todos (if-let (group (u/v-by-ea db 0 :system/group)] (let [item (u/v-by-ea db 0 :system/group-item)] (set/intersection todos (todos-by-group db group item))) todos)] (for [eid (sort todos) :let [todo (d/entity db eid)]] [:.todo {:class (when (:todo/done (d/entity db eid)) "todo_done") } [:.todo-checkbox {:on-click (fn [_] (d/transact! conn [[:db.fn/call toggle-todo eid]]))} "✔︎"] [:.todo-text (:todo/text todo)] [:.todo-subtext (when-let [due (:todo/due todo)] [:span (.toDateString due)]) (when-let [project (:todo/project todo)] [:span (:project/name project)]) (for [tag (:todo/tags todo)] [:span tag])]]))])
-
(2:10:04) Filter bar to work as implicit AND
query construction (optional, if there will be time)
(There was a brief discussion, but no coding.)
-
The editor environment used in the video is LightTable. At the time of this writing, it's unclear how usable it still is.
-
The web browser that is used is Chrome.
-
One technique that Tonsky uses in the video for demonstrating things is to evaluate top-level code in the editor. From time to time, he copies a portion of code from a function body to the top-level and edits it (e.g. replaces parameters with global values, like: db -> @conn) before evaluation. This is quite effective, but seems quite laborious, error-prone, etc. It is one of my least favorite aspects of working with Clojure. IIUC other folks apply the "inline def" technique to "work around" it. Perhaps some day there will be a better approach to getting similar results.