diff --git a/src/cljs/reagent_data_views/client/component.clj b/src/cljs/reagent_data_views/client/component.clj index edcdecc..62a45ff 100644 --- a/src/cljs/reagent_data_views/client/component.clj +++ b/src/cljs/reagent_data_views/client/component.clj @@ -1,23 +1,25 @@ (ns reagent-data-views.client.component) -(defmacro def-views-component +(defmacro defvc [component-name args view-sigs & body] `(defn ~component-name ~args - (let [gen-view-sigs# (fn ~args ~view-sigs) - view-sigs-atom# (atom nil)] + (let [gen-view-sigs# (fn ~args ~view-sigs) + get-current-view-sigs# (fn [this#] + (apply gen-view-sigs# (rest (reagent.core/argv this#))))] (reagent.core/create-class {:component-will-mount (fn [this#] - (reset! view-sigs-atom# (apply gen-view-sigs# (rest (reagent.core/argv this#)))) - (reagent-data-views.client.core/subscribe! (deref view-sigs-atom#))) + (let [current-view-sigs# (get-current-view-sigs# this#)] + (reagent-data-views.client.component/subscribe! this# current-view-sigs#))) :component-will-unmount (fn [this#] - (reagent-data-views.client.core/unsubscribe! (deref view-sigs-atom#))) + (reagent-data-views.client.component/unsubscribe-all! this#)) :component-did-update (fn [this# old-argv#] - (reagent-data-views.client.core/update-view-component-sigs this# gen-view-sigs# view-sigs-atom#)) + (let [new-view-sigs# (get-current-view-sigs# this#)] + (reagent-data-views.client.component/update-subscriptions! this# new-view-sigs#))) :component-function (fn ~args diff --git a/src/cljs/reagent_data_views/client/component.cljs b/src/cljs/reagent_data_views/client/component.cljs new file mode 100644 index 0000000..e12e655 --- /dev/null +++ b/src/cljs/reagent_data_views/client/component.cljs @@ -0,0 +1,61 @@ +(ns reagent-data-views.client.component + (:require + [reagent.core :as r] + [reagent.impl.util :refer [reagent-component?]] + [reagent-data-views.client.core :as views] + [reagent-data-views.client.utils :refer [diff update-component-state!]])) + +(defn subscribe! + "Subscribes a component to the given view-sigs. + NOTE: this function is only intended to be used internally by defvc." + [this view-sigs] + (assert (reagent-component? this)) + (r/set-state this {:view-sigs view-sigs}) + (views/subscribe! view-sigs)) + +(defn unsubscribe-all! + "Unsubscribes a component from all it's current view subscriptions. + NOTE: this function is only intended to be used internally by defvc." + [this] + (assert (reagent-component? this)) + (when-let [view-sigs (:view-sigs (r/state this))] + (update-component-state! this #(dissoc % :view-sigs)) + (views/unsubscribe! view-sigs))) + +(defn update-subscriptions! + "Updates a component's view subscriptions to match the new full list + of view-sigs. Only changed view-sigs will cause a view subscription + change to occur. + NOTE: this function is only intended to be used internally by defvc." + [this new-view-sigs] + (assert (reagent-component? this)) + (let [current-view-sigs (:view-sigs (r/state this))] + (when (not= current-view-sigs new-view-sigs) + (let [sigs-to-sub (diff new-view-sigs current-view-sigs) + sigs-to-unsub (diff current-view-sigs new-view-sigs)] + (update-component-state! this #(assoc % :view-sigs new-view-sigs)) + (views/update-subscriptions! sigs-to-sub sigs-to-unsub))))) + +(defn- get-views-by-name [view-name view-sigs] + (filter #(= view-name (first %)) view-sigs)) + +(defn view-cursor + "Returns a Reagent cursor that can be used to access the data for a view by + looking up the corresponding view-sig by name in the current component's + list of view subscriptions. + + This function can only be used within the component's render function. + + If there are currently multiple subscriptions to views with the same name + (using different arguments) an error is thrown. + + NOTE: This function is intended to be used in a read-only manner. Using + this cursor to change the data will *not* propagate to the server or + any other clients currently subscribed to this view." + [view-name] + (assert (not (nil? (r/current-component)))) + (let [view-sigs (->> (r/current-component) (r/state) :view-sigs) + match (get-views-by-name view-name view-sigs)] + (if (> (count match) 1) + (throw (str "More then one view signature by the name \"" view-name "\" found.")) + (views/view-sig-cursor (first match))))) diff --git a/src/cljs/reagent_data_views/client/core.cljs b/src/cljs/reagent_data_views/client/core.cljs index 12d58ea..41eea9d 100644 --- a/src/cljs/reagent_data_views/client/core.cljs +++ b/src/cljs/reagent_data_views/client/core.cljs @@ -1,7 +1,8 @@ (ns reagent-data-views.client.core (:require [reagent.core :as r] - [clj-browserchannel-messaging.client :as browserchannel])) + [clj-browserchannel-messaging.client :as browserchannel] + [reagent-data-views.client.utils :refer [diff]])) ;; IMPORTANT NOTE: ;; We are using Reagent's built-in RCursor instead of the one provided by reagent-cursor @@ -14,59 +15,22 @@ (defonce view-data (r/atom {})) -; return items in b that don't exist in a -(defn- diff [a b] - (vec - (reduce - (fn [item-a item-b] - (remove #(= % item-b) item-a)) - a b))) +(defn view-sig-cursor + "Returns a Reagent cursor that can be used to access the data for the view + corresponding with the view-sig. -(declare update-subscriptions!) + Generally, for code in a component's render function, you should use + reagent-data-views.client.component/view-cursor instead of using this + function directly. -(defn update-view-component-sigs - "Not intended to be used outside of the def-view-component macro's - internal functionality." - [owner view-sig-gen-fn view-sigs-atom] - (let [new-args (rest (r/argv owner)) - old-sigs @view-sigs-atom - new-sigs (apply view-sig-gen-fn new-args)] - (when (not= old-sigs new-sigs) - (let [sigs-to-sub (diff new-sigs old-sigs) - sigs-to-unsub (diff old-sigs new-sigs)] - (update-subscriptions! sigs-to-sub sigs-to-unsub) - (if (not= old-sigs new-sigs) - (reset! view-sigs-atom new-sigs)))))) - -(defn get-view-sig-cursor - "Returns a Reagent cursor that can be used to access the data for this view. - NOTE: This is intended to be used in a read-only manner. Using this cursor - to change the data will *not* propagate to the server or any other - clients currently subscribed to this view." + NOTE: This function is intended to be used in a read-only manner. Using + this cursor to change the data will *not* propagate to the server or + any other clients currently subscribed to this view." [view-sig] (r/cursor [view-sig] view-data)) -(defn- get-views-by-name [view-name] - (filter - (fn [[view-sig _]] - (= view-name (first view-sig))) - @view-data)) - -(defn get-view-cursor - "Returns a Reagent cursor that can be used to access the data for the view-sig - with the specified name. If there is currently multiple subscriptions to views - with the same name (but different arguments), this will throw an error. - NOTE: This is intended to be used in a read-only manner. Using this cursor - to change the data will *not* propagate to the server or any other - clients currently subscribed to this view." - [view-name] - (let [view-sig (get-views-by-name view-name)] - (if (> (count view-sig) 1) - (throw (str "More then one view signature by the name \"" view-name "\" found.")) - (get-view-sig-cursor (ffirst view-sig))))) - (defn- add-initial-view-data! [view-sig data] - (let [cursor (get-view-sig-cursor view-sig)] + (let [cursor (view-sig-cursor view-sig)] (reset! cursor data))) (defn- remove-view-data! [view-sig] @@ -83,7 +47,7 @@ (concat existing-data insert-deltas)) (defn- apply-deltas! [view-sig deltas] - (let [cursor (get-view-sig-cursor view-sig)] + (let [cursor (view-sig-cursor view-sig)] (doseq [{:keys [refresh-set insert-deltas delete-deltas]} deltas] (if refresh-set (reset! cursor refresh-set)) (if (seq delete-deltas) (swap! cursor apply-delete-deltas delete-deltas)) diff --git a/src/cljs/reagent_data_views/client/utils.cljs b/src/cljs/reagent_data_views/client/utils.cljs new file mode 100644 index 0000000..b9d1abb --- /dev/null +++ b/src/cljs/reagent_data_views/client/utils.cljs @@ -0,0 +1,27 @@ +(ns reagent-data-views.client.utils + (:require + [reagent.core :as r] + [reagent.impl.component :as rcomp] + [reagent.impl.util :refer [reagent-component?]])) + +(defn diff + "Given two vectors a and b, returns a vector that contains only the + items from a that do not also exist in b." + [a b] + (->> b + (reduce + (fn [item-a item-b] + (remove #(= % item-b) item-a)) + a) + (vec))) + +; TODO: relies on internal Reagent functionality. state-atom is not officially +; part of the public Reagent API yet, but will probably be part of it in +; the future. this function may need to be updated at that time. +; https://github.com/reagent-project/reagent/issues/80#issuecomment-67302125 +(defn update-component-state! + "Updates the Reagent component's internal state atom by swap!-ing in the value + returned by the function f (which receives the current state atom's value)." + [owner f] + (assert (reagent-component? owner)) + (swap! (rcomp/state-atom owner) f)) \ No newline at end of file