diff --git a/project.clj b/project.clj index e6c668a..ba820ce 100644 --- a/project.clj +++ b/project.clj @@ -29,7 +29,7 @@ [karma-junit-reporter "0.3.8"]]} :cljsbuild {:builds [{:id "test" - :source-paths ["test"] + :source-paths ["test" "src"] :compiler {:output-to "run/compiled/test.js" :source-map "run/compiled/test.js.map" :output-dir "run/compiled/test" diff --git a/src/re_frame/core.cljs b/src/re_frame/core.cljs index 6310885..63ce2a0 100644 --- a/src/re_frame/core.cljs +++ b/src/re_frame/core.cljs @@ -14,6 +14,7 @@ (def dispatch-sync router/dispatch-sync) (def register-sub subs/register) +(def register-pure-sub subs/register-pure) (def clear-sub-handlers! subs/clear-handlers!) (def subscribe subs/subscribe) diff --git a/src/re_frame/subs.cljs b/src/re_frame/subs.cljs index 1467bd8..21c8bcc 100644 --- a/src/re_frame/subs.cljs +++ b/src/re_frame/subs.cljs @@ -1,5 +1,6 @@ (ns re-frame.subs (:require + [cljs.spec :as s] [reagent.ratom :as ratom :refer [make-reaction] :refer-macros [reaction]] [re-frame.db :refer [app-db]] [re-frame.utils :refer [first-in-vector warn error]])) @@ -10,11 +11,6 @@ ;; Maps from a query-id to a handler function. (def ^:private qid->fn (atom {})) -(defn clear-handlers! - "Unregisters all existing subscription handlers" - [] - (reset! qid->fn {})) - (defn register "Registers a subscription handler function for an query id" [query-id handler-fn] @@ -31,6 +27,12 @@ ;; test "=". (def ^:private query->reaction (atom {})) +(defn clear-handlers! + "Unregisters all existing subscription handlers" + [] + (reset! qid->fn {}) + (reset! query->reaction {})) + (defn cache-and-return "cache the reaction r" [v dynv r] @@ -61,7 +63,7 @@ cached) (let [query-id (first-in-vector v) handler-fn (get @qid->fn query-id)] - (warn "Subscription crerated: " v) + (warn "Subscription created: " v) (if-not handler-fn (error "re-frame: no subscription handler registered for: \"" query-id "\". Returning a nil subscription.")) (cache-and-return v [] (handler-fn app-db v))))) @@ -83,3 +85,93 @@ ;; need to double deref it to get to the actual value. (warn "Subscription created: " v dynv) (cache-and-return v dynv (reaction @@sub)))))))) + +;; -- Helper code for register-pure ------------------- + +(s/def ::register-pure-args (s/cat + :sub-name keyword? + :sub-fn (s/? fn?) + :arrow-args (s/* (s/cat :key #{:<-} :val vector?)) + :f fn?)) + +(defn- fmap + "Returns a new version of 'm' in which f has been applied to each value. + (fmap inc {:a 4, :b 2}) => {:a 5, :b 3}" + [f m] + (into {} (for [[k val] m] [k (f val)]))) + +(defn- multi-deref + "derefs a map sequence or a singleton" + [data] + (cond + (map? data) (fmap deref data) + (coll? data) (map deref data) + :else @data)) + +(defn register-pure + "This fn allows the user to write a 'pure' subscription + i.e. that is a subscription that operates on the values within app-db + rather than the atom itself + Note there are 3 ways this function can be called + + ```(register-pure + :test-sub + (fn [db [_]] db))``` + In this example the entire app-db is derefed and passed to the subscription + function as a singleton + + ```(subs/register-pure + :a-b-sub + (fn [q-vec d-vec] + [(subs/subscribe [:a-sub]) + (subs/subscribe [:b-sub])] + (fn [[a b] [_]] {:a a :b b}))``` + In this example the the first function is called with the query vector + and the dynamic vector as arguements the return value of this function + can be singleton reaction or a list or map of reactions. Note that `q-vec` + and `d-vec` can be destructured and used in the subscriptions (this is the point + actually). Again the subscriptions are derefed and passed to the subscription + function + + ```(subs/register-pure + :a-b-sub + :<- [:a-sub] + :<- [:b-sub] + (fn [[a b] [_]] {:a a :b b}))``` + In this example the convienent syntax of `:<-` is used to cover the majority + of cases where only a simple subscription is needed without any parameters + + " + [& args] + (let [conform (s/conform ::register-pure-args args) + {:keys [sub-name + sub-fn + arrow-args + f]} conform + arrow-subs (->> arrow-args + (map :val))] + (cond + sub-fn ;; first case the user provides a custom sub-fn + (register + sub-name + (fn [db q-vec d-vec] + (let [subscriptions (sub-fn q-vec d-vec)] ;; this let needs to be outside the fn + (ratom/make-reaction + (fn [] (f (multi-deref subscriptions) q-vec d-vec)))))) + arrow-args ;; the user uses the :<- sugar + (register + sub-name + (fn [db q-vec d-vec] + (let [subscriptions (map subscribe arrow-subs)] ;; this let needs to be outside the fn + (ratom/make-reaction + (fn [] (f (multi-deref subscriptions) q-vec d-vec)))))) + :else + (register ;; the simple case with no subs + sub-name + (fn [db q-vec d-vec] + (ratom/make-reaction (fn [] (f @db q-vec d-vec)))))))) + +#_(s/fdef register-pure + :args ::register-pure-args) + +#_(s/instrument #'register-pure) diff --git a/test/re-frame/test/subs.cljs b/test/re-frame/test/subs.cljs new file mode 100644 index 0000000..8e1d46a --- /dev/null +++ b/test/re-frame/test/subs.cljs @@ -0,0 +1,219 @@ +(ns re-frame.test.subs + (:require [cljs.test :refer-macros [is deftest]] + [reagent.ratom :refer-macros [reaction]] + [re-frame.subs :as subs] + [re-frame.db :as db] + [re-frame.core :as re-frame])) + +;=====test basic subscriptions ====== + +(deftest test-reg-sub + (subs/clear-handlers!) + + (subs/register + :test-sub + (fn [db [_]] (reaction (deref db)))) + + (let [test-sub (subs/subscribe [:test-sub])] + (is (= @db/app-db @test-sub)) + (reset! db/app-db 1) + (is (= 1 @test-sub)))) + +(deftest test-chained-subs + (subs/clear-handlers!) + + (subs/register + :a-sub + (fn [db [_]] (reaction (:a @db)))) + + (subs/register + :b-sub + (fn [db [_]] (reaction (:b @db)))) + + (subs/register + :a-b-sub + (fn [db [_]] + (let [a (subs/subscribe [:a-sub]) + b (subs/subscribe [:b-sub])] + (reaction {:a @a :b @b})))) + + (let [test-sub (subs/subscribe [:a-b-sub])] + (reset! db/app-db {:a 1 :b 2}) + (is (= {:a 1 :b 2} @test-sub)) + (swap! db/app-db assoc :b 3) + (is (= {:a 1 :b 3} @test-sub)))) + +(deftest test-sub-parameters + (subs/clear-handlers!) + + (subs/register + :test-sub + (fn [db [_ b]] (reaction [(:a @db) b]))) + + (let [test-sub (subs/subscribe [:test-sub :c])] + (reset! db/app-db {:a 1 :b 2}) + (is (= [1 :c] @test-sub)))) + + +(deftest test-sub-chained-parameters + (subs/clear-handlers!) + + (subs/register + :a-sub + (fn [db [_ a]] (reaction [(:a @db) a]))) + + (subs/register + :b-sub + (fn [db [_ b]] (reaction [(:b @db) b]))) + + (subs/register + :a-b-sub + (fn [db [_ c]] + (let [a (subs/subscribe [:a-sub c]) + b (subs/subscribe [:b-sub c])] + (reaction {:a @a :b @b})))) + + (let [test-sub (subs/subscribe [:a-b-sub :c])] + (reset! db/app-db {:a 1 :b 2}) + (is (= {:a [1 :c], :b [2 :c]} @test-sub)))) + +;============== test register-pure macros ================ + +(deftest test-reg-sub-macro + (subs/clear-handlers!) + + (subs/register-pure + :test-sub + (fn [db [_]] db)) + + (let [test-sub (subs/subscribe [:test-sub])] + (is (= @db/app-db @test-sub)) + (reset! db/app-db 1) + (is (= 1 @test-sub)))) + +(deftest test-reg-sub-macro-singleton + (subs/clear-handlers!) + + (subs/register-pure + :a-sub + (fn [db [_]] (:a db))) + + (subs/register-pure + :a-b-sub + (fn [_ _ _] + (subs/subscribe [:a-sub])) + (fn [a [_]] + {:a a})) + + (let [test-sub (subs/subscribe [:a-b-sub])] + (reset! db/app-db {:a 1 :b 2}) + (is (= {:a 1} @test-sub)) + (swap! db/app-db assoc :b 3) + (is (= {:a 1} @test-sub)))) + +(deftest test-reg-sub-macro-vector + (subs/clear-handlers!) + + (subs/register-pure + :a-sub + (fn [db [_]] (:a db))) + + (subs/register-pure + :b-sub + (fn [db [_]] (:b db))) + + (subs/register-pure + :a-b-sub + (fn [_ _ _] + [(subs/subscribe [:a-sub]) + (subs/subscribe [:b-sub])]) + (fn [[a b] [_]] + {:a a :b b})) + + (let [test-sub (subs/subscribe [:a-b-sub])] + (reset! db/app-db {:a 1 :b 2}) + (is (= {:a 1 :b 2} @test-sub)) + (swap! db/app-db assoc :b 3) + (is (= {:a 1 :b 3} @test-sub)))) + +(deftest test-reg-sub-macro-map + (subs/clear-handlers!) + + (subs/register-pure + :a-sub + (fn [db [_]] (:a db))) + + (subs/register-pure + :b-sub + (fn [db [_]] (:b db))) + + (subs/register-pure + :a-b-sub + (fn [_ _ _] + {:a (subs/subscribe [:a-sub]) + :b (subs/subscribe [:b-sub])}) + (fn [{:keys [a b]} [_]] + {:a a :b b})) + + (let [test-sub (subs/subscribe [:a-b-sub])] + (reset! db/app-db {:a 1 :b 2}) + (is (= {:a 1 :b 2} @test-sub)) + (swap! db/app-db assoc :b 3) + (is (= {:a 1 :b 3} @test-sub)))) + +(deftest test-sub-macro-parameters + (subs/clear-handlers!) + + (subs/register-pure + :test-sub + (fn [db [_ b]] [(:a db) b])) + + (let [test-sub (subs/subscribe [:test-sub :c])] + (reset! db/app-db {:a 1 :b 2}) + (is (= [1 :c] @test-sub)))) + +(deftest test-sub-macros-chained-parameters + (subs/clear-handlers!) + + (subs/register-pure + :a-sub + (fn [db [_ a]] [(:a db) a])) + + (subs/register-pure + :b-sub + (fn [db [_ b]] [(:b db) b])) + + (subs/register-pure + :a-b-sub + (fn [[_ c] _] + [(subs/subscribe [:a-sub c]) + (subs/subscribe [:b-sub c])]) + (fn [[a b] [_ c]] {:a a :b b})) + + (let [test-sub (subs/subscribe [:a-b-sub :c])] + (reset! db/app-db {:a 1 :b 2}) + (is (= {:a [1 :c] :b [2 :c]} @test-sub)))) + + +(deftest test-sub-macros-chained-parameters-<- + "test the syntactial sugar" + (subs/clear-handlers!) + + (subs/register-pure + :a-sub + (fn [db [_]] (:a db))) + + (subs/register-pure + :b-sub + (fn [db [_]] (:b db))) + + (subs/register-pure + :a-b-sub + :<- [:a-sub] + :<- [:b-sub] + (fn [[a b] [_ c]] {:a a :b b})) + + (let [test-sub (subs/subscribe [:a-b-sub :c])] + (reset! db/app-db {:a 1 :b 2}) + (is (= {:a 1 :b 2} @test-sub) )) + ) \ No newline at end of file diff --git a/test/re-frame/test/test_runner.cljs b/test/re-frame/test/test_runner.cljs index 778471b..8f64d83 100644 --- a/test/re-frame/test/test_runner.cljs +++ b/test/re-frame/test/test_runner.cljs @@ -1,11 +1,13 @@ (ns re-frame.test.runner (:require [jx.reporter.karma :as karma :include-macros true] [re-frame.test.middleware] - [re-frame.test.undo])) + [re-frame.test.undo] + [re-frame.test.subs])) (defn ^:export run [karma] (karma/run-tests karma 're-frame.test.middleware - 're-frame.test.undo)) + 're-frame.test.undo + 're-frame.test.subs))