8000 feat(reagent): add reagent integration · homebaseio/homebase-react@a08fabe · GitHub
[go: up one dir, main page]

Skip to content

Commit a08fabe

Browse files
committed
feat(reagent): add reagent integration
1 parent 87f37f4 commit a08fabe

File tree

10 files changed

+299
-13
lines changed

10 files changed

+299
-13
lines changed

deps.edn

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,10 @@
22
:deps {thheller/shadow-cljs {:mvn/version "2.11.25"}
33
devcards/devcards {:mvn/version "0.2.7"}
44
datascript/datascript {:mvn/version "1.0.7"}
5-
reagent/reagent {:mvn/version "1.0.0-alpha2"}
5+
reagent/reagent {:mvn/version "1.0.0"}
66
inflections/inflections {:mvn/version "0.13.2"}
77
binaryage/devtools {:mvn/version "1.0.2"}
88
homebaseio/datalog-console {:git/url "https://github.com/homebaseio/datalog-console" :sha "91d5b6009d66807ceec9807a1f8ed099a0a6f219"}
99
;; homebaseio/datalog-console {:local/root "../datalog-console"}
10+
nano-id {:mvn/version "1.0.0"}
1011
camel-snake-kebab/camel-snake-kebab {:mvn/version "0.4.2"}}}

src/dev/example/core.cljs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,12 @@
66
[cljsjs.react.dom]
77
[reagent.core]
88
[devcards.core :as dc]
9-
[dev.example.array]
10-
[dev.example.counter]
11-
[dev.example.todo]
12-
[dev.example.todo-firebase]))
9+
[dev.example.react.array]
10+
[dev.example.react.counter]
11+
[dev.example.react.todo]
12+
[dev.example.react.todo-firebase]
13+
[dev.example.reagent.counter]
14+
[dev.example.reagent.todo]))
1315

1416
(js/goog.exportSymbol "marked" marked)
1517
(js/goog.exportSymbol "DevcardsMarked" marked)

src/dev/example/array.cljs renamed to src/dev/example/react/array.cljs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
(ns dev.example.array
1+
(ns dev.example.react.array
22
(:require
33
[devcards.core :as dc]
44
[homebase.react]
5-
["./js_compiled/array" :as react-example])
5+
["../js_compiled/array" :as react-example])
66
(:require-macros
77
[devcards.core :refer [defcard-rg defcard-doc]]
88
[dev.macros :refer [inline-resource]]))

src/dev/example/counter.cljs renamed to src/dev/example/react/counter.cljs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
(ns dev.example.counter
1+
(ns dev.example.react.counter
22
(:require
33
[devcards.core :as dc]
44
[homebase.react]
5-
["./js_compiled/counter" :as react-example])
5+
["../js_compiled/counter" :as react-example])
66
(:require-macros
77
[devcards.core :refer [defcard-rg defcard-doc]]
88
[dev.macros :refer [inline-resource]]))

src/dev/example/todo.cljs renamed to src/dev/example/react/todo.cljs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
(ns dev.example.todo
1+
(ns dev.example.react.todo
22
(:require
33
[devcards.core :as dc]
44
[homebase.react]
5-
["./js_compiled/todo" :as react-example])
5+
["../js_compiled/todo" :as react-example])
66
(:require-macros
77
[devcards.core :refer [defcard-rg defcard-doc]]
88
[dev.macros :refer [inline-resource]]))

src/dev/example/todo_firebase.cljs renamed to src/dev/example/react/todo_firebase.cljs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
(ns dev.example.todo-firebase
1+
(ns dev.example.react.todo-firebase
22
(:require
33
[devcards.core :as dc]
44
[homebase.react]
5-
["./js_compiled/todo-firebase" :as react-example])
5+
["../js_compiled/todo-firebase" :as react-example])
66
(:require-macros
77
[devcards.core :refer [defcard-rg defcard-doc]]
88
[dev.macros :refer [inline-resource]]))

