Om Next subcomponents using DataScript store

Introduction

Example of using Om Next subcomponents with a DataScript store. This new attempt has better hierarchy, using secretary routing with one root component (App) responding to changes in the DataScript store. The database changes, which are done in the secretary routing functions via an Om Next mutate, are manually calling the parser function, thus avoiding direct knowledge of underlying storage mechanism (see routing.cljs in the listings below).

This example code is on github: https://github.com/maridonkers/om-next-datascript

Updates:

project.clj

 1(defproject browser "0.0.0-SNAPSHOT"
 2  :description "browser"
 3  :dependencies [[org.clojure/clojure "1.7.0"]
 4                 [org.clojure/clojurescript "1.7.228"]
 5                 [org.omcljs/om "1.0.0-alpha28"]
 6                 [datascript "0.15.0"]
 7
 8                 [secretary "1.2.3"]
 9
10                 [com.cemerick/piggieback "0.2.1"]
11                 [figwheel-sidecar "0.5.0-3" :scope "test"]])

script/figwheel.clj

 1(require '[figwheel-sidecar.repl :as r]
 2         '[figwheel-sidecar.repl-api :as ra])
 3
 4;; In Emacs use M-x cider-jack-in to start REPL and then C-c C-k) to
 5;; load this file and start figwheel.
 6;;
 7;; (Beware: piggieback must have been added to Leiningen dependencies, e.g.
 8;; as follows: [com.cemerick/piggieback "0.2.1"])
 9;;
10;; See documentation at:
11;; https://github.com/bhauman/lein-figwheel/wiki/Using-the-Figwheel-REPL-within-NRepl
12;;
13(ra/start-figwheel!
14 {:figwheel-options {}
15  :build-ids ["dev"]
16  :all-builds
17  [{:id "dev"
18    :figwheel true
19    :source-paths ["src"]
20    :compiler {:main 'browser.core
21               :asset-path "js"
22               :output-to "resources/public/js/main.js"
23               :output-dir "resources/public/js"
24               :verbose true}}]})
25
26(ra/cljs-repl)

src/browser/ds.cljs

 1(ns browser.ds
 2  (:require [datascript.core :as d]))
 3
 4;; Database schema (only type ref entities need be specified).
 5(def schema {:app/dimensions {:db/valueType :db.type/ref}})
 6
 7;; Database connection.
 8(def conn (d/create-conn schema))
 9
10;; Log database transactions for debug purposes. BEWARE: nil as a value
11;; is not allowed and should not show up in logs!
12(d/listen! conn :log
13           (fn [tx-report]
14             (println (str "DS: " (:tx-data tx-report)))))
15
16;; Initial contents of (in-memory) database.
17(defn init!
18  "Initializes database contents."
19  []
20  {:post [(not (nil?  %))]}
21  (d/transact! conn
22               [{:db/id -1
23                 :navbar/key :navbar
24                 :navbar/collapsed? true}
25
26                {:db/id -2
27                 :home/key :home
28                 :home/title "HOME (to be done)"
29                 :home/count 0}
30
31                {:db/id -3
32                 :about/key :about
33                 :about/title "ABOUT (to be done)"}
34
35                {:db/id -4
36                 :about/key :error
37                 :about/title "ERROR (to be done)"}
38
39                {:db/id -100
40                 :app/key :app
41                 :app/page "/"
42                 :app/locale :nl-NL
43                 :app/logged-in? true
44                 :app/dimensions {:db/id -1000
45                                  :dimensions/orientation :landscape
46                                  :dimensions/width 1024
47                                  :dimensions/height 768}}]))
48
49;;---------------------
50;; Initialize database.
51(init!)

src/browser/core.cljs

 1(ns browser.core
 2  (:require [browser.util :as util]
 3            [browser.routing :as routing]))
 4
 5(enable-console-print!)
 6
 7;; -------------------------
 8;; Set-up.
 9(routing/hook-browser-navigation!)
10(routing/om-next-root!)
11(routing/restore-page!)

src/browser/util.cljs

1(ns browser.util)
2
3(defn nav!
4  "Navigates to supplied page by updating the URL."
5  [url]
6  (set! (.. js/document -location -href) (str "#" url)))

