diff --git a/src/re_frame/core.cljc b/src/re_frame/core.cljc index 67e5a7c74..d3e787914 100644 --- a/src/re_frame/core.cljc +++ b/src/re_frame/core.cljc @@ -213,6 +213,85 @@ can be created later, when a node is bought into existence by the use of `subscribe` in a `View Function`. + `reg-sub` arguments are: + - a `query-id` (typically a namespaced keyword) + - a function which returns the inputs required by this kind of node (can be supplied in one of three ways) + - a function which computes the value of this kind of node (can be supplied in one of three ways) + + The `computation function` is always the last argument supplied and has three ways to be called. + Two of these methods are syntactic sugar to provide easier access to functional abstractions around your data. + + 1. A function that will accept two parameters, the `input-values` and `query-vector`. This is the + standard way to provide a `computation-function` + #!clj + (reg-sub + :query-id + (fn [input-values query-vector] + (:foo input-values))) + + 2. A single sugary tuple of `:->` and a 1-arity `computation-function`: + #!clj + (reg-sub + :query-id + :-> computation-fn) + + This sugary variation allows you to pass a function that will expect only one parameter, + namely the `input-values` and entirely omit the `query-vector`. A typical `computation-function` + expects two pramenters which can cause unfortunate results when attempting to use + clojure standard library functions, or other functions, in a functional manner. + + For example, a significant number of subscriptions exist only to get a value + from the `input-values`. As shown below, this subscription will simply retrieve + the value associated with the `:foo` key in our db: + #!clj + (reg-sub + :query-id + (fn [db _] ;; :<---- trivial boilerplate we might want to skip over + (:foo db))) + + This is slightly more boilerplate than we might like to do, + as we can use a keyword directly as a function, and we might like to do this: + #!clj + (reg-sub + :query-id + :foo) ;; :<---- This could be dangerous. If `:foo` is not in db, we get the `query-vector` instead of `nil`. + + By using `:->` our function would not contain the `query-vector`, and any + missing keys would be represented as such: + #!clj + (reg-sub + :query-id + :-> :foo) + + This form allows us to ignore the `query-vector` if our `computation-function` + has no need for it, and be safe from any accidents. Any 1-arity function can be provided, + and for more complicated use cases, `partial`, `comp`, and anonymous functions can still be used. + + 3. A single sugary tuple of `:=>` and a multi-arity `computation-function` + #!clj + (reg-sub + :query-id + :=> computation-fn) + + The `query-vector` can be broken into two components `[query-id & optional-values]`, and + some subscriptions require the `optional-values` for extra work within the subscription. + To use them in variation #1, we need to destructure our `computation-function` parameters + in order to use them. + #!clj + (reg-sub + :query-id + (fn [db [_ foo]] + [db foo])) + + Again we are writing boilerplate just to reach our values, and we might prefer to + have direction access through a parameter vector like `[input-values optional-values]` + instead, so we might be able to use a multi-arity function directly as our `computation-function`. + A rewrite of the above sub using this sugary syntax would look like this: + #!clj + (reg-sub + :query-id + :=> vector) ;; :<---- Could also be `(fn [db foo] [db foo])` + The `computation function` is expected to take two arguments: - `input-values` - the values which flow into this node (how is it wired into the graph?) @@ -331,6 +410,16 @@ (fn [a query-vec] ;; only one pair, so 1st argument is a single value ...)) + Syntactic sugar for both the `signal-fn` and `computation-fn` can be used together + and the direction of arrows shows the flow of data and functions. The example from + directly above is reproduced here: + #!clj + (reg-sub + :a-b-sub + :<- [:a-sub] + :<- [:b-sub] + :-> (partial zipmap [:a :b])) + For further understanding, read the tutorials, and look at the detailed comments in /examples/todomvc/src/subs.cljs. diff --git a/src/re_frame/subs.cljc b/src/re_frame/subs.cljc index b967b06a5..6339592fa 100644 --- a/src/re_frame/subs.cljc +++ b/src/re_frame/subs.cljc @@ -147,12 +147,30 @@ (trace/merge-trace! {:tags {:input-signals (doall (to-seq (map-signals reagent-id signals)))}}) dereffed-signals)) - (defn reg-sub [query-id & args] - (let [computation-fn (last args) - input-args (butlast args) ;; may be empty, or one signal fn, or pairs of :<- / vector - err-header (str "re-frame: reg-sub for " query-id ", ") + (let [err-header (str "re-frame: reg-sub for " query-id ", ") + [input-args ;; may be empty, or one signal fn, or pairs of :<- / vector + computation-fn] (let [[op f :as comp-f] (take-last 2 args)] + (if (or (= 1 (count comp-f)) + (fn? op) + (vector? op)) + [(butlast args) (last args)] + (let [args (drop-last 2 args)] + (case op + ;; return a function that calls the computation fn + ;; on the input signal, removing the query vector + :-> + [args (fn [db _] + (f db))] + ;; return a function that calls the computation fn + ;; on the input signal and the data in the query vector + ;; that is not the query-id + :=> + [args (fn [db [_ & qs]] + (apply f db qs))] + ;; an incorrect keyword was passed + (console :error err-header "expected :-> or :=> as second to last argument, got:" op))))) inputs-fn (case (count input-args) ;; no `inputs` function provided - give the default 0 (fn diff --git a/test/re_frame/subs_test.cljs b/test/re_frame/subs_test.cljs index 6cc240174..2efa7ccde 100644 --- a/test/re_frame/subs_test.cljs +++ b/test/re_frame/subs_test.cljs @@ -254,6 +254,84 @@ (reset! db/app-db {:a 1 :b 2}) (is (= {:a 1 :b 2} @test-sub)))) +(deftest test-sub-macros--> + "test the syntactical sugar for input signal" + (subs/reg-sub + :a-sub + :-> :a) + + (subs/reg-sub + :b-sub + :-> :b) + + (subs/reg-sub + :c-sub + :-> :c) + + (subs/reg-sub + :d-sub + :-> :d) + + (subs/reg-sub + :d-first-sub + :<- [:d-sub] + :-> first) + + ;; variant of :d-first-sub without an input parameter + (subs/reg-sub + :e-first-sub + :-> (comp first :e)) + + ;; test for equality + (subs/reg-sub + :c-foo?-sub + :<- [:c-sub] + :-> #{:foo}) + + (subs/reg-sub + :a-b-sub + :<- [:a-sub] + :<- [:b-sub] + :-> (partial zipmap [:a :b])) + + (let [test-sub (subs/subscribe [:a-b-sub]) + test-sub-c (subs/subscribe [:c-foo?-sub]) + test-sub-d (subs/subscribe [:d-first-sub]) + test-sub-e (subs/subscribe [:e-first-sub])] + (is (= nil @test-sub-c)) + (reset! db/app-db {:a 1 :b 2 :c :foo :d [1 2] :e [3 4]}) + (is (= {:a 1 :b 2} @test-sub)) + (is (= :foo @test-sub-c)) + (is (= 1 @test-sub-d)) + (is (= 3 @test-sub-e)))) + +(deftest test-sub-macros-=> + "test the syntactical sugar for input signals and query vector arguments" + (subs/reg-sub + :a-sub + :-> :a) + + (subs/reg-sub + :b-sub + :-> :b) + + (subs/reg-sub + :test-a-sub + :<- [:a-sub] + :=> vector) + + ;; test for equality of input signal and query parameter + (subs/reg-sub + :test-b-sub + :<- [:b-sub] + :=> =) + + (let [test-a-sub (subs/subscribe [:test-a-sub :c]) + test-b-sub (subs/subscribe [:test-b-sub 2])] + (reset! db/app-db {:a 1 :b 2}) + (is (= [1 :c] @test-a-sub)) + (is (= true @test-b-sub)))) + (deftest test-registering-subs-doesnt-create-subscription (let [sub-called? (atom false)] (with-redefs [subs/subscribe (fn [& args] (reset! sub-called? true))]