Skip to content

Instantly share code, notes, and snippets.

@sogaiu
Last active June 13, 2021 06:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save sogaiu/17705652cc872de6cb0f5b4d7f84e207 to your computer and use it in GitHub Desktop.
Save sogaiu/17705652cc872de6cb0f5b4d7f84e207 to your computer and use it in GitHub Desktop.
tonsky's datascript-todo video notes

Notes on Tonsky's datascript-todo Code Walkthrough

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.

Regarding the Specific Code in the Video

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.)

Steps with Links

  • (15:09) Create DB schema

    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)))
    
  • (39:34) Make task completable

    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.)

  • (2:11:04) Questions

Miscellaneous Notes

  • 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment