How do I handle input elements in Clojure Re-Frame?
Asked Answered
B

1

8

I have a couple options, but both seem a bit laggy, and I'm thinking that there should be a better alternative. I just would like to be able to create forms, even dynamically create them (eg adding rows to a form from within my app), and have reagent/re-frame/react-appropriate access to the values of the different inputs.

Not sure if either of these are the best alternative though, since they both run functions after every :on-change...

Option #1 - update :on-change into the global atom

[:input {:value       @new-job-form
         :on-change   #(dispatch [:new-job-form (-> % .-target .-value)])}]

(reg-event-db
 :new-job-form
 (fn [db [_ v]]
   (assoc db :new-job-form v)))

Option #2 - update some local state, that only dispatches to the global atom :on-blur

(defn text-input
  "adapted from:
  https://yogthos.net/posts/2016-09-25-ReagentComponents.html

  The big idea is this holds local state, and pushes it to the global
  state only when necessary"
  [{:keys [sub-path disp]}]
  (r/with-let [value    (r/atom nil)
               focused? (r/atom false)]
    [:div
     [:input
      {:type      :text
       :on-focus  #(do (reset! value @(subscribe sub-path))
                       (reset! focused? true))
       :on-blur   #(do (dispatch (conj disp @value))
                       (reset! focused? false))
       :value     (if @focused? @value @(subscribe sub-path))
       :on-change #(reset! value (-> % .-target .-value))}]]))

The second option is slightly less laggy, but more laggy than just a raw text input...

EDIT:

Option #3 - for completeness, a slightly different flavor adapted from re-frame's TODOMVC

(defn text-input
  "adapted from re-frame's TODOMVC:
      https://github.com/Day8/re-frame/blob/master/examples/todomvc/src/todomvc/views.cljs

  note: this is one-way bound to the global atom, it doesn't subscribe to it"
  [{:keys [on-save on-stop props]}]
  (let [inner (r/atom "")]
    (fn [] [:input (merge props
                          {:type        "text"
                           :value       @inner
                           :on-blur     (on-save @inner)
                           :on-change   #(reset! inner (-> % .-target .-value))
                           :on-key-down #(case (.-which %)
                                           13 (on-save @inner) ; enter
                                           27 (on-stop) ; esc
                                           nil)})])))

[text-input {:on-save #(dispatch [:new-job-form {:path [:a]
                                                         :v    %}])
                     :on-stop #(js/console.log "stopp")
                     :props   {:placeholder "url"}}]
Bevan answered 14/10, 2016 at 2:51 Comment(3)
What do you mean by laggy ? Do you actually experience any visible drop in performance ? I've accomplished quite a lot using either of the two methods you described and I've never had any performance issues.Lexielexigraphy
@KubaBirecki yes, there's a visible drop in performance as the text box struggles to keep up with my typing. I have a fast computer, there's no other obvious reason why it should be slow.Bevan
I'm unclear on exactly what your requirements are, but I'd start by looking at the re-frame todomvc and how that adds todos, and see if that gets you started: github.com/Day8/re-frame/tree/master/examples/todomvcAdnah
W
2

Re-frame, and reagent+ React on a lower level, try to limit re-rendering to components that change. In your case, lagginess may result if another component (or the whole UI) re-renders in addition to the text field, which was the only thing that changed.

An example building on your "Option one":

(defn busy-wait [ms]
  (let [start (.getTime (js/Date.))]
    (while (< (.getTime (js/Date.)) (+ start ms)))))

(defn slow-component []
  (busy-wait 2000)
  (.log js/console "Ouch!")
  [:h2 "I was busy"])

(defn main-panel []
  (let [new-job-form (re-frame/subscribe [:new-job-form])
    (fn []
      [:div.container-fluid
        (slow-component)
        [:input       {:value    @new-job-form
         :on-change   #(dispatch [:new-job-form (-> % .-target .-value)])}]
;; etc

This leads to slow-component re-rendering every time text is entered, and is really laggy, since slow-component takes at least 2000 ms to render.

In the above case, a simple solution is to provide the slow-component as a function to re-frame, changing the call into a vector, i.e.:

[:div.container-fluid
  [slow-component]

This allows re-frame to see that the slow-component does not need re-rendering, as its data has not changed. We skip this reasoning, when we call the function ourselves in the original example:

[:div.container-fluid
  (slow-component)

A good practise is also to use Form-2 components when binding to subscriptions.

Wrack answered 1/7, 2017 at 10:1 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.