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.
This commit is contained in:
Gered 2014-12-28 14:51:35 -05:00
parent a95ebef0bc
commit f2012fdc29
4 changed files with 91 additions and 87 deletions

View file

@ -1,26 +1,42 @@
(ns reagent-data-views.client.component) (ns reagent-data-views.client.component)
(defmacro defvc (defmacro defvc
[component-name args view-sigs & body] "Defines a Reagent component that works the same as any other defined
`(defn ~component-name ~args using defn with the addition that view-cursor can be used in the
(let [gen-view-sigs# (fn ~args ~view-sigs) render function of these components to access data from subscribed
get-current-view-sigs# (fn [this#] views.
(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#)))
:component-will-unmount To subscribe to a view, simply call view-cursor with the signature of
(fn [this#] the view you want to subscribe to and read data from. view-cursor and
(reagent-data-views.client.component/unsubscribe-all! this#)) 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 :component-did-mount
(fn [this# old-argv#] (fn [this#]
(let [new-view-sigs# (get-current-view-sigs# this#)] ; invoked immediately after the initial render has occurred.
(reagent-data-views.client.component/update-subscriptions! this# new-view-sigs#))) ; 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 :component-will-unmount
(fn ~args (fn [this#]
~@body)})))) (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)})))

View file

@ -1,63 +1,62 @@
(ns reagent-data-views.client.component (ns reagent-data-views.client.component
(:require (:require
[clojure.set :refer [difference]]
[reagent.core :as r] [reagent.core :as r]
[reagent.impl.util :refer [reagent-component?]] [reagent.impl.util :refer [reagent-component?]]
[reagent-data-views.client.core :as views] [reagent-data-views.client.core :as views]
[reagent-data-views.client.utils :refer [diff update-component-state!]])) [reagent-data-views.client.utils :refer [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! (defn unsubscribe-all!
"Unsubscribes a component from all it's current view subscriptions. "Unsubscribes a component from all it's current view subscriptions.
NOTE: this function is only intended to be used internally by defvc." NOTE: this function is only intended to be used internally by defvc."
[this] [this]
(assert (reagent-component? this)) (assert (reagent-component? this))
(when-let [view-sigs (:view-sigs (r/state this))] (let [last-used-view-sigs (:last-used-view-sigs (r/state this))]
(update-component-state! this #(dissoc % :view-sigs)) (views/unsubscribe! last-used-view-sigs)
(views/unsubscribe! 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! (defn update-subscriptions!
"Updates a component's view subscriptions to match the new full list "Updates view subscriptions by checking what view-sigs were passed to
of view-sigs. Only changed view-sigs will cause a view subscription any view-cursor calls during the most recent render and comparing
change to occur. 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." NOTE: this function is only intended to be used internally by defvc."
[this new-view-sigs] [this]
(assert (reagent-component? this)) (assert (reagent-component? this))
(let [current-view-sigs (:view-sigs (r/state this))] (let [{:keys [used-view-sigs last-used-view-sigs]} (r/state this)]
(when (not= current-view-sigs new-view-sigs) (if (not= used-view-sigs last-used-view-sigs)
(let [sigs-to-sub (diff new-view-sigs current-view-sigs) (let [sigs-to-unsub (vec (difference last-used-view-sigs used-view-sigs))
sigs-to-unsub (diff current-view-sigs new-view-sigs)] sigs-to-sub (vec (difference used-view-sigs last-used-view-sigs))]
(update-component-state! this #(assoc % :view-sigs new-view-sigs)) (views/update-subscriptions! sigs-to-sub sigs-to-unsub)
(views/update-subscriptions! sigs-to-sub sigs-to-unsub))))) (r/set-state this {:used-view-sigs #{}
:last-used-view-sigs used-view-sigs})))))
(defn- get-views-by-name [view-name view-sigs]
(filter #(= view-name (first %)) view-sigs))
(defn view-cursor (defn view-cursor
"Returns a Reagent cursor that can be used to access the data for a view by "Returns a Reagent cursor that can be used to access the data for a view.
looking up the corresponding view-sig by name in the current component's If the view-sig is not currently subscribed to, the subscription will be
list of view subscriptions. 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 NOTE: The data returned by this function is intended to be used in a
(using different arguments) an error is thrown. read-only manner. Using this cursor to change the data will *not*
propagate the changes to the server."
NOTE: This function is intended to be used in a read-only manner. Using [view-sig]
this cursor to change the data will *not* propagate to the server or (let [this (r/current-component)]
any other clients currently subscribed to this view." (assert (not (nil? this)) "view-cursor can only be used within a defvc component's render function.")
[view-name] (update-component-state! this #(update-in % [:used-view-sigs] conj view-sig))
(assert (not (nil? (r/current-component)))) (views/->view-sig-cursor view-sig)))
(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.")))))

View file

@ -1,8 +1,7 @@
(ns reagent-data-views.client.core (ns reagent-data-views.client.core
(:require (:require
[reagent.core :as r] [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: ;; IMPORTANT NOTE:
;; We are using Reagent's built-in RCursor instead of the one provided by reagent-cursor ;; 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 {})) (defonce view-data (r/atom {}))
(defn view-sig-cursor (defn ->view-sig-cursor
"Returns a Reagent cursor that can be used to access the data for the view "Creates and returns a Reagent cursor that can be used to access the data
corresponding with the view-sig. for the view corresponding with the view-sig.
Generally, for code in a component's render function, you should use Generally, for code in a component's render function, you should use
reagent-data-views.client.component/view-cursor instead of using this 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 NOTE: The data returned by this function is intended to be used in a
this cursor to change the data will *not* propagate to the server or read-only manner. Using this cursor to change the data will *not*
any other clients currently subscribed to this view." propagate the changes to the server."
[view-sig] [view-sig]
(r/cursor [view-sig :data] view-data)) (r/cursor [view-sig :data] view-data))
@ -40,7 +40,7 @@
(get-in @view-data path))) (get-in @view-data path)))
(defn- add-initial-view-data! [view-sig data] (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))) (reset! cursor data)))
(defn- remove-view-data! [view-sig] (defn- remove-view-data! [view-sig]
@ -57,7 +57,7 @@
(concat existing-data insert-deltas)) (concat existing-data insert-deltas))
(defn- apply-deltas! [view-sig 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] (doseq [{:keys [refresh-set insert-deltas delete-deltas]} deltas]
(if refresh-set (reset! cursor refresh-set)) (if refresh-set (reset! cursor refresh-set))
(if (seq delete-deltas) (swap! cursor apply-delete-deltas delete-deltas)) (if (seq delete-deltas) (swap! cursor apply-delete-deltas delete-deltas))
@ -85,7 +85,7 @@
(defn subscribe! (defn subscribe!
"Subscribes to the specified view(s). Updates to the data on the server will "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)." render it in any component(s)."
[view-sigs] [view-sigs]
(doseq [view-sig view-sigs] (doseq [view-sig view-sigs]

View file

@ -4,17 +4,6 @@
[reagent.impl.component :as rcomp] [reagent.impl.component :as rcomp]
[reagent.impl.util :refer [reagent-component?]])) [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 ; 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 ; 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. ; the future. this function may need to be updated at that time.