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)
|
(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#))))]
|
|
||||||
|
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
|
(reagent.core/create-class
|
||||||
{:component-will-mount
|
{:component-will-mount
|
||||||
(fn [this#]
|
(fn [this#]
|
||||||
(let [current-view-sigs# (get-current-view-sigs# this#)]
|
(reagent-data-views.client.component/prepare-for-render! this#))
|
||||||
(reagent-data-views.client.component/subscribe! this# current-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-will-unmount
|
:component-will-unmount
|
||||||
(fn [this#]
|
(fn [this#]
|
||||||
(reagent-data-views.client.component/unsubscribe-all! 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
|
:component-did-update
|
||||||
(fn [this# old-argv#]
|
(fn [this# old-argv#]
|
||||||
(let [new-view-sigs# (get-current-view-sigs# this#)]
|
(reagent-data-views.client.component/update-subscriptions! this#))
|
||||||
(reagent-data-views.client.component/update-subscriptions! this# new-view-sigs#)))
|
|
||||||
|
|
||||||
:component-function
|
:component-function
|
||||||
(fn ~args
|
(fn ~args
|
||||||
~@body)}))))
|
~@body)})))
|
||||||
|
|
|
@ -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.")))))
|
|
|
@ -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]
|
||||||
|
|
|
@ -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.
|
||||||
|
|
Loading…
Reference in a new issue