src/browser/reconciler.cljs

 1(ns browser.reconciler
 2  (:require [om.next :as om]
 3
 4            [browser.ds :as ds]))
 5
 6;; -------------------------
 7;; The Om Next read functions
 8(defmulti read
 9  "Read data from DataScript store."
10  om/dispatch)
11
12;; -----------------------------
13;; The Om Next mutate functions.
14(defmulti mutate
15  "Mutate data in DataScript store."
16  om/dispatch)
17
18;; -------------------
19;; The Om Next parser.
20;;
21(def parser (om/parser {:read read :mutate mutate}))
22
23;; -------------------------
24;; Configures Om Next read and mutate functions.
25(def reconciler
26  (om/reconciler
27   {:state ds/conn
28    :parser parser}))

src/browser/routing.cljs

 1(ns browser.routing
 2  (:require [goog.dom :as gdom]
 3            [om.next :as om]
 4            [datascript.core :as d]
 5
 6            [secretary.core :as secretary :include-macros true]
 7            [goog.events :as events]
 8            [goog.history.EventType :as EventType]
 9
10            [browser.ds :as ds]
11            [browser.util :as util]
12            [browser.reconciler :refer [reconciler parser]]
13
14            [browser.app :refer [App]])
15  (:import goog.History))
16
17;;-------------
18;; Change page.
19;;
20;; Use Om Next parser to avoid direct knowledge of underlying storage.
21;;
22(defn set-page!
23  "Sets page in Om Next data."
24  [new-page]
25
26  (let [app-props (parser {:state ds/conn}
27                          [{:app/query [:db/id :app/page]}])
28        entity (get-in app-props [:app/query 0])
29        {:keys [db/id
30                app/page]} entity]
31
32    (when (not= page new-page)
33      (parser {:state ds/conn}
34              `[(app/set-page ~{:db/id id :app/page new-page})]))))
35
36;; -------
37;; Routes.
38;; Extend when pages added. Also see case-statement
39;; in browser.app component.
40;;
41(secretary/set-config! :prefix "#")
42
43(secretary/defroute home-page "/" []
44  (set-page! "/")) 
45
46(secretary/defroute about-page "/about" []
47  (set-page! "/about"))
48
49;; --------
50;; History.
51;; must be called after routes have been defined.
52(defn hook-browser-navigation!
53  "Connects browser navigation to secretary routing."
54  []
55  (doto (History.)
56    (events/listen
57     EventType/NAVIGATE
58     (fn [event]
59       (secretary/dispatch! (.-token event))))
60    (.setEnabled true)))
61
62(defn om-next-root!
63  "Sets Om Next root component."
64  []
65  (om/add-root! reconciler
66                App (gdom/getElement "app")))
67
68(defn restore-page!
69  "Restores saved page (if any); otherwise home page."
70  []
71
72  (if-let [url (d/q '[:find ?p .
73                      :where [?e :app/page ?p]] @ds/conn)]
74    (util/nav! url)))

src/browser/app.cljs

 1(ns browser.app
 2  (:require [om.next :as om :refer-macros [defui]]
 3            [om.dom :as dom]
 4            [secretary.core :as secretary]
 5
 6            [browser.parsers.app :as app-parser]
 7
 8            [browser.navbar :refer [Navbar navbar]]
 9            [browser.pages.home :refer [HomePage home-page]]
10            [browser.pages.about :refer [AboutPage about-page]]
11            [browser.pages.error :refer [ErrorPage error-page]]))
12
13;;------------------
14;; Om Next component
15;;
16;; This defines dimensions.
17(defui Dimensions
18  static om/IQuery
19  (query [this]
20         [:db/id
21          :dimensions/orientation
22          :dimensions/width
23          :dimensions/height]))
24
25;;------------------------
26;; Om Next root component.
27;;
28(defui App
29  static om/IQuery
30  (query [this]
31         [{:app/query [:db/id
32                       :app/page
33                       :app/locale
34                       :app/logged-in?
35                       {:app/dimensions (om/get-query Dimensions)}]}
36
37          {:navbar/query (om/get-query Navbar)}
38          {:home/query (om/get-query HomePage)}
39          {:about/query (om/get-query AboutPage)}
40          {:error/query (om/get-query ErrorPage)}])
41
42  Object
43  (render [this]
44          (let [props (om/props this)
45
46                app-props (get-in (om/props this) [:app/query 0])
47                navbar-props (get-in (om/props this) [:navbar/query 0])
48                home-props (get-in (om/props this) [:home/query 0])
49                about-props (get-in (om/props this) [:about/query 0])
50                error-props (get-in (om/props this) [:error/query 0])
51
52                {:keys [db/id
53                        app/page
54                        app/locale
55                        app/logged-in?]} app-props]
56
57            (dom/div nil
58                     (navbar (om/computed navbar-props
59                                          {:app-id id
60                                           :lc locale
61                                           :logged-in? logged-in?}))
62
63                     ;; Extend this when new pages are added. Also see routes
64                     ;; in browser.routing component.
65                     ;;
66                     (case page
67                       "/" (home-page home-props)
68                       "/about" (about-page about-props)
69                       (error-page error-props))))))

src/browser/navbar.cljs

 1(ns browser.navbar
 2  (:require [om.next :as om :refer-macros [defui]]
 3            [om.dom :as dom]
 4
 5            [browser.util :as util]
 6
 7            [browser.parsers.navbar :as navbar-parser]))
 8
 9;;-------------------
10;; Om Next component.
11(defui Navbar
12  static om/IQuery
13  (query [this]
14         [:db/id
15          :navbar/collapsed?])
16  Object
17  (render
18   [this]
19   (dom/div
20    nil
21    (let [props (om/props this)
22
23          {:keys [navbar/collapsed?]} props
24
25          cmp (om/get-computed props)
26          {:keys [app-id
27                  lc
28                  logged-in?]} cmp]
29
30      (when logged-in?
31        (dom/button
32         #js {:type "button"
33              :onClick (fn [e]
34                         (util/nav! "/")
35                         (let [entity {:db/id app-id}]
36                   (om/transact! this
37                                 `[(app/logout ~entity)])))}
38         "Logout!"))))))
39
40(def navbar (om/factory Navbar))

