reagent-project / reagent-forms

Bootstrap form components for Reagent
339 stars 78 forks source link

Computing values with re-frame #154

Open njj opened 5 years ago

njj commented 5 years ago

With the re-framing example, everything is handled through the events function:

(def events
  {:get (fn [path] @(re-frame/subscribe [:value path]))
   :save! (fn [path value] (re-frame/dispatch [:set-value path value]))
   :update! (fn [path save-fn value]
              ; save-fn should accept two arguments: old-value, new-value
              (re-frame/dispatch [:update-value save-fn path value]))
   :doc (fn [] @(re-frame/subscribe [:doc]))})

This works fine for initializing and updating values but what if we wanted to calculate the value of a non-editable field (similar to the BMI example). I have a field that comes from the backend that is non-editable and I want to show the calculation of that field to the user before sending it back over. Is there a way to update a specific field in the doc outside of the the events function setup?

yogthos commented 5 years ago

You can handle computed values via re-frame events, e.g:

(ns example.core
  (:require
   [reagent.core :as r]
   [re-frame.core :as re-frame]
   [reagent-forms.core :refer [bind-fields]]))

(re-frame/reg-event-db
 :init
 (fn [_ _]
   {:doc {}}))

(re-frame/reg-sub
 :doc
 (fn [db _]
   (:doc db)))

(re-frame/reg-sub
 :value
 :<- [:doc]
 (fn [doc [_ path]]
   (get-in doc path)))

(defmulti rule (fn [_ path _] path))

(defn bmi [{:keys [weight height] :as doc}]
  (assoc doc :bmi (/ weight (* height height))))

(defmethod rule [:height] [doc path value]
  (bmi doc))

(defmethod rule [:weight] [doc path value]
  (bmi doc))

(defmethod rule :default [doc path value]
  doc)

(re-frame/reg-event-db
 :set-value
 (fn [{:keys [doc] :as db} [_ path value]]
   (-> db
       (assoc-in (into [:doc] path) value)
       (update :doc rule path value))))

(def events
  {:get (fn [path] @(re-frame/subscribe [:value path]))
   :save! (fn [path value] (re-frame/dispatch [:set-value path value]))  
   :doc (fn [] @(re-frame/subscribe [:doc]))})

(defn row [label input]
  [:div
   [:div [:label label]]
   [:div input]])

(def form-template
  [:div
   [:h3 "BMI Calculator"]
   (row "Height" [:input {:field :numeric :id :height}])
   (row "Weight" [:input {:field :numeric :id :weight}])
   (row "BMI" [:label {:field :label :id :bmi}])])

(defn home-page []
  [:div [:h2 "Welcome to Reagent"]
   [bind-fields form-template events]])
njj commented 5 years ago

@yogthos Thanks this is super helpful! It might benefit others to have your re-frame documentation in the readme reflect this.

The only issue I have w/ this implementation is it assumes the only computed value is the BMI, and I may have several forms with varying computed values. Is there a recommended way to refactor and abstract this such that you can pass in a variable rule? I tried doing so on my own but ran into a ton of issues. Something a long the lines of:

(re-frame/reg-event-db
 :set-value
 (fn [{:keys [doc] :as db} [_ path value rule]]
   (-> db
       (assoc-in (into [:doc] path) value)
       (when rule
         (update :doc rule path value)))))
yogthos commented 5 years ago

Note that the is actually a multimethod that's keyed on the path with the default behavior of doing nothing. So, the pattern I would suggest would be to create rule defmethods for the paths that should trigger business rules to recalculate values.