From f2012fdc29ac9b8b31c132293ff25f130b18444a Mon Sep 17 00:00:00 2001 From: gered Date: Sun, 28 Dec 2014 14:51:35 -0500 Subject: [PATCH] significant rework to defvc, simplifying sub/unsubing to views defvc components no longer declare the view-sigs that will be used. calls to view-cursor within the component's render function now automatically trigger the component to subscribe/unsubscribe to the view-sigs being passed to any view-cursor calls. --- .../reagent_data_views/client/component.clj | 56 +++++++----- .../reagent_data_views/client/component.cljs | 87 +++++++++---------- src/cljs/reagent_data_views/client/core.cljs | 24 ++--- src/cljs/reagent_data_views/client/utils.cljs | 11 --- 4 files changed, 91 insertions(+), 87 deletions(-) diff --git a/src/cljs/reagent_data_views/client/component.clj b/src/cljs/reagent_data_views/client/component.clj index 62a45ff..a8f31b6 100644 --- a/src/cljs/reagent_data_views/client/component.clj +++ b/src/cljs/reagent_data_views/client/component.clj @@ -1,26 +1,42 @@ (ns reagent-data-views.client.component) (defmacro defvc - [component-name args view-sigs & body] - `(defn ~component-name ~args - (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#] - (let [current-view-sigs# (get-current-view-sigs# this#)] - (reagent-data-views.client.component/subscribe! this# current-view-sigs#))) + "Defines a Reagent component that works the same as any other defined + using defn with the addition that view-cursor can be used in the + render function of these components to access data from subscribed + views. - :component-will-unmount - (fn [this#] - (reagent-data-views.client.component/unsubscribe-all! this#)) + To subscribe to a view, simply call view-cursor with the signature of + the view you want to subscribe to and read data from. view-cursor and + components defined with defvc will automatically manage subscribing + and unsubscribing to views as the view signatures passed to any + view-cursor calls change across the lifetime of this component." + [component-name args & body] + `(defn ~component-name [] + (reagent.core/create-class + {:component-will-mount + (fn [this#] + (reagent-data-views.client.component/prepare-for-render! this#)) - :component-did-update - (fn [this# old-argv#] - (let [new-view-sigs# (get-current-view-sigs# this#)] - (reagent-data-views.client.component/update-subscriptions! this# new-view-sigs#))) + :component-did-mount + (fn [this#] + ; invoked immediately after the initial render has occurred. + ; we do this here because component-did-mount does not get called + ; after the initial render, but will be after all subsequent renders. + (reagent-data-views.client.component/update-subscriptions! this#)) - :component-function - (fn ~args - ~@body)})))) + :component-will-unmount + (fn [this#] + (reagent-data-views.client.component/unsubscribe-all! this#)) + + :component-will-receive-props + (fn [this# new-argv#] + (reagent-data-views.client.component/prepare-for-render! this#)) + + :component-did-update + (fn [this# old-argv#] + (reagent-data-views.client.component/update-subscriptions! this#)) + + :component-function + (fn ~args + ~@body)}))) diff --git a/src/cljs/reagent_data_views/client/component.cljs b/src/cljs/reagent_data_views/client/component.cljs index 02c594c..b308a24 100644 --- a/src/cljs/reagent_data_views/client/component.cljs +++ b/src/cljs/reagent_data_views/client/component.cljs @@ -1,63 +1,62 @@ (ns reagent-data-views.client.component (:require + [clojure.set :refer [difference]] [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)) + [reagent-data-views.client.utils :refer [update-component-state!]])) (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))) + (let [last-used-view-sigs (:last-used-view-sigs (r/state this))] + (views/unsubscribe! last-used-view-sigs) + (update-component-state! this #(dissoc % :used-view-sigs :last-used-view-sigs)))) + +(defn prepare-for-render! + "Prepares the used-view/last-used-view sigs state for the upcoming + component render. + NOTE: this function is only intended to be used internally by defvc." + [this] + (assert (reagent-component? this)) + (let [{:keys [used-view-sigs]} (r/state this)] + (r/set-state this {:used-view-sigs #{} + :last-used-view-sigs (or used-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. + "Updates view subscriptions by checking what view-sigs were passed to + any view-cursor calls during the most recent render and comparing + against the view-sigs that were used during the previous render. + Automatically subscribes to new view-sigs and unsubscribes from old + ones only as is needed. NOTE: this function is only intended to be used internally by defvc." - [this new-view-sigs] + [this] (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)) + (let [{:keys [used-view-sigs last-used-view-sigs]} (r/state this)] + (if (not= used-view-sigs last-used-view-sigs) + (let [sigs-to-unsub (vec (difference last-used-view-sigs used-view-sigs)) + sigs-to-sub (vec (difference used-view-sigs last-used-view-sigs))] + (views/update-subscriptions! sigs-to-sub sigs-to-unsub) + (r/set-state this {:used-view-sigs #{} + :last-used-view-sigs used-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. + "Returns a Reagent cursor that can be used to access the data for a view. + If the view-sig is not currently subscribed to, the subscription will be + added automatically by the containing component, but this function will + return a cursor pointing to nil data until the server sends the initial + data for the new subscription (at which point a re-render is triggered). - This function can only be used within the component's render function. + This function can only be used with the render function of a component + defined using defvc. - 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) - num-matches (count match)] - (case num-matches - 1 (views/view-sig-cursor (first match)) - 0 (throw (str "No matching view signature by the name \"" view-name "\".")) - (throw (str "More then one view signature by the name \"" view-name "\" found."))))) + NOTE: The data returned by this function is intended to be used in a + read-only manner. Using this cursor to change the data will *not* + propagate the changes to the server." + [view-sig] + (let [this (r/current-component)] + (assert (not (nil? this)) "view-cursor can only be used within a defvc component's render function.") + (update-component-state! this #(update-in % [:used-view-sigs] conj view-sig)) + (views/->view-sig-cursor view-sig))) \ No newline at end of file diff --git a/src/cljs/reagent_data_views/client/core.cljs b/src/cljs/reagent_data_views/client/core.cljs index 4013c31..4ebffa3 100644 --- a/src/cljs/reagent_data_views/client/core.cljs +++ b/src/cljs/reagent_data_views/client/core.cljs @@ -1,8 +1,7 @@ (ns reagent-data-views.client.core (:require [reagent.core :as r] - [clj-browserchannel-messaging.client :as browserchannel] - [reagent-data-views.client.utils :refer [diff]])) + [clj-browserchannel-messaging.client :as browserchannel])) ;; IMPORTANT NOTE: ;; We are using Reagent's built-in RCursor instead of the one provided by reagent-cursor @@ -15,17 +14,18 @@ (defonce view-data (r/atom {})) -(defn view-sig-cursor - "Returns a Reagent cursor that can be used to access the data for the view - corresponding with the view-sig. +(defn ->view-sig-cursor + "Creates and returns a Reagent cursor that can be used to access the data + for the view corresponding with the view-sig. 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. + function directly. Use of this function instead requires you to manage + view subscription/unsubscription yourself. - 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." + NOTE: The data returned by this function is intended to be used in a + read-only manner. Using this cursor to change the data will *not* + propagate the changes to the server." [view-sig] (r/cursor [view-sig :data] view-data)) @@ -40,7 +40,7 @@ (get-in @view-data path))) (defn- add-initial-view-data! [view-sig data] - (let [cursor (view-sig-cursor view-sig)] + (let [cursor (->view-sig-cursor view-sig)] (reset! cursor data))) (defn- remove-view-data! [view-sig] @@ -57,7 +57,7 @@ (concat existing-data insert-deltas)) (defn- apply-deltas! [view-sig deltas] - (let [cursor (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)) @@ -85,7 +85,7 @@ (defn subscribe! "Subscribes to the specified view(s). Updates to the data on the server will - be automatically pushed out. Use get-data-cursor to read this data and + be automatically pushed out. Use a 'view cursor' to read this data and render it in any component(s)." [view-sigs] (doseq [view-sig view-sigs] diff --git a/src/cljs/reagent_data_views/client/utils.cljs b/src/cljs/reagent_data_views/client/utils.cljs index b9d1abb..0a2cade 100644 --- a/src/cljs/reagent_data_views/client/utils.cljs +++ b/src/cljs/reagent_data_views/client/utils.cljs @@ -4,17 +4,6 @@ [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.