src/dev/example/reagent/counter.cljs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
(ns dev.example.reagent.counter
2+
(:require
3+
[devcards.core :as dc]
4+
[datascript.core :as d]
5+
[homebase.reagent :as hbr])
6+
(:require-macros
7+
[devcards.core :refer [defcard-rg]]))
8+
9+
(def db-conn (d/create-conn {}))
10+
(hbr/connect! db-conn)
11+
(d/transact! db-conn [[:db/add 1 :count 0]])
12+
13+
(defn counter []
14+
(let [e (hbr/entity db-conn 1)]
15+
;; (fn [])
16+
[:div
17+
"Count: " (:count @e)
18+
[:div
19+
[:button {:on-click #(d/transact! db-conn [[:db/add 1 :count (inc (:count @e))]])}
20+
"Increment"]]]))
21+
22+
(defcard-rg counter-example
23+
counter)

src/dev/example/reagent/todo.cljs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
(ns dev.example.reagent.todo
2+
(:require
3+
[devcards.core :as dc]
4+
[datascript.core :as d]
5+
[reagent.core :as r]
6+
[homebase.reagent :as hbr])
7+
(:require-macros
8+
[devcards.core :refer [defcard-rg]]))
9+
10+
(def db-conn (d/create-conn {}))
11+
(hbr/connect! db-conn)
12+
(d/transact! db-conn [{:todo/name "Do another thing"
13+
:todo/created-at (js/Date.now)}
14+
{:todo/name "Do a thing"
15+
:todo/created-at (js/Date.now)}])
16+
17+
#_(dotimes [n 1000]
18+
(d/transact! db-conn [{:todo/name (str n)
19+
:todo/created-at (js/Date.now)}]))
20+
21+
(defn todo [id]
22+
(let [todo (hbr/entity db-conn id)]
23+
(fn []
24+
[:div {:style {:display "flex" :flex-direction "row" :align-items "center"}}
25+
[:input
26+
{:type "checkbox"
27+
:checked (true? (:todo/completed? @todo))
28+
:on-change #(d/transact! db-conn [[:db/add (:db/id @todo) :todo/completed? (goog.object/getValueByKeys % #js ["target" "checked"])]])}]
29+
[:div {:style {:padding-left 6}}
30+
[:input
31+
{:type "text"
32+
:style {:border "none" :width "auto" :font-weight "bold"}
33+
:value (:todo/name @todo)
34+
:on-change #(d/transact! db-conn [[:db/add (:db/id @todo) :todo/name (goog.object/getValueByKeys % #js ["target" "value"])]])}]
35+
[:small {:style {:padding-left 6}}
36+
(.toString (js/Date. (:todo/created-at @todo)))]
37+
[:button
38+
{:on-click #(d/transact! db-conn [[:db/retractEntity (:db/id @todo)]])}
39+
"Delete"]]])))
40+
41+
(defn todos []
42+
(let [todos (hbr/q '[:find ?e ?t
43+
:where [?e :todo/created-at ?t]]
44+
db-conn)]
45+
(fn []
46+
[:div
47+
(doall
48+
(for [[id] (reverse (sort-by peek @todos))]
49+
^{:key id} [todo id]))])))
50+
51+
(defn new-todo []
52+
(let [name (r/atom "")]
53+
(fn []
54+
[:form {:on-submit (fn [e]
55+
(.preventDefault e)
56+
(d/transact! db-conn [{:todo/name @name
57+
:todo/created-at (js/Date.now)}])
58+
(reset! name ""))}
59+
[:input {:type "text"
60+
:on-change #(reset! name (goog.object/getValueByKeys % #js ["target" "value"]))
61+
:value @name
62+
:placeholder "Write a todo..."}]
63+
[:button {:type "submit"} "Create todo"]])))
64+
65+
(defn todo-app []
66+
[:div
67+
[new-todo]
68+
[todos]])
69+
70+
(defcard-rg todo-example
71+
todo-app)

src/homebase/cache.cljs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
(ns homebase.cache
2+
(:require
3+
[datascript.core :as datascript]
4+
[datascript.db]))
5+
6+
(defn create-conn []
7+
(atom
8+
{:ea {}
9+
:q {}}))
10+
11+
(defn assoc-ea
12+
[cache eid-attr-tuple component-uid change-handler]
13+
(assoc-in cache [:ea eid-attr-tuple component-uid] change-handler))
14+
15+
(defn dissoc-ea
16+
[cache eid-attr-tuple component-uid]
17+
(let [cache (update-in cache [:ea eid-attr-tuple] dissoc component-uid)]
18+
(if (empty? (get-in cache [:ea eid-attr-tuple]))
19+
(update cache :ea dissoc eid-attr-tuple)
20+
cache)))
21+
22+
(defn assoc-q
23+
[cache query component-uid change-handler]
24+
(assoc-in cache [:q query component-uid] change-handler))
25+
26+
(defn dissoc-q
27+
[cache query component-uid]
28+
(let [cache (update-in cache [:q query] dissoc component-uid)]
29+
(if (empty? (get-in cache [:q query]))
30+
(update cache :q dissoc query)
31+
cache)))
32+
33+
(defn create-listener
34+
"Returns a listener function that invokes all subscribed change-handlers in the cache when a datom is transacted."
35+
[cache-conn]
36+
(fn [{:keys [tx-data]}]
37+
(let [cache @cache-conn]
38+
;; EA handlers
39+
(doseq [[e a :as datom] tx-data]
40+
(let [subscriptions (get-in cache [:ea [e a]])]
41+
(doseq [[component-uid change-handler] subscriptions]
42+
(change-handler {:datom datom
43+
:component-uid component-uid}))))
44+
;; Query handlers
45+
;; TODO: dispatch on change-handlers more judiciously instead of on every transaction.
46+
;; See work on incremental view manintinence e.g. https://github.com/sixthnormal/clj-3df
47+
(let [subscriptions (map (comp flatten seq) (vals (:q cache)))]
48+
(doseq [[component-uid change-handler] subscriptions]
49+
(change-handler {:component-uid component-uid}))))))
50+
51+
(defn db-conn-type [db-conn]
52+
(if (instance? cljs.core/Atom db-conn)
53+
(type @db-conn)
54+
(type db-conn)))
55+
56+
(defmulti connect!
57+
"Connect the cache to a database connection and listen to changes in the transaction log."
58+
(fn [cache-conn db-conn] (db-conn-type db-conn)))
59+
(defmethod connect! datascript.db/DB [cache-conn db-conn]
60+
(swap! db-conn with-meta (merge (meta @db-conn) {::conn cache-conn}))
61+
(datascript/listen! db-conn ::connection (create-listener cache-conn)))
62+
63+
(defmulti disconnect!
64+
"Disconnect the transaction log listener."
65+
(fn [db-conn] (db-conn-type db-conn)))
66+
(defmethod disconnect! datascript.db/DB [db-conn]
67+
(swap! db-conn with-meta (dissoc (meta @db-conn) ::conn))
68+
(datascript/unlisten! db-conn ::connection))
69+
70+
(comment
71+
(do
72+
(def cache-conn (create-conn))
73+
(def db-conn (datascript/create-conn {}))
74+
(connect! cache-conn db-conn)
75+
(swap! cache-conn assoc-ea [1 :a] "abc123" #(print "yolo" %)))
76+
(datascript/transact! db-conn [{:a "a" :b "b" :c "c"}])
77+
(datascript/transact! db-conn [[:db/retract 1 :a]])
78+
(datascript/transact! db-conn [[:db/retractEntity 1]])
79+
(swap! cache-conn dissoc-ea [1 :a] "abc123")
80+
(disconnect! db-conn))

src/homebase/reagent.cljs

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
(ns homebase.reagent
2+
(:require
3+
[homebase.cache :as hbc]
4+
[datalog-console.chrome.formatters] ; Load the formatters ns to extend cljs-devtools to better render db entities in the chrome console if cljs-devtools is enabled.
5+
[devtools.protocols :as dtp :refer [IFormat]]
6+
[datascript.impl.entity :as de]
7+
[reagent.core :as r]
8+
[nano-id.core :refer [nano-id]]
9+
[datascript.core :as d]))
10+
11+
(declare lookup-entity)
12+
13+
(deftype Entity [^de/Entity entity meta]
14+
IFormat
15+
(-header [_] (dtp/-header entity))
16+
(-has-body [_] (dtp/-has-body entity))
17+
(-body [_] (dtp/-body entity))
18+
IMeta
19+
(-meta [_] meta)
20+
IWithMeta
21+
(-with-meta [_ new-meta] (Entity. entity new-meta))
22+
ILookup
23+
(-lookup [this attr] (lookup-entity this attr nil))
24+
(-lookup [this attr not-found] (lookup-entity this attr not-found))
25+
IAssociative
26+
(-contains-key? [this k] (not= ::nf (lookup-entity this k ::nf)))
27+
IFn
28+
(-invoke [this k] (lookup-entity this k nil))
29+
(-invoke [this k not-found] (lookup-entity this k not-found)))
30+
31+
(defn lookup-entity [^Entity entity attr not-found]
32+
(let [result (de/lookup-entity ^de/Entity (.-entity entity) attr not-found)
33+
after-lookup (::after-lookup (meta entity))]
34+
(when after-lookup (after-lookup {:entity entity :attr attr :result result}))
35+
(if (instance? de/Entity result)
36+
(Entity. result {::after-lookup after-lookup})
37+
result)))
38+
39+
(defn connect!
40+
"Connects a db-conn to a homebase.cache. This is a prerequisite for any of the db read functions in this namespace to be reactive. Returns a homebase.cache connection."
41+
[db-conn]
42+
(let [cache-conn (hbc/create-conn)]
43+
(hbc/connect! cache-conn db-conn)
44+
cache-conn))
45+
46+
(defn disconnect! [db-conn]
47+
(hbc/disconnect! db-conn))
48+
49+
(defn get-cache-conn-from-db [db]
50+
(let [cache-conn (:homebase.cache/conn (meta db))
51+
_ (when (not cache-conn)
52+
(throw (ex-info "Cache not connected. Connect your db to the cache with (homebase.reagent/connect! db-conn) first."
53+
{})))]
54+
cache-conn))
55+
56+
(defn make-reactive-entity [{:keys [^de/Entity entity r-entity tracked-ea-pairs db-conn cache-conn component-uid] :as args}]
57+
(let [entity-id (:db/id entity)
58+
e (Entity. entity {::after-lookup
59+
(fn [{:keys [attr]}]
60+
(swap! tracked-ea-pairs conj [entity-id attr])
61+
(swap! cache-conn hbc/assoc-ea [entity-id attr] component-uid
62+
(fn []
63+
(reset! r-entity
64+
(make-reactive-entity
65+
(merge args {:entity (d/entity @db-conn entity-id)}))))))})]
66+
e))
67+
68+
(defn entity
69+
"Returns a reactive homebase.reagent/Entity.
70+
71+
It offers a normalized subset of other entity APIs with the
72+
primary addition being that implemented protocols are reactive
73+
and trigger re-renders when related datoms change.
74+
75+
NOTE: This takes a conn, not a db."
76+
[db-conn lookup]
77+
(let [cache-conn (get-cache-conn-from-db @db-conn)
78+
entity (d/entity @db-conn lookup)
79+
component-uid (nano-id)
80+
tracked-ea-pairs (atom #{})
81+
r-entity (r/atom entity)
82+
hbr-entity (make-reactive-entity {:entity entity :r-entity r-entity :tracked-ea-pairs tracked-ea-pairs :db-conn db-conn :cache-conn cache-conn :component-uid component-uid})
83+
_ (reset! r-entity hbr-entity)
84+
f (fn []
85+
(r/with-let []
86+
@r-entity
87+
(finally ; handle unmounting this component
88+
(doseq [ea @tracked-ea-pairs]
89+
(swap! cache-conn hbc/dissoc-ea ea component-uid)
90+
#_(js/console.log ea @cache-conn)))))]
91+
(r/track f)))
92+
93+
(defn q
94+
"Returns a reactive query result that will trigger a re-render when its result changes. NOTE: This takes a conn, not a db."
95+
[query db-conn & inputs]
96+
(let [cache-conn (get-cache-conn-from-db @db-conn)
97+
result (apply d/q query @db-conn inputs)
98+
r-result (r/atom result)
99+
component-uid (nano-id)
100+
_ (swap! cache-conn hbc/assoc-q query component-uid
101+
(fn []
102+
(reset! r-result (apply d/q query @db-conn inputs))))
103+
f (fn []
104+
(r/with-let []
105+
@r-result
106+
(finally ; handle unmounting this component
107+
(swap! cache-conn hbc/dissoc-q query component-uid)
108+
#_(js/console.log query @cache-conn))))]
109+
(r/track f)))

0 commit comments

Comments
 (0)
0