src/browser/pages/home.cljs

 1(ns browser.pages.home
 2  (:require [om.next :as om :refer-macros [defui]]
 3            [om.dom :as dom]
 4            [datascript.core :as d]
 5
 6            [browser.util :as util]
 7
 8            [browser.parsers.home :as home-parser]))
 9
10;; ------------------------------------
11;; Om Next component for the home page.
12(defui HomePage
13  static om/IQuery
14  (query [this]
15         [:db/id :home/title :home/count])
16  Object
17  (render [this]
18          (let [props (om/props this)
19
20                {:keys [db/id
21                        home/title
22                        home/count]} props]
23
24            (dom/div
25             nil
26             (dom/h2 nil title)
27             (dom/span nil (str "Home (count): " count))
28             (dom/button
29              #js {:type "button"
30                   :onClick (fn [e]
31                              (util/nav! "/about")
32                              (let [entity {:db/id id :home/count count}]
33                                (om/transact! this
34                                              `[(home/increment ~entity)])))}
35              "Increment!")))))
36
37(def home-page (om/factory HomePage))

src/browser/pages/about.cljs

 1(ns browser.pages.about
 2  (:require [om.next :as om :refer-macros [defui]]
 3            [om.dom :as dom]
 4            [datascript.core :as d]
 5
 6            [browser.util :as util]
 7
 8            [browser.parsers.about :as about-parser]))
 9
10;; ------------------------------------
11;; Om Next component for the about page.
12(defui AboutPage
13  static om/IQuery
14  (query [this]
15         [:db/id :about/title])
16  Object
17  (render [this]
18          (let [props (om/props this)
19
20                {:keys [about/title]} props]
21
22            (dom/div
23             nil
24             (dom/h2 nil title)
25             (dom/button
26              #js {:type "button"
27                   :onClick (fn [e]
28                              (util/nav! "/"))}
29              "HOME!")))))
30
31(def about-page (om/factory AboutPage))

src/browser/pages/error.cljs

 1(ns browser.pages.error
 2  (:require [om.next :as om :refer-macros [defui]]
 3            [om.dom :as dom]
 4            [datascript.core :as d]
 5
 6            [browser.util :as util]
 7
 8            [browser.parsers.error :as error-parser]))
 9
10;; -------------------------------------
11;; Om Next component for the error page.
12(defui ErrorPage
13  static om/IQuery
14  (query [this]
15         [:db/id :error/title])
16  Object
17  (render [this]
18          (let [props (om/props this)
19
20                {:keys [error/title]} props]
21
22            (dom/div
23             nil
24             (dom/h2 nil title)
25             (dom/button
26              #js {:type "button"
27                   :onClick (fn [e]
28                              (util/nav! "/"))}
29              "HOME!")))))
30
31(def error-page (om/factory ErrorPage))

src/browser/parsers/app.cljs

 1(ns browser.parsers.app
 2  (:require [om.next :as om]
 3            [datascript.core :as d]
 4
 5            [browser.reconciler :refer [mutate read]]))
 6
 7(defmethod read :app/query
 8  [{:keys [state query]} _ _]
 9
10  {:value (d/q '[:find [(pull ?e ?selector) ...]
11                 :in $ ?selector
12                 :where [?e :app/key]]
13               (d/db state) query)})
14
15(defmethod mutate 'app/set-page
16  [{:keys [state]} _ entity]
17
18  {:value {:keys [:app/query]}
19   :action (fn []
20             (d/transact! state
21                          [entity]))}) ;; new value in entity
22
23(defmethod mutate 'app/login
24  [{:keys [state]} _ entity]
25
26  {:value {:keys [:app/query]}
27   :action (fn []
28             (d/transact! state
29                          [(assoc entity :app/logged-in? true)]))})
30
31(defmethod mutate 'app/logout
32  [{:keys [state]} _ entity]
33
34  {:value {:keys [:app/query]}
35   :action (fn []
36             (d/transact! state
37                          [(assoc entity :app/logged-in? false)]))})

src/browser/parsers/navbar.cljs

 1(ns browser.parsers.navbar
 2  (:require [om.next :as om]
 3            [datascript.core :as d]
 4
 5            [browser.reconciler :refer [mutate read]]))
 6
 7(defmethod read :navbar/query
 8  [{:keys [state query]} _ _]
 9
10  {:value (d/q '[:find [(pull ?e ?selector) ...]
11                 :in $ ?selector
12                 :where [?e :navbar/key]]
13               (d/db state) query)})

src/browser/parsers/home.cljs

 1(ns browser.parsers.home
 2  (:require [om.next :as om]
 3            [datascript.core :as d]
 4
 5            [browser.reconciler :refer [mutate read]]))
 6
 7;;-----------------------
 8;; Parser read functions.
 9(defmethod read :home/query
10  [{:keys [state query]} _ _]
11
12  {:value (d/q '[:find [(pull ?e ?selector) ...]
13                 :in $ ?selector
14                 :where [?e :home/key]]
15               (d/db state) query)})
16
17;;-------------------------
18;; Parser mutate functions.
19(defmethod mutate 'home/increment
20  [{:keys [state]} _ entity]
21
22  {:value {:keys [:home/query]}
23   :action (fn []
24             (d/transact! state
25                          [(update-in entity [:home/count] inc)]))})

src/browser/parsers/about.cljs

 1(ns browser.parsers.about
 2  (:require [om.next :as om]
 3            [datascript.core :as d]
 4
 5            [browser.reconciler :refer [mutate read]]))
 6
 7;;-----------------------
 8;; Parser read functions.
 9(defmethod read :about/query
10  [{:keys [state query]} _ _]
11
12  {:value (d/q '[:find [(pull ?e ?selector) ...]
13                 :in $ ?selector
14                 :where [?e :about/key]]
15               (d/db state) query)})
16
17;;-------------------------
18;; Parser mutate functions.

src/browser/parsers/error.cljs

 1(ns browser.parsers.error
 2  (:require [om.next :as om]
 3            [datascript.core :as d]
 4
 5            [browser.reconciler :refer [mutate read]]))
 6
 7;;-----------------------
 8;; Parser read functions.
 9(defmethod read :error/query
10  [{:keys [state query]} _ _]
11
12  {:value (d/q '[:find [(pull ?e ?selector) ...]
13                 :in $ ?selector
14                 :where [?e :error/key]]
15               (d/db state) query)})
16
17;;-------------------------
18;; Parser mutate functions.

src/browser/resources/public/index.html

 1<html>
 2  <head lang="en">
 3    <META http-equiv="Content-Type" content="text/html; charset=UTF-8">
 4    <meta name="viewport" content="width=device-width, initial-scale=1">
 5    <title>Welcome</title>
 6  </head>
 7  <body>
 8    <div id="app"></div>
 9    <script src="js/main.js"></script>
10  </body>
11</html>

Posts in this Series

Translations: