reagent-project / reagent-forms

Bootstrap form components for Reagent
339 stars 78 forks source link

Elaborate on documentation #103

Closed afhammad closed 6 years ago

afhammad commented 8 years ago

The documentation states:

The templates are eagerly evaluated, and you should always call the helper functions as in the example above instead of putting them in a vector. These will be replaced by Reagent components when the bind-field is called to compile the template.

  • Could you please explain why the need for the helper functions instead of plain vectors?
  • Also how the eager evaluation works and what this means to how we interact with templates?
yogthos commented 8 years ago

The library uses postwalk to go through each element in the template and create a Reagent component out of it. Specifically, it looks for vectors that contain a map with the :field key and creates appropriate widgets based on the type of the field. All other nodes in the structure are let unchanged.

This is the reason the helper functions need to be evaluated. If you have a function in the template then it will simply be left as is.

afhammad commented 8 years ago

Thanks @yogthos.

I'm not sure I understand the helper functions yet, since their output (a vector of dom elements) is what is passed in and not an unevaluated function. i.e how is:

(row "first name" [:input {:field :text :id :first-name}])

different from

[:div
  [:label "first name"]
  [:input {:field :text :id :first-name}]]

The reason i ask is because I'm having trouble toggling css classes reactively based on a ratom so i'm trying to understand how/when changes are re-rendered.

Assuming this is my template:

(defn template [state]
  [:div {:class (str "ui form" (when (:errors @state) " error"))}
   [:div {:class (when (get-in @state [:errors :slug]) " error")}
     [:label "Slug"]
     [:input {:field :text
                 :id :slug
                 :placeholder "slug"}]]

and parent

(defn render []
  (let [state (r/atom {})]
    [:div
     [bind-fields
      (template state)
      state]])

The above dynamic adding of "error" class doesn't work, even though adding console logs there show that the new values are reaching there.

afhammad commented 8 years ago

Defining a ratom inside the template function like so seems to do the trick (maybe the readme could do with an example of this?):

(defn template []
  (let [errors (r/atom {})]
    (fn []
      [:div {:class (str "ui form" (when-not (empty? @errors) " error"))}
       [:div {:class (when (:slug @errors) " error")}
         [:label "Slug"]
         [:input {:field :text
                     :id :slug
                     :placeholder "slug"}]])))

Obviously there's more to the template including a validation check that resets the errors atom.

afhammad commented 8 years ago

Actually that breaks the form binding...

yogthos commented 8 years ago

Unfortunately, when you update the atom externally the changes aren't guaranteed to be reflected. The way to add events would be by providing event handler functions as described here.

afhammad commented 8 years ago

Wouldn't that require me to run my validation every time anything changes on the form as opposed to only when the submit button is clicked? Not really ideal. How do others handle this scenario?

yogthos commented 8 years ago

One approach for validation would be to do it externally and rerun bind-fields after validation. That way the form would get reinitialized with the latest data.