3

I have a reagent component with an integer input field and two buttons "+1" and "-1". I'd like to make it possible for the user to:

  • enter an integer value directly in the input field
  • click on "+1" to increment the value in the input field by 1
  • click on "-1" to decrement the value in the input field by 1

In addition, when using re-frame, I'd like to be able to

  • save the value to re-frame's app db after it's entered or adjusted by clicking on one of the buttons
  • update the value of the input field if it changes in re-frame's app db (for example, after we fetch the value from some API server)

How would I go about doing that?

4

1 回答 1

6

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:

  1. :on-click
  2. (reset! int-atom) -> re-render input field
  3. (dispatch [:integer-input-field-updated new-value])
  4. (assoc db :integer-value value)
  5. reagent reaction is triggered
  6. (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)}]])))
于 2016-01-29T18:46:26.363 回答