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)
(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#))))]
"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.
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#]
(let [current-view-sigs# (get-current-view-sigs# this#)]
(reagent-data-views.client.component/subscribe! this# current-view-sigs#)))
(reagent-data-views.client.component/prepare-for-render! this#))
: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-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#]
(let [new-view-sigs# (get-current-view-sigs# this#)]
(reagent-data-views.client.component/update-subscriptions! this# new-view-sigs#)))
(reagent-data-views.client.component/update-subscriptions! this#))
:component-function
(fn ~args
~@body)}))))
~@body)})))

View file

@ -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)))

View file

@ -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]

View file

@ -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.