General outline:
- Use a reagent atom locally defined inside the component to hold the integer value
reset!
the value in :on-change
and :on-click
In order to integrate with re-frame's app db, we need to add the following:
- call
re-frame/dispatch
in :on-change
and :on-click
re-frame/subscribe
to where the value is stored in re-frame's app db and update the local reagent atom accordingly
Let's look at some code, starting with the reagent component:
(defn integer-field [default-value]
(let [int-atom (atom default-value)]
(fn []
[:div
[:input {:type "text"
:value @int-atom
:on-change #(reset! int-atom (-> % .-target .-value))}]
[:button {:on-click #(adjust-int int-atom 1)} "+"]
[:button {:on-click #(adjust-int int-atom -1)}]])))
The trick is in reset!
-ing the atom every time we get an :on-change
event and letting reagent/react take care of re-rendering the field behind the scenes.
Also note that :on-click
calls another function adjust-int
that increments/decrements the value of the field-atom. Here it is:
(defn- adjust-int [int-atom delta]
(let [v (js/parseInt @int-atom)
valid? (not (js/Number.isNaN v))
new-v (+v delta)]
(when valid?
(reset! int-atom new-v))))
This works as is. However, we do need more code if we decide to store the integer value in re-frame. First, let's make a handler that stores values it receives from the input field in the db:
(re-frame/register-handler
:integer-input-field-updated
(fn [db [_ value]]
(assoc db :integer-value value)))
We now need to call (re-frame/dispatch :integer-input-field-updated value)
every time the value of the input field changes in addition to reset!
-ing the reagent atom. Let's add a helper function for it:
(defn- store-int-value [int-atom value]
(reset! int-atom value)
(re-frame/dispatch :integer-input-field-updated value))
We now need to call this function instead of reset!
every time the value is ready to be stored. Let's make that change in adjust-int
first:
(defn- adjust-int [int-atom delta]
(let [v (js/parseInt @int-atom)
valid? (not (js/Number.isNaN v))
new-v (+v delta)]
(when valid?
(store-int-value int-atom new-v)))) ;; <---- changed
Let's recap what we have so far:
- Input field and two buttons that change the value in the input field
- A reagent atom that stores the value and ties the UI elements together
- A place in re-frame's app db that stores the value
We need a way to support the flow of values in the other direction, from the app db into the local atom. We could hook up a subscription that reacts to changes to (:integer-value @db)
and resets our local atom. Wait, that sounds like some sort of a loop, doesn't it? Let's visualize the flow:
:on-click
(reset! int-atom)
-> re-render input field
(dispatch [:integer-input-field-updated new-value])
(assoc db :integer-value value)
- reagent reaction is triggered
(reset! int-atom)
-> re-render input field AGAIN
That's not cool. We need to make sure that the information is only flowing in one direction, meaning that changes to :integer-value
should not trigger changes to the local atom, but that changes to local atom do get propagated to :integer-value
. Basically, our UI shouldn't react to updates that it itself initiates. We can accomplish that by introducing another key in the db, re-frame/subscribe
to it, and force all of the updates to :integer-value
that aren't coming from the UI to update both. Let's call this key :integer-value-input-field
and make a sub and a handler for it:
(re-frame/register-sub
:integer-field-input-value
(fn [db _]
(reaction (:integer-field-input-value @db))))
(re-frame/register-handler
:integer-value-updated ;; <--- for use by other parts of the app
(fn [db [_ value]]
(assoc db :integer-field value :integer-field-input-value value)))
Finally, let's refactor the integer-field
component:
(defn integer-field [default-value]
(let [int-atom (atom default-value)
the-int (re-frame/subscribe [:integer-field-input-value])] ;; <--- source of truth
(fn []
(reset! int-atom @the-int) ;; <--- spread the truth
[:div
[:input {:type "text"
:value @int-atom
:on-change #(store-int-value int-atom(-> % .-target .-value))}]
[:button {:on-click #(adjust-int int-atom 1)} "+"]
[:button {:on-click #(adjust-int int-atom -1)}]])))
And now, ALL of the code in one place...
(re-frame/register-sub
:integer-field-input-value
(fn [db _]
(reaction (:integer-field-input-value @db))))
;; to be used by other parts of the app
(re-frame/register-handler
:integer-value-updated
(fn [db [_ value]]
(assoc db :integer-field value :integer-field-input-value value)))
;; to be used by the integer-field component
(re-frame/register-handler
:integer-input-field-updated
(fn [db [_ value]]
(assoc db :integer-value value)))
(defn- store-int-value [int-atom value]
(reset! int-atom value)
(re-frame/dispatch :integer-input-field-updated value))
(defn- adjust-int [int-atom delta]
(let [v (js/parseInt @int-atom)
valid? (not (js/Number.isNaN v))
new-v (+v delta)]
(when valid?
(store-int-value int-atom new-v))))
(defn integer-field [default-value]
(let [int-atom (atom default-value)
the-int (re-frame/subscribe [:integer-field-input-value])]
(fn []
(reset! int-atom @the-int)
[:div
[:input {:type "text"
:value @int-atom
:on-change #(store-int-value int-atom(-> % .-target .-value))}]
[:button {:on-click #(adjust-int int-atom 1)} "+"]
[:button {:on-click #(adjust-int int-atom -1)}]])))