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:
parent
a95ebef0bc
commit
f2012fdc29
|
@ -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)})))
|
||||
|
|
|
@ -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)))
|
|
@ -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]
|
||||
|
|
|
@ -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.
|
||||
|
|
Loading…
Reference in a new issue